mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 13:36:30 +03:00
Compare commits
4 Commits
flub-confi
...
fix/str-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d47d25c1e | ||
|
|
e4f9446e3c | ||
|
|
abbbd0cb67 | ||
|
|
ae9f52e001 |
@@ -1,12 +1,10 @@
|
|||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
executors:
|
executors:
|
||||||
default:
|
default:
|
||||||
docker:
|
docker:
|
||||||
- image: filecoin/rust:latest
|
- image: filecoin/rust:latest
|
||||||
working_directory: /mnt/crate
|
working_directory: /mnt/crate
|
||||||
doxygen:
|
|
||||||
docker:
|
|
||||||
- image: hrektts/doxygen
|
|
||||||
|
|
||||||
restore-workspace: &restore-workspace
|
restore-workspace: &restore-workspace
|
||||||
attach_workspace:
|
attach_workspace:
|
||||||
@@ -15,7 +13,7 @@ restore-workspace: &restore-workspace
|
|||||||
restore-cache: &restore-cache
|
restore-cache: &restore-cache
|
||||||
restore_cache:
|
restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
- cargo-v0-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||||
- repo-source-{{ .Branch }}-{{ .Revision }}
|
- repo-source-{{ .Branch }}-{{ .Revision }}
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
@@ -26,9 +24,20 @@ commands:
|
|||||||
steps:
|
steps:
|
||||||
- *restore-workspace
|
- *restore-workspace
|
||||||
- *restore-cache
|
- *restore-cache
|
||||||
|
- setup_remote_docker:
|
||||||
|
docker_layer_caching: true
|
||||||
|
# TODO: move into image
|
||||||
|
- run:
|
||||||
|
name: Install Docker client
|
||||||
|
command: |
|
||||||
|
set -x
|
||||||
|
VER="18.09.2"
|
||||||
|
curl -L -o /tmp/docker-$VER.tgz https://download.docker.com/linux/static/stable/x86_64/docker-$VER.tgz
|
||||||
|
tar -xz -C /tmp -f /tmp/docker-$VER.tgz
|
||||||
|
mv /tmp/docker/* /usr/bin
|
||||||
- run:
|
- run:
|
||||||
name: Test (<< parameters.target >>)
|
name: Test (<< parameters.target >>)
|
||||||
command: TARGET=<< parameters.target >> ci_scripts/run-rust-test.sh
|
command: TARGET=<< parameters.target >> ci/run.sh
|
||||||
no_output_timeout: 15m
|
no_output_timeout: 15m
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -44,23 +53,21 @@ jobs:
|
|||||||
command: cargo generate-lockfile
|
command: cargo generate-lockfile
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
- cargo-v0-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||||
- run: rustup install $(cat rust-toolchain)
|
- run: rustup install $(cat rust-toolchain)
|
||||||
- run: rustup default $(cat rust-toolchain)
|
- run: rustup default $(cat rust-toolchain)
|
||||||
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
|
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
|
||||||
- run: rustup component add --toolchain $(cat rust-toolchain) clippy-preview
|
|
||||||
- run: cargo update
|
- run: cargo update
|
||||||
- run: cargo fetch
|
- run: cargo fetch
|
||||||
- run: rustc +stable --version
|
- run: rustc +stable --version
|
||||||
- run: rustc +$(cat rust-toolchain) --version
|
- run: rustc +$(cat rust-toolchain) --version
|
||||||
# make sure this git repo doesn't grow too big
|
- run: rm -rf .git
|
||||||
- run: git gc
|
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: /mnt
|
root: /mnt
|
||||||
paths:
|
paths:
|
||||||
- crate
|
- crate
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
key: cargo-v0-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||||
paths:
|
paths:
|
||||||
- "~/.cargo"
|
- "~/.cargo"
|
||||||
- "~/.rustup"
|
- "~/.rustup"
|
||||||
@@ -95,7 +102,7 @@ jobs:
|
|||||||
- run: cargo fetch
|
- run: cargo fetch
|
||||||
- run:
|
- run:
|
||||||
name: Test
|
name: Test
|
||||||
command: TARGET=x86_64-apple-darwin ci_scripts/run-rust-test.sh
|
command: TARGET=x86_64-apple-darwin ci/run.sh
|
||||||
|
|
||||||
test_x86_64-unknown-linux-gnu:
|
test_x86_64-unknown-linux-gnu:
|
||||||
executor: default
|
executor: default
|
||||||
@@ -116,78 +123,42 @@ jobs:
|
|||||||
target: "aarch64-linux-android"
|
target: "aarch64-linux-android"
|
||||||
|
|
||||||
|
|
||||||
build_doxygen:
|
build_test_docs_wheel:
|
||||||
executor: doxygen
|
machine: True
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run: bash ci_scripts/run-doxygen.sh
|
# - run: docker pull deltachat/doxygen
|
||||||
- run: mkdir -p workspace/c-docs
|
- run: docker pull deltachat/coredeps
|
||||||
- run: cp -av deltachat-ffi/{html,xml} workspace/c-docs/
|
- run:
|
||||||
|
name: build docs, run tests and build wheels
|
||||||
|
command: ci_scripts/ci_run.sh
|
||||||
|
environment:
|
||||||
|
TESTS: 1
|
||||||
|
DOCS: 1
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: copying docs and wheels to workspace
|
||||||
|
command: |
|
||||||
|
mkdir -p workspace/python
|
||||||
|
# cp -av docs workspace/c-docs
|
||||||
|
cp -av python/.docker-tox/wheelhouse workspace/
|
||||||
|
cp -av python/doc/_build/ workspace/py-docs
|
||||||
|
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: workspace
|
root: workspace
|
||||||
paths:
|
paths:
|
||||||
- c-docs
|
# - c-docs
|
||||||
|
- py-docs
|
||||||
# build_test_docs_wheel:
|
- wheelhouse
|
||||||
# docker:
|
|
||||||
# - image: deltachat/coredeps
|
|
||||||
# environment:
|
|
||||||
# TESTS: 1
|
|
||||||
# DOCS: 1
|
|
||||||
# working_directory: /mnt/crate
|
|
||||||
# steps:
|
|
||||||
# - *restore-workspace
|
|
||||||
# - *restore-cache
|
|
||||||
# - run:
|
|
||||||
# name: build docs, run tests and build wheels
|
|
||||||
# command: ci_scripts/run-python.sh
|
|
||||||
# - run:
|
|
||||||
# name: copying docs and wheels to workspace
|
|
||||||
# command: |
|
|
||||||
# mkdir -p workspace/python
|
|
||||||
# # cp -av docs workspace/c-docs
|
|
||||||
# cp -av python/.docker-tox/wheelhouse workspace/
|
|
||||||
# cp -av python/doc/_build/ workspace/py-docs
|
|
||||||
# - persist_to_workspace:
|
|
||||||
# root: workspace
|
|
||||||
# paths:
|
|
||||||
# # - c-docs
|
|
||||||
# - py-docs
|
|
||||||
# - wheelhouse
|
|
||||||
|
|
||||||
remote_tests_rust:
|
|
||||||
machine: true
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: ci_scripts/remote_tests_rust.sh
|
|
||||||
|
|
||||||
remote_tests_python:
|
|
||||||
machine: true
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
#- attach_workspace:
|
|
||||||
# at: workspace
|
|
||||||
- run: ci_scripts/remote_tests_python.sh
|
|
||||||
# workspace/py-docs workspace/wheelhouse workspace/c-docs
|
|
||||||
|
|
||||||
upload_docs_wheels:
|
upload_docs_wheels:
|
||||||
machine: true
|
machine: True
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: workspace
|
at: workspace
|
||||||
- run: pyenv global 3.5.2
|
|
||||||
- run: ls -laR workspace
|
- run: ls -laR workspace
|
||||||
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
|
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse
|
||||||
|
|
||||||
clippy:
|
|
||||||
executor: default
|
|
||||||
steps:
|
|
||||||
- *restore-workspace
|
|
||||||
- *restore-cache
|
|
||||||
- run:
|
|
||||||
name: Run cargo clippy
|
|
||||||
command: cargo clippy --all
|
|
||||||
|
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
@@ -195,29 +166,19 @@ workflows:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
jobs:
|
jobs:
|
||||||
|
- build_test_docs_wheel
|
||||||
|
- upload_docs_wheels:
|
||||||
|
requires:
|
||||||
|
- build_test_docs_wheel
|
||||||
- cargo_fetch
|
- cargo_fetch
|
||||||
|
|
||||||
- remote_tests_rust
|
|
||||||
|
|
||||||
- remote_tests_python
|
|
||||||
|
|
||||||
# - upload_docs_wheels:
|
|
||||||
# requires:
|
|
||||||
# - build_test_docs_wheel
|
|
||||||
# - build_doxygen
|
|
||||||
- rustfmt:
|
- rustfmt:
|
||||||
requires:
|
requires:
|
||||||
- cargo_fetch
|
- cargo_fetch
|
||||||
- clippy:
|
|
||||||
requires:
|
|
||||||
- cargo_fetch
|
|
||||||
|
|
||||||
- build_doxygen
|
|
||||||
|
|
||||||
# Linux Desktop 64bit
|
# Linux Desktop 64bit
|
||||||
# - test_x86_64-unknown-linux-gnu:
|
- test_x86_64-unknown-linux-gnu:
|
||||||
# requires:
|
requires:
|
||||||
# - cargo_fetch
|
- cargo_fetch
|
||||||
|
|
||||||
# Linux Desktop 32bit
|
# Linux Desktop 32bit
|
||||||
# - test_i686-unknown-linux-gnu:
|
# - test_i686-unknown-linux-gnu:
|
||||||
|
|||||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -2,10 +2,6 @@
|
|||||||
# ensures this even if the user has not set core.autocrlf.
|
# ensures this even if the user has not set core.autocrlf.
|
||||||
* text=auto
|
* text=auto
|
||||||
|
|
||||||
# This directory contains email messages verbatim, and changing CRLF to
|
|
||||||
# LF will corrupt them.
|
|
||||||
test-data/* text=false
|
|
||||||
|
|
||||||
# binary files should be detected by git, however, to be sure, you can add them here explicitly
|
# binary files should be detected by git, however, to be sure, you can add them here explicitly
|
||||||
*.png binary
|
*.png binary
|
||||||
*.jpg binary
|
*.jpg binary
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -16,12 +16,5 @@ python/.tox
|
|||||||
*.egg-info
|
*.egg-info
|
||||||
__pycache__
|
__pycache__
|
||||||
python/src/deltachat/capi*.so
|
python/src/deltachat/capi*.so
|
||||||
python/.venv/
|
|
||||||
|
|
||||||
python/liveconfig*
|
python/liveconfig*
|
||||||
|
|
||||||
# ignore doxgen generated files
|
|
||||||
deltachat-ffi/html
|
|
||||||
deltachat-ffi/xml
|
|
||||||
|
|
||||||
.rsynclist
|
|
||||||
|
|||||||
182
CHANGELOG.md
182
CHANGELOG.md
@@ -1,182 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## next (pending)
|
|
||||||
|
|
||||||
- restructured (but not change) imap idle handling into own file. cc @link2xt
|
|
||||||
|
|
||||||
## 1.0.0-beta.12
|
|
||||||
|
|
||||||
- fix python bindings to use core for copying attachments to blobdir
|
|
||||||
and fix core to actually do it. @hpk42
|
|
||||||
|
|
||||||
## 1.0.0-beta.11
|
|
||||||
|
|
||||||
- trigger reconnect more often on imap error states. Should fix an
|
|
||||||
issue observed when trying to empty a folder. @hpk42
|
|
||||||
|
|
||||||
- un-split qr tests: we fixed qr-securejoin protocol flakyness
|
|
||||||
last weeks. @hpk42
|
|
||||||
|
|
||||||
## 1.0.0-beta.10
|
|
||||||
|
|
||||||
- fix grpid-determination from in-reply-to and references headers. @hpk42
|
|
||||||
|
|
||||||
- only send Autocrypt-gossip headers on encrypted messages. @dignifiedquire
|
|
||||||
|
|
||||||
- fix reply-to-encrypted message to also be encrypted. @hpk42
|
|
||||||
|
|
||||||
- remove last unsafe code from dc_receive_imf :) @hpk42
|
|
||||||
|
|
||||||
- add experimental new dc_chat_get_info_json FFI/API so that desktop devs
|
|
||||||
can play with using it. @jikstra
|
|
||||||
|
|
||||||
- fix encoding of subjects and attachment-filenames @hpk42
|
|
||||||
@dignifiedquire .
|
|
||||||
|
|
||||||
## 1.0.0-beta.9
|
|
||||||
|
|
||||||
- historic: we now use the mailparse crate and lettre-email to generate mime
|
|
||||||
messages. This got rid of mmime completely, the C2rust generated port of the libetpan
|
|
||||||
mime-parse -- IOW 22KLocs of cumbersome code removed! see
|
|
||||||
https://github.com/deltachat/deltachat-core-rust/pull/904#issuecomment-561163330
|
|
||||||
many thanks @dignifiedquire for making everybody's life easier
|
|
||||||
and @jonhoo (from rust-imap fame) for suggesting to use the mailparse crate :)
|
|
||||||
|
|
||||||
- lots of improvements and better error handling in many rust modules
|
|
||||||
thanks @link2xt @flub @r10s, @hpk42 and @dignifiedquire
|
|
||||||
|
|
||||||
- @r10s introduced a new device chat which has an initial
|
|
||||||
welcome message. See
|
|
||||||
https://c.delta.chat/classdc__context__t.html#a1a2aad98bd23c1d21ee42374e241f389
|
|
||||||
for the main new FFI-API.
|
|
||||||
|
|
||||||
- fix moving self-sent messages, thanks @r10s, @flub, @hpk42
|
|
||||||
|
|
||||||
- fix flakyness/sometimes-failing verified/join-protocols,
|
|
||||||
thanks @flub, @r10s, @hpk42
|
|
||||||
|
|
||||||
- fix reply-to-encrypted message to keep encryption
|
|
||||||
|
|
||||||
- new DC_EVENT_SECUREJOIN_MEMBER_ADDED event
|
|
||||||
|
|
||||||
- many little fixes and rustifications (@link2xt, @flub, @hpk42)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.0.0-beta.8
|
|
||||||
|
|
||||||
- now uses async-email/async-imap as the new base
|
|
||||||
which makes imap-idle interruptible and thus fixes
|
|
||||||
several issues around the imap thread being in zombie state .
|
|
||||||
thanks @dignifiedquire, @hpk42 and @link2xt.
|
|
||||||
|
|
||||||
- fixes imap-protocol parsing bugs that lead to infinitely
|
|
||||||
repeated crashing while trying to receive messages with
|
|
||||||
a subjec that contained non-utf8. thanks @link2xt
|
|
||||||
|
|
||||||
- fixed logic to find encryption subkey -- previously
|
|
||||||
delta chat would use the primary key for encryption
|
|
||||||
(which works with RSA but not ECC). thanks @link2xt
|
|
||||||
|
|
||||||
- introduce a new device chat where core and UIs can
|
|
||||||
add "device" messages. Android uses it for an initial
|
|
||||||
welcome message. thanks @r10s
|
|
||||||
|
|
||||||
- fix time smearing (when two message are virtually send
|
|
||||||
in the same second, there would be misbehaviour because
|
|
||||||
we didn't persist smeared time). thanks @r10s
|
|
||||||
|
|
||||||
- fix double-dotted extensions like .html.zip or .tar.gz
|
|
||||||
to not mangle them when creating blobfiles. thanks @flub
|
|
||||||
|
|
||||||
- fix backup/exports where the wrong sql file would be modified,
|
|
||||||
leading to problems when exporting twice. thanks @hpk42
|
|
||||||
|
|
||||||
- several other little fixes and improvements
|
|
||||||
|
|
||||||
|
|
||||||
## 1.0.0-beta.7
|
|
||||||
|
|
||||||
- fix location-streaming #782
|
|
||||||
|
|
||||||
- fix display of messages that could not be decrypted #785
|
|
||||||
|
|
||||||
- fix smtp MAILER-DAEMON bug #786
|
|
||||||
|
|
||||||
- fix a logging of durations #783
|
|
||||||
|
|
||||||
- add more error logging #779
|
|
||||||
|
|
||||||
- do not panic on some bad utf-8 mime #776
|
|
||||||
|
|
||||||
## 1.0.0-beta.6
|
|
||||||
|
|
||||||
- fix chatlist.get_msg_id to return id, instead of wrongly erroring
|
|
||||||
|
|
||||||
## 1.0.0-beta.5
|
|
||||||
|
|
||||||
- fix dc_get_msg() to return empty messages when asked for special ones
|
|
||||||
|
|
||||||
## 1.0.0-beta.4
|
|
||||||
|
|
||||||
- fix more than one sending of autocrypt setup message
|
|
||||||
|
|
||||||
- fix recognition of mailto-address-qr-codes, add tests
|
|
||||||
|
|
||||||
- tune down error to warning when adding self to chat
|
|
||||||
|
|
||||||
## 1.0.0-beta.3
|
|
||||||
|
|
||||||
- add back `dc_empty_server()` #682
|
|
||||||
|
|
||||||
- if `show_emails` is set to `DC_SHOW_EMAILS_ALL`,
|
|
||||||
email-based contact requests are added to the chatlist directly
|
|
||||||
|
|
||||||
- fix IMAP hangs #717 and cleanups
|
|
||||||
|
|
||||||
- several rPGP fixes
|
|
||||||
|
|
||||||
- code streamlining and rustifications
|
|
||||||
|
|
||||||
|
|
||||||
## 1.0.0-beta.2
|
|
||||||
|
|
||||||
- https://c.delta.chat docs are now regenerated again through our CI
|
|
||||||
|
|
||||||
- several rPGP cleanups, security fixes and better multi-platform support
|
|
||||||
|
|
||||||
- reconnect on io errors and broken pipes (imap)
|
|
||||||
|
|
||||||
- probe SMTP with real connection not just setup
|
|
||||||
|
|
||||||
- various imap/smtp related fixes
|
|
||||||
|
|
||||||
- use to_string_lossy in most places instead of relying on valid utf-8
|
|
||||||
encodings
|
|
||||||
|
|
||||||
- rework, rustify and test autoconfig-reading and parsing
|
|
||||||
|
|
||||||
- some rustifications/boolifications of c-ints
|
|
||||||
|
|
||||||
|
|
||||||
## 1.0.0-beta.1
|
|
||||||
|
|
||||||
- first beta of the Delta Chat Rust core library. many fixes of crashes
|
|
||||||
and other issues compared to 1.0.0-alpha.5.
|
|
||||||
|
|
||||||
- Most code is now "rustified" and does not do manual memory allocation anymore.
|
|
||||||
|
|
||||||
- The `DC_EVENT_GET_STRING` event is not used anymore, removing the last
|
|
||||||
event where the core requested a return value from the event callback.
|
|
||||||
|
|
||||||
Please now use `dc_set_stock_translation()` API for core messages
|
|
||||||
to be properly localized.
|
|
||||||
|
|
||||||
- Deltachat FFI docs are automatically generated and available here:
|
|
||||||
https://c.delta.chat
|
|
||||||
|
|
||||||
- New events ImapMessageMoved and ImapMessageDeleted
|
|
||||||
|
|
||||||
For a full list of changes, please see our closed Pull Requests:
|
|
||||||
|
|
||||||
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
|
||||||
|
|
||||||
2564
Cargo.lock
generated
2564
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
53
Cargo.toml
53
Cargo.toml
@@ -1,28 +1,29 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-alpha.3"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["dignifiedquire <dignifiedquire@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MPL"
|
license = "MPL"
|
||||||
|
|
||||||
[dependencies]
|
[build-dependencies]
|
||||||
deltachat_derive = { path = "./deltachat_derive" }
|
cc = "1.0.35"
|
||||||
|
pkg-config = "0.3"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
libc = "0.2.51"
|
libc = "0.2.51"
|
||||||
pgp = { git = "https://github.com/rpgp/rpgp", branch = "master", default-features = false }
|
pgp = { version = "0.2", default-features = false }
|
||||||
hex = "0.4.0"
|
hex = "0.3.2"
|
||||||
sha2 = "0.8.0"
|
sha2 = "0.8.0"
|
||||||
rand = "0.6.5"
|
rand = "0.6.5"
|
||||||
smallvec = "0.6.9"
|
smallvec = "0.6.9"
|
||||||
reqwest = { version = "0.9.15", default-features = false, features = ["rustls-tls"] }
|
reqwest = "0.9.15"
|
||||||
num-derive = "0.2.5"
|
num-derive = "0.2.5"
|
||||||
num-traits = "0.2.6"
|
num-traits = "0.2.6"
|
||||||
lettre = { git = "https://github.com/deltachat/lettre", branch = "feat/mail" }
|
native-tls = "0.2.3"
|
||||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "feat/mail" }
|
lettre = "0.9.0"
|
||||||
async-imap = { git = "https://github.com/async-email/async-imap", branch="master" }
|
imap = "1.0.1"
|
||||||
async-tls = "0.6"
|
mmime = "0.1.0"
|
||||||
async-std = { version = "1.0", features = ["unstable"] }
|
base64 = "0.10"
|
||||||
base64 = "0.11"
|
|
||||||
charset = "0.1"
|
charset = "0.1"
|
||||||
percent-encoding = "2.0"
|
percent-encoding = "2.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@@ -32,41 +33,27 @@ failure = "0.1.5"
|
|||||||
failure_derive = "0.1.5"
|
failure_derive = "0.1.5"
|
||||||
# TODO: make optional
|
# TODO: make optional
|
||||||
rustyline = "4.1.0"
|
rustyline = "4.1.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.3.0"
|
||||||
regex = "1.1.6"
|
regex = "1.1.6"
|
||||||
rusqlite = { version = "0.20", features = ["bundled"] }
|
rusqlite = { version = "0.20", features = ["bundled"] }
|
||||||
|
addr = "0.2.0"
|
||||||
r2d2_sqlite = "0.12.0"
|
r2d2_sqlite = "0.12.0"
|
||||||
r2d2 = "0.8.5"
|
r2d2 = "0.8.5"
|
||||||
strum = "0.16.0"
|
strum = "0.15.0"
|
||||||
strum_macros = "0.16.0"
|
strum_macros = "0.15.0"
|
||||||
thread-local-object = "0.1.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"
|
||||||
image-meta = "0.1.0"
|
|
||||||
quick-xml = "0.15.0"
|
|
||||||
escaper = "0.1.0"
|
|
||||||
bitflags = "1.1.0"
|
|
||||||
debug_stub_derive = "0.3.0"
|
|
||||||
sanitize-filename = "0.2.1"
|
|
||||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
|
||||||
rustls = "0.16.0"
|
|
||||||
webpki-roots = "0.18.0"
|
|
||||||
webpki = "0.21.0"
|
|
||||||
mailparse = "0.10.1"
|
|
||||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
|
||||||
derive_deref = "1.1.0"
|
|
||||||
|
|
||||||
[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"
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"deltachat-ffi",
|
"deltachat-ffi"
|
||||||
"deltachat_derive",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
@@ -80,6 +67,6 @@ path = "examples/repl/main.rs"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["nightly", "ringbuf"]
|
default = ["nightly", "ringbuf"]
|
||||||
vendored = []
|
vendored = ["native-tls/vendored", "reqwest/default-tls-vendored"]
|
||||||
nightly = ["pgp/nightly"]
|
nightly = ["pgp/nightly"]
|
||||||
ringbuf = ["pgp/ringbuf"]
|
ringbuf = ["pgp/ringbuf"]
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -1,9 +1,11 @@
|
|||||||
# Delta Chat Rust
|
# Delta Chat Rust
|
||||||
|
|
||||||
> Deltachat-core written in Rust
|
> Project porting deltachat-core to rust
|
||||||
|
|
||||||
[![CircleCI build status][circle-shield]][circle] [![Appveyor build status][appveyor-shield]][appveyor]
|
[![CircleCI build status][circle-shield]][circle] [![Appveyor build status][appveyor-shield]][appveyor]
|
||||||
|
|
||||||
|
Current commit on deltachat/deltachat-core: `12ef73c8e76185f9b78e844ea673025f56a959ab`.
|
||||||
|
|
||||||
## Installing Rust and Cargo
|
## Installing Rust and Cargo
|
||||||
|
|
||||||
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:
|
||||||
@@ -14,7 +16,7 @@ curl https://sh.rustup.rs -sSf | sh
|
|||||||
|
|
||||||
## Using the CLI client
|
## Using the CLI client
|
||||||
|
|
||||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
Compile and run Delta Chat Core using `cargo`:
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo run --example repl -- /path/to/db
|
cargo run --example repl -- /path/to/db
|
||||||
@@ -87,23 +89,6 @@ $ cargo test --all
|
|||||||
$ cargo build -p deltachat_ffi --release
|
$ cargo build -p deltachat_ffi --release
|
||||||
```
|
```
|
||||||
|
|
||||||
## Debugging environment variables
|
|
||||||
|
|
||||||
- `DCC_IMAP_DEBUG`: if set IMAP protocol commands and responses will be
|
|
||||||
printed
|
|
||||||
|
|
||||||
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Expensive tests
|
|
||||||
|
|
||||||
Some tests are expensive and marked with `#[ignore]`, to run these
|
|
||||||
use the `--ignored` argument to the test binary (not to cargo itself):
|
|
||||||
```sh
|
|
||||||
$ cargo test -- --ignored
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ install:
|
|||||||
build: false
|
build: false
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- cargo test --release --all
|
- cargo test --release
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
- target
|
- target
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.8 KiB |
@@ -1,83 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
inkscape:export-ydpi="409.60001"
|
|
||||||
inkscape:export-xdpi="409.60001"
|
|
||||||
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-device.png"
|
|
||||||
version="1.0"
|
|
||||||
width="60"
|
|
||||||
height="60"
|
|
||||||
viewBox="0 0 45 45"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
id="svg4344"
|
|
||||||
sodipodi:docname="icon-device.svg"
|
|
||||||
inkscape:version="1.0beta1 (32d4812, 2019-09-19)">
|
|
||||||
<defs
|
|
||||||
id="defs4348" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
inkscape:snap-global="false"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1600"
|
|
||||||
inkscape:window-height="1035"
|
|
||||||
id="namedview4346"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:zoom="3.959798"
|
|
||||||
inkscape:cx="28.322498"
|
|
||||||
inkscape:cy="24.898474"
|
|
||||||
inkscape:window-x="45"
|
|
||||||
inkscape:window-y="23"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg4344" />
|
|
||||||
<metadata
|
|
||||||
id="metadata4336">
|
|
||||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<rect
|
|
||||||
y="-4.4408921e-16"
|
|
||||||
x="0"
|
|
||||||
height="45"
|
|
||||||
width="45"
|
|
||||||
id="rect860"
|
|
||||||
style="opacity:1;fill:#76868b;fill-opacity:1;stroke-width:0.819271" />
|
|
||||||
<g
|
|
||||||
fill="#000000"
|
|
||||||
stroke="none"
|
|
||||||
style="fill:#ffffff;fill-opacity:1"
|
|
||||||
transform="matrix(0.00255113,0,0,-0.00255113,5.586152,38.200477)"
|
|
||||||
id="g4342">
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1"
|
|
||||||
d="m 8175,12765 c -703,-114 -1248,-608 -1387,-1258 -17,-82 -21,-136 -22,-277 0,-202 15,-307 70,-470 149,-446 499,-733 1009,-828 142,-26 465,-23 619,6 691,131 1201,609 1328,1244 31,158 31,417 0,565 -114,533 -482,889 -1038,1004 -133,27 -448,35 -579,14 z"
|
|
||||||
id="path4338"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1"
|
|
||||||
d="m 7070,9203 c -212,-20 -275,-27 -397,-48 -691,-117 -1400,-444 -2038,-940 -182,-142 -328,-270 -585,-517 -595,-571 -911,-974 -927,-1181 -6,-76 11,-120 69,-184 75,-80 159,-108 245,-79 109,37 263,181 632,595 539,606 774,826 1035,969 135,75 231,105 341,106 82,1 94,-2 138,-27 116,-68 161,-209 122,-376 -9,-36 -349,-868 -757,-1850 -407,-982 -785,-1892 -838,-2021 -287,-694 -513,-1389 -615,-1889 -70,-342 -90,-683 -52,-874 88,-440 381,-703 882,-792 124,-23 401,-30 562,-16 783,69 1674,461 2561,1125 796,596 1492,1354 1607,1751 43,146 -33,308 -168,360 -61,23 -100,15 -173,-36 -105,-74 -202,-170 -539,-529 -515,-551 -762,-783 -982,-927 -251,-164 -437,-186 -543,-65 -56,64 -74,131 -67,247 13,179 91,434 249,815 135,324 1588,4102 1646,4280 106,325 151,561 159,826 9,281 -22,463 -112,652 -58,122 -114,199 -211,292 -245,233 -582,343 -1044,338 -91,-1 -181,-3 -200,-5 z"
|
|
||||||
id="path4340"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,71 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
inkscape:export-ydpi="409.60001"
|
|
||||||
inkscape:export-xdpi="409.60001"
|
|
||||||
inkscape:export-filename="/home/kerle/test-icon.png"
|
|
||||||
version="1.0"
|
|
||||||
width="60"
|
|
||||||
height="60"
|
|
||||||
viewBox="0 0 45 45"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
id="svg4344"
|
|
||||||
sodipodi:docname="icon-saved-messages.svg"
|
|
||||||
inkscape:version="1.0beta1 (32d4812, 2019-09-19)">
|
|
||||||
<defs
|
|
||||||
id="defs4348" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1395"
|
|
||||||
inkscape:window-height="855"
|
|
||||||
id="namedview4346"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:zoom="4"
|
|
||||||
inkscape:cx="29.308676"
|
|
||||||
inkscape:cy="49.03624"
|
|
||||||
inkscape:window-x="89"
|
|
||||||
inkscape:window-y="108"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg4344"
|
|
||||||
inkscape:lockguides="false" />
|
|
||||||
<metadata
|
|
||||||
id="metadata4336">
|
|
||||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<rect
|
|
||||||
y="0"
|
|
||||||
x="0"
|
|
||||||
height="45"
|
|
||||||
width="45"
|
|
||||||
id="rect1420"
|
|
||||||
style="fill:#87aade;fill-opacity:1;stroke:none;stroke-width:0.968078" />
|
|
||||||
<path
|
|
||||||
id="rect846"
|
|
||||||
style="fill:#ffffff;stroke-width:0.58409804"
|
|
||||||
d="M 13.5,7.5 V 39 h 0.08654 L 22.533801,29.370239 31.482419,39 h 0.01758 V 7.5 Z m 9.004056,4.108698 1.879508,4.876388 5.039514,0.359779 -3.879358,3.363728 1.227764,5.095749 -4.276893,-2.796643 -4.280949,2.788618 1.237229,-5.093073 -3.873949,-3.371754 5.040866,-0.350417 z"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB |
38
build.rs
Normal file
38
build.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
extern crate cc;
|
||||||
|
|
||||||
|
fn link_static(lib: &str) {
|
||||||
|
println!("cargo:rustc-link-lib=static={}", lib);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_framework(fw: &str) {
|
||||||
|
println!("cargo:rustc-link-lib=framework={}", fw);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_search_path(p: &str) {
|
||||||
|
println!("cargo:rustc-link-search={}", p);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_tools() {
|
||||||
|
let mut config = cc::Build::new();
|
||||||
|
config.file("misc.c").compile("libtools.a");
|
||||||
|
|
||||||
|
println!("rerun-if-changed=build.rs");
|
||||||
|
println!("rerun-if-changed=misc.h");
|
||||||
|
println!("rerun-if-changed=misc.c");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
build_tools();
|
||||||
|
|
||||||
|
add_search_path("/usr/local/lib");
|
||||||
|
|
||||||
|
let target = std::env::var("TARGET").unwrap();
|
||||||
|
if target.contains("-apple") || target.contains("-darwin") {
|
||||||
|
link_framework("CoreFoundation");
|
||||||
|
link_framework("CoreServices");
|
||||||
|
link_framework("Security");
|
||||||
|
}
|
||||||
|
|
||||||
|
// local tools
|
||||||
|
link_static("tools");
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
#export RUST_TEST_THREADS=1
|
export RUST_TEST_THREADS=1
|
||||||
export RUST_BACKTRACE=1
|
export RUST_BACKTRACE=1
|
||||||
export RUSTFLAGS='--deny warnings'
|
export RUSTFLAGS='--deny warnings'
|
||||||
export OPT="--target=$TARGET"
|
export OPT="--target=$TARGET"
|
||||||
@@ -31,15 +31,16 @@ fi
|
|||||||
if [[ $NORUN == "1" ]]; then
|
if [[ $NORUN == "1" ]]; then
|
||||||
export CARGO_SUBCMD="build"
|
export CARGO_SUBCMD="build"
|
||||||
else
|
else
|
||||||
export CARGO_SUBCMD="test --all"
|
export CARGO_SUBCMD="test"
|
||||||
export OPT="${OPT} "
|
export OPT="${OPT} "
|
||||||
export OPT_RELEASE="${OPT_RELEASE} "
|
export OPT_RELEASE="${OPT_RELEASE} "
|
||||||
export OPT_RELEASE_IGNORED="${OPT_RELEASE} -- --ignored"
|
export OPT_RELEASE_IGNORED="${OPT_RELEASE} -- --ignored"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run all the test configurations
|
# Run all the test configurations:
|
||||||
# RUSTC_WRAPPER=SCCACHE seems to destroy parallelism / prolong the test
|
|
||||||
unset RUSTC_WRAPPER
|
|
||||||
$CARGO_CMD $CARGO_SUBCMD $OPT
|
$CARGO_CMD $CARGO_SUBCMD $OPT
|
||||||
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE
|
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE
|
||||||
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE_IGNORED
|
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE_IGNORED
|
||||||
|
|
||||||
|
# Build the ffi lib
|
||||||
|
$CARGO_CMD $CARGO_SUBCMD $OPT_FFI_RELEASE
|
||||||
@@ -1,46 +1,52 @@
|
|||||||
|
|
||||||
# Continuous Integration Scripts for Delta Chat
|
# Continuous Integration Scripts for Delta Chat
|
||||||
|
|
||||||
Continuous Integration, run through CircleCI and an own build machine.
|
Continuous Integration is run through CircleCI
|
||||||
|
but is largely independent of it.
|
||||||
## Description of scripts
|
|
||||||
|
|
||||||
- `../.circleci/config.yml` describing the build jobs that are run
|
|
||||||
by Circle-CI
|
|
||||||
|
|
||||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
|
||||||
`run-python-test.sh` remotely on the build machine.
|
|
||||||
|
|
||||||
- `remote_tests_rust.sh` rsyncs to the build machine and runs
|
|
||||||
`run-rust-test.sh` remotely on the build machine.
|
|
||||||
|
|
||||||
- `doxygen/Dockerfile` specifies an image that contains
|
|
||||||
the doxygen tool which is used by `run-doxygen.sh`
|
|
||||||
to generate C-docs which are then uploaded
|
|
||||||
via `ci_upload.sh` to `https://c.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
|
|
||||||
(and the master branch is linked to https://c.delta.chat proper).
|
|
||||||
|
|
||||||
|
|
||||||
## Triggering runs on the build machine locally (fast!)
|
## Generating docker containers for performing build step work
|
||||||
|
|
||||||
There is experimental support for triggering a remote Python or Rust test run
|
All tests, docs and wheel building is run in docker containers:
|
||||||
from your local checkout/branch. You will need to be authorized to login to
|
|
||||||
the build machine (ask your friendly sysadmin on #deltachat freenode) to type::
|
|
||||||
|
|
||||||
ci_scripts/manual_remote_tests.sh rust
|
- **coredeps/Dockerfile** specifies an image that contains all
|
||||||
ci_scripts/manual_remote_tests.sh python
|
of Delta Chat's core dependencies as linkable libraries.
|
||||||
|
It also serves to run python tests and build wheels
|
||||||
|
(binary packages for Python).
|
||||||
|
|
||||||
This will **rsync** your current checkout to the remote build machine
|
- **doxygen/Dockerfile** specifies an image that contains
|
||||||
(no need to commit before) and then run either rust or python tests.
|
the doxygen tool which is used to generate C-docs.
|
||||||
|
|
||||||
# Outdated files (for later re-use)
|
To run tests locally you can pull existing images from "docker.io",
|
||||||
|
the hub for sharing Docker images::
|
||||||
|
|
||||||
`coredeps/Dockerfile` specifies an image that contains all
|
docker pull deltachat/coredeps
|
||||||
of Delta Chat's core dependencies. It used to run
|
docker pull deltachat/doxygen
|
||||||
python tests and build wheels (binary packages for Python)
|
|
||||||
|
|
||||||
You can build the docker images yourself locally
|
or you can build the docker images yourself locally
|
||||||
to avoid the relatively large download::
|
to avoid the relatively large download::
|
||||||
|
|
||||||
cd ci_scripts # where all CI things are
|
cd ci_scripts # where all CI things are
|
||||||
docker build -t deltachat/coredeps docker-coredeps
|
docker build -t deltachat/coredeps docker-coredeps
|
||||||
docker build -t deltachat/doxygen docker-doxygen
|
docker build -t deltachat/doxygen docker-doxygen
|
||||||
|
|
||||||
|
## ci_run.sh (main entrypoint called by circle-ci)
|
||||||
|
|
||||||
|
Once you have the docker images available
|
||||||
|
you can run python testing, documentation generation
|
||||||
|
and building binary wheels::
|
||||||
|
|
||||||
|
sh DOCS=1 TESTS=1 ci_scripts/ci_run.sh
|
||||||
|
|
||||||
|
## ci_upload.sh (uploading artifacts on success)
|
||||||
|
|
||||||
|
- python docs to `https://py.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
|
||||||
|
|
||||||
|
- doxygen docs to `https://c.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
|
||||||
|
|
||||||
|
- python wheels to `https://m.devpi.net/dc/<BRANCH>`
|
||||||
|
so that you install fully self-contained wheels like this:
|
||||||
|
`pip install -U -i https://m.devpi.net/dc/<BRANCH> deltachat`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
22
ci_scripts/ci_run.sh
Executable file
22
ci_scripts/ci_run.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
# perform CI jobs on PRs and after merges to master.
|
||||||
|
# triggered from .circleci/config.yml
|
||||||
|
|
||||||
|
set -e -x
|
||||||
|
|
||||||
|
export BRANCH=${CIRCLE_BRANCH:-test7}
|
||||||
|
|
||||||
|
# run doxygen on c-source (needed by later doc-generation steps).
|
||||||
|
# XXX modifies the host filesystem docs/xml and docs/html directories
|
||||||
|
# XXX which you can then only remove with sudo as they belong to root
|
||||||
|
|
||||||
|
# XXX we don't do doxygen doc generation with Rust anymore, needs to be
|
||||||
|
# substituted with rust-docs
|
||||||
|
#if [ -n "$DOCS" ] ; then
|
||||||
|
# docker run --rm -it -v $PWD:/mnt -w /mnt/docs deltachat/doxygen doxygen
|
||||||
|
#fi
|
||||||
|
|
||||||
|
# run everything else inside docker (TESTS, DOCS, WHEELS)
|
||||||
|
docker run -e BRANCH -e TESTS -e DOCS \
|
||||||
|
--rm -it -v $(pwd):/mnt -w /mnt \
|
||||||
|
deltachat/coredeps ci_scripts/run_all.sh
|
||||||
|
|
||||||
@@ -7,30 +7,24 @@ fi
|
|||||||
|
|
||||||
set -xe
|
set -xe
|
||||||
|
|
||||||
|
#DOXYDOCDIR=${1:?directory where doxygen docs to be found}
|
||||||
PYDOCDIR=${1:?directory with python docs}
|
PYDOCDIR=${1:?directory with python docs}
|
||||||
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
|
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
|
||||||
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
|
|
||||||
|
|
||||||
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
|
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
|
||||||
|
|
||||||
|
|
||||||
# DISABLED: python docs to py.delta.chat
|
# python docs to py.delta.chat
|
||||||
#ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
|
|
||||||
#rsync -avz \
|
|
||||||
# -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
|
||||||
# "$PYDOCDIR/html/" \
|
|
||||||
# delta@py.delta.chat:build/${BRANCH}
|
|
||||||
|
|
||||||
# C docs to c.delta.chat
|
|
||||||
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@c.delta.chat mkdir -p build-c/${BRANCH}
|
|
||||||
rsync -avz \
|
rsync -avz \
|
||||||
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||||
"$DOXYDOCDIR/html/" \
|
"$PYDOCDIR/html/" \
|
||||||
delta@c.delta.chat:build-c/${BRANCH}
|
delta@py.delta.chat:build/${BRANCH}
|
||||||
|
|
||||||
exit 0
|
# C docs to c.delta.chat
|
||||||
|
#rsync -avz \
|
||||||
# OUTDATED -- for re-use from python release-scripts
|
# -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||||
|
# "$DOXYDOCDIR/html/" \
|
||||||
|
# delta@py.delta.chat:build-c/${BRANCH}
|
||||||
|
|
||||||
echo -----------------------
|
echo -----------------------
|
||||||
echo upload wheels
|
echo upload wheels
|
||||||
@@ -39,17 +33,15 @@ echo -----------------------
|
|||||||
# Bundle external shared libraries into the wheels
|
# Bundle external shared libraries into the wheels
|
||||||
pushd $WHEELHOUSEDIR
|
pushd $WHEELHOUSEDIR
|
||||||
|
|
||||||
pip3 install devpi-client
|
pip install devpi-client
|
||||||
devpi use https://m.devpi.net
|
devpi use https://m.devpi.net
|
||||||
devpi login dc --password $DEVPI_LOGIN
|
devpi login dc --password $DEVPI_LOGIN
|
||||||
|
|
||||||
N_BRANCH=${BRANCH//[\/]}
|
devpi use dc/$BRANCH || {
|
||||||
|
devpi index -c $BRANCH
|
||||||
devpi use dc/$N_BRANCH || {
|
devpi use dc/$BRANCH
|
||||||
devpi index -c $N_BRANCH
|
|
||||||
devpi use dc/$N_BRANCH
|
|
||||||
}
|
}
|
||||||
devpi index $N_BRANCH bases=/root/pypi
|
devpi index $BRANCH bases=/root/pypi
|
||||||
devpi upload deltachat*.whl
|
devpi upload deltachat*.whl
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
|||||||
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
||||||
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
||||||
|
|
||||||
|
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||||
|
|
||||||
|
# Install python tools (auditwheels,tox, ...)
|
||||||
|
ADD deps/build_python.sh /builder/build_python.sh
|
||||||
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
|
# Install Rust nightly
|
||||||
|
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||||
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
# Install a recent Perl, needed to install OpenSSL
|
# Install a recent Perl, needed to install OpenSSL
|
||||||
ADD deps/build_perl.sh /builder/build_perl.sh
|
ADD deps/build_perl.sh /builder/build_perl.sh
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||||
@@ -13,12 +23,3 @@ RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
|||||||
ADD deps/build_openssl.sh /builder/build_openssl.sh
|
ADD deps/build_openssl.sh /builder/build_openssl.sh
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_openssl.sh && cd .. && rm -r tmp1
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_openssl.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
|
||||||
|
|
||||||
# Install python tools (auditwheels,tox, ...)
|
|
||||||
ADD deps/build_python.sh /builder/build_python.sh
|
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
|
||||||
|
|
||||||
# Install Rust nightly
|
|
||||||
ADD deps/build_rust.sh /builder/build_rust.sh
|
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
PERL_VERSION=5.30.0
|
PERL_VERSION=5.28.0
|
||||||
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
||||||
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||||
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||||
tar -xzf perl-${PERL_VERSION}.tar.gz
|
tar xzf perl-${PERL_VERSION}.tar.gz
|
||||||
cd perl-${PERL_VERSION}
|
cd perl-${PERL_VERSION}
|
||||||
|
|
||||||
./Configure -de
|
./Configure -de
|
||||||
make
|
make
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -e -x
|
set -e -x
|
||||||
|
|
||||||
# Install Rust
|
# Install Rust
|
||||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-09-12 -y
|
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-04-19 -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
|
||||||
|
rm -rf /root/.rustup/toolchains/nightly-2019-04-19-x86_64-unknown-linux-gnu/share/
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -xe
|
|
||||||
export CIRCLE_JOB=remote_tests_${1:?need to specify 'rust' or 'python'}
|
|
||||||
export CIRCLE_BUILD_NUM=$USER
|
|
||||||
export CIRCLE_BRANCH=`git branch | grep \* | cut -d ' ' -f2`
|
|
||||||
export CIRCLE_PROJECT_REPONAME=$(basename `git rev-parse --show-toplevel`)
|
|
||||||
|
|
||||||
time bash ci_scripts/$CIRCLE_JOB.sh
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -Dwarnings
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_and_test:
|
|
||||||
name: Build and test
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
|
||||||
rust: [nightly]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- name: Install ${{ matrix.rust }}
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: check
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
if: matrix.rust == 'nightly'
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
args: --all --bins --examples --tests
|
|
||||||
|
|
||||||
- name: tests
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all
|
|
||||||
|
|
||||||
- name: tests ignored
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all --release -- --ignored
|
|
||||||
|
|
||||||
check_fmt:
|
|
||||||
name: Checking fmt and docs
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: nightly
|
|
||||||
override: true
|
|
||||||
components: rustfmt
|
|
||||||
|
|
||||||
- name: fmt
|
|
||||||
run: cargo fmt --all -- --check
|
|
||||||
|
|
||||||
# clippy_check:
|
|
||||||
# name: Clippy check
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
#
|
|
||||||
# steps:
|
|
||||||
# - uses: actions/checkout@v1
|
|
||||||
# - uses: actions-rs/toolchain@v1
|
|
||||||
# with:
|
|
||||||
# profile: minimal
|
|
||||||
# toolchain: nightly
|
|
||||||
# override: true
|
|
||||||
# components: clippy
|
|
||||||
#
|
|
||||||
# - name: clippy
|
|
||||||
# run: cargo clippy --all
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
|
||||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
|
||||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
|
||||||
|
|
||||||
# we construct the BUILDDIR such that we can easily share the
|
|
||||||
# CARGO_TARGET_DIR between runs ("..")
|
|
||||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
|
||||||
|
|
||||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
|
||||||
|
|
||||||
set -xe
|
|
||||||
|
|
||||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
|
||||||
git ls-files >.rsynclist
|
|
||||||
# we seem to need .git for setuptools_scm versioning
|
|
||||||
find .git >>.rsynclist
|
|
||||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
|
||||||
|
|
||||||
set +x
|
|
||||||
|
|
||||||
echo "--- Running $CIRCLE_JOB remotely"
|
|
||||||
|
|
||||||
ssh $SSHTARGET <<_HERE
|
|
||||||
set +x -e
|
|
||||||
cd $BUILDDIR
|
|
||||||
# let's share the target dir with our last run on this branch/job-type
|
|
||||||
# cargo will make sure to block/unblock us properly
|
|
||||||
export CARGO_TARGET_DIR=\`pwd\`/../target
|
|
||||||
export TARGET=release
|
|
||||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
|
||||||
|
|
||||||
#we rely on tox/virtualenv being available in the host
|
|
||||||
#rm -rf virtualenv venv
|
|
||||||
#virtualenv -q -p python3.7 venv
|
|
||||||
#source venv/bin/activate
|
|
||||||
#pip install -q tox virtualenv
|
|
||||||
|
|
||||||
set -x
|
|
||||||
which python
|
|
||||||
source \$HOME/venv/bin/activate
|
|
||||||
which python
|
|
||||||
|
|
||||||
bash ci_scripts/run-python-test.sh
|
|
||||||
_HERE
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
|
||||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
|
||||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
|
||||||
|
|
||||||
# we construct the BUILDDIR such that we can easily share the
|
|
||||||
# CARGO_TARGET_DIR between runs ("..")
|
|
||||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
|
||||||
|
|
||||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
|
||||||
git ls-files >.rsynclist
|
|
||||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
|
||||||
|
|
||||||
echo "--- Running $CIRCLE_JOB remotely"
|
|
||||||
|
|
||||||
ssh $SSHTARGET <<_HERE
|
|
||||||
set +x -e
|
|
||||||
cd $BUILDDIR
|
|
||||||
# let's share the target dir with our last run on this branch/job-type
|
|
||||||
# cargo will make sure to block/unblock us properly
|
|
||||||
export CARGO_TARGET_DIR=\`pwd\`/../target
|
|
||||||
export TARGET=x86_64-unknown-linux-gnu
|
|
||||||
export RUSTC_WRAPPER=sccache
|
|
||||||
|
|
||||||
bash ci_scripts/run-rust-test.sh
|
|
||||||
_HERE
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
cd deltachat-ffi
|
|
||||||
doxygen
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Run functional tests for Delta Chat core using the python bindings
|
|
||||||
# and tox/pytest.
|
|
||||||
|
|
||||||
set -e -x
|
|
||||||
|
|
||||||
# for core-building and python install step
|
|
||||||
export DCC_RS_TARGET=release
|
|
||||||
export DCC_RS_DEV=`pwd`
|
|
||||||
|
|
||||||
cd python
|
|
||||||
|
|
||||||
python install_python_bindings.py onlybuild
|
|
||||||
|
|
||||||
# remove and inhibit writing PYC files
|
|
||||||
rm -rf tests/__pycache__
|
|
||||||
rm -rf src/deltachat/__pycache__
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
|
||||||
|
|
||||||
# run python tests (tox invokes pytest to run tests in python/tests)
|
|
||||||
#TOX_PARALLEL_NO_SPINNER=1 tox -e lint,doc
|
|
||||||
tox -e lint
|
|
||||||
tox -e doc,py37
|
|
||||||
@@ -36,25 +36,15 @@ if [ -n "$TESTS" ]; then
|
|||||||
rm -rf src/deltachat/__pycache__
|
rm -rf src/deltachat/__pycache__
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
# run tox. The circle-ci project env-var-setting DCC_PY_LIVECONFIG
|
# run tox
|
||||||
# allows running of "liveconfig" tests but for speed reasons
|
tox --workdir "$TOXWORKDIR" -e lint,py27,py35,py36,py37,auditwheels
|
||||||
# we run them only for the highest python version we support
|
|
||||||
|
|
||||||
# we split out qr-tests run to minimize likelyness of flaky tests
|
|
||||||
# (some qr tests are pretty heavy in terms of send/received
|
|
||||||
# messages and rust's imap code likely has concurrency problems)
|
|
||||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
|
|
||||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
|
|
||||||
unset DCC_PY_LIVECONFIG
|
|
||||||
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
|
||||||
tox --workdir "$TOXWORKDIR" -e auditwheels
|
|
||||||
popd
|
popd
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# if [ -n "$DOCS" ]; then
|
if [ -n "$DOCS" ]; then
|
||||||
# echo -----------------------
|
echo -----------------------
|
||||||
# echo generating python docs
|
echo generating python docs
|
||||||
# echo -----------------------
|
echo -----------------------
|
||||||
# (cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
||||||
# fi
|
fi
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-alpha.3"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["dignifiedquire <dignifiedquire@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
@@ -16,14 +16,13 @@ crate-type = ["cdylib", "staticlib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat = { path = "../", default-features = false }
|
deltachat = { path = "../", default-features = false }
|
||||||
deltachat-provider-database = "0.2.1"
|
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
human-panic = "1.0.1"
|
human-panic = "1.0.1"
|
||||||
num-traits = "0.2.6"
|
num-traits = "0.2.6"
|
||||||
failure = "0.1.6"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored", "nightly", "ringbuf"]
|
default = ["vendored", "nightly", "ringbuf"]
|
||||||
vendored = ["deltachat/vendored"]
|
vendored = ["deltachat/vendored"]
|
||||||
nightly = ["deltachat/nightly"]
|
nightly = ["deltachat/nightly"]
|
||||||
ringbuf = ["deltachat/ringbuf"]
|
ringbuf = ["deltachat/ringbuf"]
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
/* the code snippet frame, defaults to white which tends to get badly readable in combination with explaining text around */
|
|
||||||
div.fragment {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
border: 0;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
@@ -1,10 +1 @@
|
|||||||
# Delta Chat C Interface
|
# Delta Chat C Interface
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
To generate the C Interface documentation,
|
|
||||||
call doxygen in the `deltachat-ffi` directory
|
|
||||||
and browse the `html` subdirectory.
|
|
||||||
|
|
||||||
If thinks work,
|
|
||||||
the documentation is also available online at <https://c.delta.chat>
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
|||||||
extern crate deltachat_provider_database;
|
|
||||||
|
|
||||||
use std::ptr;
|
|
||||||
|
|
||||||
use crate::string::{to_string_lossy, StrExt};
|
|
||||||
use deltachat_provider_database::StatusState;
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub type dc_provider_t = deltachat_provider_database::Provider;
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_new_from_domain(
|
|
||||||
domain: *const libc::c_char,
|
|
||||||
) -> *const dc_provider_t {
|
|
||||||
match deltachat_provider_database::get_provider_info(&to_string_lossy(domain)) {
|
|
||||||
Some(provider) => provider,
|
|
||||||
None => ptr::null(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_new_from_email(
|
|
||||||
email: *const libc::c_char,
|
|
||||||
) -> *const dc_provider_t {
|
|
||||||
let email = to_string_lossy(email);
|
|
||||||
let domain = deltachat_provider_database::get_domain_from_email(&email);
|
|
||||||
match deltachat_provider_database::get_provider_info(domain) {
|
|
||||||
Some(provider) => provider,
|
|
||||||
None => ptr::null(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! null_guard {
|
|
||||||
($context:tt) => {
|
|
||||||
if $context.is_null() {
|
|
||||||
return ptr::null_mut() as *mut libc::c_char;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_overview_page(
|
|
||||||
provider: *const dc_provider_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
format!(
|
|
||||||
"{}/{}",
|
|
||||||
deltachat_provider_database::PROVIDER_OVERVIEW_URL,
|
|
||||||
(*provider).overview_page
|
|
||||||
)
|
|
||||||
.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_name(provider: *const dc_provider_t) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
(*provider).name.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_markdown(
|
|
||||||
provider: *const dc_provider_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
(*provider).markdown.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_status_date(
|
|
||||||
provider: *const dc_provider_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
(*provider).status.date.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> u32 {
|
|
||||||
if provider.is_null() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
match (*provider).status.state {
|
|
||||||
StatusState::OK => 1,
|
|
||||||
StatusState::PREPARATION => 2,
|
|
||||||
StatusState::BROKEN => 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {
|
|
||||||
()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO expose general provider overview url?
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
use failure::Fail;
|
|
||||||
use std::ffi::{CStr, CString};
|
|
||||||
|
|
||||||
/// Duplicates a string
|
|
||||||
///
|
|
||||||
/// returns an empty string if NULL is given, never returns NULL (exits on errors)
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust,norun
|
|
||||||
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
|
|
||||||
/// unsafe {
|
|
||||||
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
|
|
||||||
/// let str_a_copy = dc_strdup(str_a);
|
|
||||||
/// assert_eq!(to_string_lossy(str_a_copy), "foobar");
|
|
||||||
/// assert_ne!(str_a, str_a_copy);
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
|
||||||
let ret: *mut libc::c_char;
|
|
||||||
if !s.is_null() {
|
|
||||||
ret = libc::strdup(s);
|
|
||||||
assert!(!ret.is_null());
|
|
||||||
} else {
|
|
||||||
ret = libc::calloc(1, 1) as *mut libc::c_char;
|
|
||||||
assert!(!ret.is_null());
|
|
||||||
}
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error type for the [OsStrExt] trait
|
|
||||||
#[derive(Debug, Fail, PartialEq)]
|
|
||||||
pub enum CStringError {
|
|
||||||
/// The string contains an interior null byte
|
|
||||||
#[fail(display = "String contains an interior null byte")]
|
|
||||||
InteriorNullByte,
|
|
||||||
/// The string is not valid Unicode
|
|
||||||
#[fail(display = "String is not valid unicode")]
|
|
||||||
NotUnicode,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extra convenience methods on [std::ffi::OsStr] to work with `*libc::c_char`.
|
|
||||||
///
|
|
||||||
/// The primary function of this trait is to more easily convert
|
|
||||||
/// [OsStr], [OsString] or [Path] into pointers to C strings. This always
|
|
||||||
/// allocates a new string since it is very common for the source
|
|
||||||
/// string not to have the required terminal null byte.
|
|
||||||
///
|
|
||||||
/// It is implemented for `AsRef<std::ffi::OsStr>>` trait, which
|
|
||||||
/// allows any type which implements this trait to transparently use
|
|
||||||
/// this. This is how the conversion for [Path] works.
|
|
||||||
///
|
|
||||||
/// [OsStr]: std::ffi::OsStr
|
|
||||||
/// [OsString]: std::ffi::OsString
|
|
||||||
/// [Path]: std::path::Path
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use deltachat::dc_tools::{dc_strdup, OsStrExt};
|
|
||||||
/// let path = std::path::Path::new("/some/path");
|
|
||||||
/// let path_c = path.to_c_string().unwrap();
|
|
||||||
/// unsafe {
|
|
||||||
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub trait OsStrExt {
|
|
||||||
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
|
|
||||||
///
|
|
||||||
/// This is useful to convert e.g. a [std::path::Path] to
|
|
||||||
/// [*libc::c_char] by using
|
|
||||||
/// [Path::as_os_str()](std::path::Path::as_os_str) and
|
|
||||||
/// [CStr::as_ptr()](std::ffi::CStr::as_ptr).
|
|
||||||
///
|
|
||||||
/// This returns [CString] and not [&CStr] because not all [OsStr]
|
|
||||||
/// slices end with a null byte, particularly those coming from
|
|
||||||
/// [Path] do not have a null byte and having to handle this as
|
|
||||||
/// the caller would defeat the point of this function.
|
|
||||||
///
|
|
||||||
/// On Windows this requires that the [OsStr] contains valid
|
|
||||||
/// unicode, which should normally be the case for a [Path].
|
|
||||||
///
|
|
||||||
/// [CString]: std::ffi::CString
|
|
||||||
/// [CStr]: std::ffi::CStr
|
|
||||||
/// [OsStr]: std::ffi::OsStr
|
|
||||||
/// [Path]: std::path::Path
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Since a C `*char` is terminated by a NULL byte this conversion
|
|
||||||
/// will fail, when the [OsStr] has an interior null byte. The
|
|
||||||
/// function will return
|
|
||||||
/// `[Err]([CStringError::InteriorNullByte])`. When converting
|
|
||||||
/// from a [Path] it should be safe to
|
|
||||||
/// [`.unwrap()`](std::result::Result::unwrap) this anyway since a
|
|
||||||
/// [Path] should not contain interior null bytes.
|
|
||||||
///
|
|
||||||
/// On windows when the string contains invalid Unicode
|
|
||||||
/// `[Err]([CStringError::NotUnicode])` is returned.
|
|
||||||
fn to_c_string(&self) -> Result<CString, CStringError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsRef<std::ffi::OsStr>> OsStrExt for T {
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn to_c_string(&self) -> Result<CString, CStringError> {
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
CString::new(self.as_ref().as_bytes()).map_err(|err| match err {
|
|
||||||
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn to_c_string(&self) -> Result<CString, CStringError> {
|
|
||||||
os_str_to_c_string_unicode(&self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation for os_str_to_c_string on windows.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn os_str_to_c_string_unicode(
|
|
||||||
os_str: &dyn AsRef<std::ffi::OsStr>,
|
|
||||||
) -> Result<CString, CStringError> {
|
|
||||||
match os_str.as_ref().to_str() {
|
|
||||||
Some(val) => CString::new(val.as_bytes()).map_err(|err| match err {
|
|
||||||
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
|
||||||
}),
|
|
||||||
None => Err(CStringError::NotUnicode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience methods/associated functions for working with [CString]
|
|
||||||
///
|
|
||||||
/// This is helps transitioning from unsafe code.
|
|
||||||
pub trait CStringExt {
|
|
||||||
/// Create a new [CString], yolo style
|
|
||||||
///
|
|
||||||
/// This unwrap the result, panicking when there are embedded NULL
|
|
||||||
/// bytes.
|
|
||||||
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
|
|
||||||
CString::new(t).expect("String contains null byte, can not be CString")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CStringExt for CString {}
|
|
||||||
|
|
||||||
/// Convenience methods to make transitioning from raw C strings easier.
|
|
||||||
///
|
|
||||||
/// To interact with (legacy) C APIs we often need to convert from
|
|
||||||
/// Rust strings to raw C strings. This can be clumsy to do correctly
|
|
||||||
/// and the compiler sometimes allows it in an unsafe way. These
|
|
||||||
/// methods make it more succinct and help you get it right.
|
|
||||||
pub trait StrExt {
|
|
||||||
/// Allocate a new raw C `*char` version of this string.
|
|
||||||
///
|
|
||||||
/// This allocates a new raw C string which must be freed using
|
|
||||||
/// `free`. It takes care of some common pitfalls with using
|
|
||||||
/// [CString.as_ptr].
|
|
||||||
///
|
|
||||||
/// [CString.as_ptr]: std::ffi::CString.as_ptr
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// This function will panic when the original string contains an
|
|
||||||
/// interior null byte as this can not be represented in raw C
|
|
||||||
/// strings.
|
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsRef<str>> StrExt for T {
|
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
|
||||||
let tmp = CString::yolo(self.as_ref());
|
|
||||||
dc_strdup(tmp.as_ptr())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
|
||||||
if s.is_null() {
|
|
||||||
return "".into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let cstr = unsafe { CStr::from_ptr(s) };
|
|
||||||
|
|
||||||
cstr.to_string_lossy().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
|
||||||
if s.is_null() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(to_string_lossy(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a C `*char` pointer to a [std::path::Path] slice.
|
|
||||||
///
|
|
||||||
/// This converts a `*libc::c_char` pointer to a [Path] slice. This
|
|
||||||
/// essentially has to convert the pointer to [std::ffi::OsStr] to do
|
|
||||||
/// so and thus is the inverse of [OsStrExt::to_c_string]. Just like
|
|
||||||
/// [OsStrExt::to_c_string] requires valid Unicode on Windows, this
|
|
||||||
/// requires that the pointer contains valid UTF-8 on Windows.
|
|
||||||
///
|
|
||||||
/// Because this returns a reference the [Path] silce can not outlive
|
|
||||||
/// the original pointer.
|
|
||||||
///
|
|
||||||
/// [Path]: std::path::Path
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
|
||||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
unsafe {
|
|
||||||
let c_str = std::ffi::CStr::from_ptr(s).to_bytes();
|
|
||||||
let os_str = std::ffi::OsStr::from_bytes(c_str);
|
|
||||||
std::path::Path::new(os_str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// as_path() implementation for windows, documented above.
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
|
||||||
as_path_unicode(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation for as_path() on Windows.
|
|
||||||
//
|
|
||||||
// Having this as a separate function means it can be tested on unix
|
|
||||||
// too.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
|
||||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
|
||||||
|
|
||||||
let cstr = unsafe { CStr::from_ptr(s) };
|
|
||||||
let str = cstr.to_str().unwrap_or_else(|err| panic!("{}", err));
|
|
||||||
|
|
||||||
std::path::Path::new(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use libc::{free, strcmp};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_os_str_to_c_string_cwd() {
|
|
||||||
let some_dir = std::env::current_dir().unwrap();
|
|
||||||
some_dir.as_os_str().to_c_string().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_os_str_to_c_string_unicode() {
|
|
||||||
let some_str = String::from("/some/valid/utf8");
|
|
||||||
let some_dir = std::path::Path::new(&some_str);
|
|
||||||
assert_eq!(
|
|
||||||
some_dir.as_os_str().to_c_string().unwrap(),
|
|
||||||
CString::new("/some/valid/utf8").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_os_str_to_c_string_nul() {
|
|
||||||
let some_str = std::ffi::OsString::from("foo\x00bar");
|
|
||||||
assert_eq!(
|
|
||||||
some_str.to_c_string().err().unwrap(),
|
|
||||||
CStringError::InteriorNullByte
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_path_to_c_string_cwd() {
|
|
||||||
let some_dir = std::env::current_dir().unwrap();
|
|
||||||
some_dir.to_c_string().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_path_to_c_string_unicode() {
|
|
||||||
let some_str = String::from("/some/valid/utf8");
|
|
||||||
let some_dir = std::path::Path::new(&some_str);
|
|
||||||
assert_eq!(
|
|
||||||
some_dir.as_os_str().to_c_string().unwrap(),
|
|
||||||
CString::new("/some/valid/utf8").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_os_str_to_c_string_unicode_fn() {
|
|
||||||
let some_str = std::ffi::OsString::from("foo");
|
|
||||||
assert_eq!(
|
|
||||||
os_str_to_c_string_unicode(&some_str).unwrap(),
|
|
||||||
CString::new("foo").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_path_to_c_string_unicode_fn() {
|
|
||||||
let some_str = String::from("/some/path");
|
|
||||||
let some_path = std::path::Path::new(&some_str);
|
|
||||||
assert_eq!(
|
|
||||||
os_str_to_c_string_unicode(&some_path).unwrap(),
|
|
||||||
CString::new("/some/path").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_os_str_to_c_string_unicode_fn_nul() {
|
|
||||||
let some_str = std::ffi::OsString::from("fooz\x00bar");
|
|
||||||
assert_eq!(
|
|
||||||
os_str_to_c_string_unicode(&some_str).err().unwrap(),
|
|
||||||
CStringError::InteriorNullByte
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_as_path() {
|
|
||||||
let some_path = CString::new("/some/path").unwrap();
|
|
||||||
let ptr = some_path.as_ptr();
|
|
||||||
assert_eq!(as_path(ptr), std::ffi::OsString::from("/some/path"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_as_path_unicode_fn() {
|
|
||||||
let some_path = CString::new("/some/path").unwrap();
|
|
||||||
let ptr = some_path.as_ptr();
|
|
||||||
assert_eq!(as_path_unicode(ptr), std::ffi::OsString::from("/some/path"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cstring_yolo() {
|
|
||||||
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_strdup_str() {
|
|
||||||
unsafe {
|
|
||||||
let s = "hello".strdup();
|
|
||||||
let cmp = strcmp(s, b"hello\x00" as *const u8 as *const libc::c_char);
|
|
||||||
free(s as *mut libc::c_void);
|
|
||||||
assert_eq!(cmp, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_strdup_string() {
|
|
||||||
unsafe {
|
|
||||||
let s = String::from("hello").strdup();
|
|
||||||
let cmp = strcmp(s, b"hello\x00" as *const u8 as *const libc::c_char);
|
|
||||||
free(s as *mut libc::c_void);
|
|
||||||
assert_eq!(cmp, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "deltachat_derive"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Dmitry Bogatov <KAction@debian.org>"]
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
syn = "0.14.4"
|
|
||||||
quote = "0.6.3"
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#![recursion_limit = "128"]
|
|
||||||
extern crate proc_macro;
|
|
||||||
|
|
||||||
use crate::proc_macro::TokenStream;
|
|
||||||
use quote::quote;
|
|
||||||
use syn;
|
|
||||||
|
|
||||||
// For now, assume (not check) that these macroses are applied to enum without
|
|
||||||
// data. If this assumption is violated, compiler error will point to
|
|
||||||
// generated code, which is not very user-friendly.
|
|
||||||
|
|
||||||
#[proc_macro_derive(ToSql)]
|
|
||||||
pub fn to_sql_derive(input: TokenStream) -> TokenStream {
|
|
||||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
|
||||||
let name = &ast.ident;
|
|
||||||
|
|
||||||
let gen = quote! {
|
|
||||||
impl rusqlite::types::ToSql for #name {
|
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
|
||||||
let num = *self as i64;
|
|
||||||
let value = rusqlite::types::Value::Integer(num);
|
|
||||||
let output = rusqlite::types::ToSqlOutput::Owned(value);
|
|
||||||
std::result::Result::Ok(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
gen.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_derive(FromSql)]
|
|
||||||
pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
|
||||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
|
||||||
let name = &ast.ident;
|
|
||||||
|
|
||||||
let gen = quote! {
|
|
||||||
impl rusqlite::types::FromSql for #name {
|
|
||||||
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
|
||||||
let inner = rusqlite::types::FromSql::column_result(col)?;
|
|
||||||
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
gen.into()
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Usage: cargo run --example repl --release -- <databasefile>
|
//! Usage: cargo run --example repl --release -- <databasefile>
|
||||||
//! All further options can be set using the set-command (type ? for help).
|
//! All further options can be set using the set-command (type ? for help).
|
||||||
|
#![feature(ptr_cast)]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate deltachat;
|
extern crate deltachat;
|
||||||
@@ -14,19 +15,19 @@ extern crate lazy_static;
|
|||||||
extern crate rusqlite;
|
extern crate rusqlite;
|
||||||
|
|
||||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||||
use std::io::{self, Write};
|
|
||||||
use std::path::Path;
|
|
||||||
use std::process::Command;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
|
||||||
use deltachat::config;
|
use deltachat::config;
|
||||||
use deltachat::configure::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::context::*;
|
use deltachat::context::*;
|
||||||
use deltachat::job::*;
|
use deltachat::dc_configure::*;
|
||||||
|
use deltachat::dc_job::*;
|
||||||
|
use deltachat::dc_securejoin::*;
|
||||||
|
use deltachat::dc_tools::*;
|
||||||
use deltachat::oauth2::*;
|
use deltachat::oauth2::*;
|
||||||
use deltachat::securejoin::*;
|
use deltachat::types::*;
|
||||||
use deltachat::Event;
|
use deltachat::x::*;
|
||||||
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;
|
||||||
@@ -41,74 +42,96 @@ use self::cmdline::*;
|
|||||||
|
|
||||||
// Event Handler
|
// Event Handler
|
||||||
|
|
||||||
fn receive_event(_context: &Context, event: Event) -> libc::uintptr_t {
|
unsafe extern "C" fn receive_event(
|
||||||
|
_context: &Context,
|
||||||
|
event: Event,
|
||||||
|
data1: uintptr_t,
|
||||||
|
data2: uintptr_t,
|
||||||
|
) -> uintptr_t {
|
||||||
match event {
|
match event {
|
||||||
Event::Info(msg) => {
|
Event::GET_STRING => {}
|
||||||
|
Event::INFO => {
|
||||||
/* do not show the event as this would fill the screen */
|
/* do not show the event as this would fill the screen */
|
||||||
println!("{}", msg);
|
println!("{}", to_string(data2 as *const _),);
|
||||||
}
|
}
|
||||||
Event::SmtpConnected(msg) => {
|
Event::SMTP_CONNECTED => {
|
||||||
println!("[DC_EVENT_SMTP_CONNECTED] {}", msg);
|
println!("[DC_EVENT_SMTP_CONNECTED] {}", to_string(data2 as *const _));
|
||||||
}
|
}
|
||||||
Event::ImapConnected(msg) => {
|
Event::IMAP_CONNECTED => {
|
||||||
println!("[DC_EVENT_IMAP_CONNECTED] {}", msg);
|
println!("[DC_EVENT_IMAP_CONNECTED] {}", to_string(data2 as *const _),);
|
||||||
}
|
}
|
||||||
Event::SmtpMessageSent(msg) => {
|
Event::SMTP_MESSAGE_SENT => {
|
||||||
println!("[DC_EVENT_SMTP_MESSAGE_SENT] {}", msg);
|
println!(
|
||||||
}
|
"[DC_EVENT_SMTP_MESSAGE_SENT] {}",
|
||||||
Event::Warning(msg) => {
|
to_string(data2 as *const _),
|
||||||
println!("[Warning] {}", msg);
|
|
||||||
}
|
|
||||||
Event::Error(msg) => {
|
|
||||||
println!("\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m", msg);
|
|
||||||
}
|
|
||||||
Event::ErrorNetwork(msg) => {
|
|
||||||
println!("\x1b[31m[DC_EVENT_ERROR_NETWORK] msg={}\x1b[0m", msg);
|
|
||||||
}
|
|
||||||
Event::ErrorSelfNotInGroup(msg) => {
|
|
||||||
println!("\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m", msg);
|
|
||||||
}
|
|
||||||
Event::MsgsChanged { chat_id, msg_id } => {
|
|
||||||
print!(
|
|
||||||
"\x1b[33m{{Received DC_EVENT_MSGS_CHANGED(chat_id={}, msg_id={})}}\n\x1b[0m",
|
|
||||||
chat_id, msg_id,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::ContactsChanged(_) => {
|
Event::WARNING => {
|
||||||
|
println!("[Warning] {}", to_string(data2 as *const _),);
|
||||||
|
}
|
||||||
|
Event::ERROR => {
|
||||||
|
println!(
|
||||||
|
"\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m",
|
||||||
|
to_string(data2 as *const _),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Event::ERROR_NETWORK => {
|
||||||
|
println!(
|
||||||
|
"\x1b[31m[DC_EVENT_ERROR_NETWORK] first={}, msg={}\x1b[0m",
|
||||||
|
data1 as usize,
|
||||||
|
to_string(data2 as *const _),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Event::ERROR_SELF_NOT_IN_GROUP => {
|
||||||
|
println!(
|
||||||
|
"\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m",
|
||||||
|
to_string(data2 as *const _),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Event::MSGS_CHANGED => {
|
||||||
|
print!(
|
||||||
|
"\x1b[33m{{Received DC_EVENT_MSGS_CHANGED({}, {})}}\n\x1b[0m",
|
||||||
|
data1 as usize, data2 as usize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Event::CONTACTS_CHANGED => {
|
||||||
print!("\x1b[33m{{Received DC_EVENT_CONTACTS_CHANGED()}}\n\x1b[0m");
|
print!("\x1b[33m{{Received DC_EVENT_CONTACTS_CHANGED()}}\n\x1b[0m");
|
||||||
}
|
}
|
||||||
Event::LocationChanged(contact) => {
|
Event::LOCATION_CHANGED => {
|
||||||
print!(
|
print!(
|
||||||
"\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={:?})}}\n\x1b[0m",
|
"\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={})}}\n\x1b[0m",
|
||||||
contact,
|
data1 as usize,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::ConfigureProgress(progress) => {
|
Event::CONFIGURE_PROGRESS => {
|
||||||
print!(
|
print!(
|
||||||
"\x1b[33m{{Received DC_EVENT_CONFIGURE_PROGRESS({} ‰)}}\n\x1b[0m",
|
"\x1b[33m{{Received DC_EVENT_CONFIGURE_PROGRESS({} ‰)}}\n\x1b[0m",
|
||||||
progress,
|
data1 as usize,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::ImexProgress(progress) => {
|
Event::IMEX_PROGRESS => {
|
||||||
print!(
|
print!(
|
||||||
"\x1b[33m{{Received DC_EVENT_IMEX_PROGRESS({} ‰)}}\n\x1b[0m",
|
"\x1b[33m{{Received DC_EVENT_IMEX_PROGRESS({} ‰)}}\n\x1b[0m",
|
||||||
progress,
|
data1 as usize,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::ImexFileWritten(file) => {
|
Event::IMEX_FILE_WRITTEN => {
|
||||||
print!(
|
print!(
|
||||||
"\x1b[33m{{Received DC_EVENT_IMEX_FILE_WRITTEN({})}}\n\x1b[0m",
|
"\x1b[33m{{Received DC_EVENT_IMEX_FILE_WRITTEN({})}}\n\x1b[0m",
|
||||||
file.display()
|
to_string(data1 as *const _)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::ChatModified(chat) => {
|
Event::CHAT_MODIFIED => {
|
||||||
print!(
|
print!(
|
||||||
"\x1b[33m{{Received DC_EVENT_CHAT_MODIFIED({})}}\n\x1b[0m",
|
"\x1b[33m{{Received DC_EVENT_CHAT_MODIFIED({})}}\n\x1b[0m",
|
||||||
chat
|
data1 as usize,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event);
|
print!(
|
||||||
|
"\x1b[33m{{Received {:?}({}, {})}}\n\x1b[0m",
|
||||||
|
event, data1 as usize, data2 as usize,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +173,13 @@ fn start_threads(c: Arc<RwLock<Context>>) {
|
|||||||
let ctx = c.clone();
|
let ctx = c.clone();
|
||||||
let handle_imap = std::thread::spawn(move || loop {
|
let handle_imap = std::thread::spawn(move || loop {
|
||||||
while_running!({
|
while_running!({
|
||||||
perform_inbox_jobs(&ctx.read().unwrap());
|
unsafe {
|
||||||
perform_inbox_fetch(&ctx.read().unwrap());
|
dc_perform_imap_jobs(&ctx.read().unwrap());
|
||||||
|
dc_perform_imap_fetch(&ctx.read().unwrap());
|
||||||
|
}
|
||||||
while_running!({
|
while_running!({
|
||||||
let context = ctx.read().unwrap();
|
let context = ctx.read().unwrap();
|
||||||
perform_inbox_idle(&context);
|
dc_perform_imap_idle(&context);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -162,9 +187,9 @@ fn start_threads(c: Arc<RwLock<Context>>) {
|
|||||||
let ctx = c.clone();
|
let ctx = c.clone();
|
||||||
let handle_mvbox = std::thread::spawn(move || loop {
|
let handle_mvbox = std::thread::spawn(move || loop {
|
||||||
while_running!({
|
while_running!({
|
||||||
perform_mvbox_fetch(&ctx.read().unwrap());
|
unsafe { dc_perform_mvbox_fetch(&ctx.read().unwrap()) };
|
||||||
while_running!({
|
while_running!({
|
||||||
perform_mvbox_idle(&ctx.read().unwrap());
|
unsafe { dc_perform_mvbox_idle(&ctx.read().unwrap()) };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -172,9 +197,9 @@ fn start_threads(c: Arc<RwLock<Context>>) {
|
|||||||
let ctx = c.clone();
|
let ctx = c.clone();
|
||||||
let handle_sentbox = std::thread::spawn(move || loop {
|
let handle_sentbox = std::thread::spawn(move || loop {
|
||||||
while_running!({
|
while_running!({
|
||||||
perform_sentbox_fetch(&ctx.read().unwrap());
|
unsafe { dc_perform_sentbox_fetch(&ctx.read().unwrap()) };
|
||||||
while_running!({
|
while_running!({
|
||||||
perform_sentbox_idle(&ctx.read().unwrap());
|
unsafe { dc_perform_sentbox_idle(&ctx.read().unwrap()) };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -182,9 +207,9 @@ fn start_threads(c: Arc<RwLock<Context>>) {
|
|||||||
let ctx = c;
|
let ctx = c;
|
||||||
let handle_smtp = std::thread::spawn(move || loop {
|
let handle_smtp = std::thread::spawn(move || loop {
|
||||||
while_running!({
|
while_running!({
|
||||||
perform_smtp_jobs(&ctx.read().unwrap());
|
unsafe { dc_perform_smtp_jobs(&ctx.read().unwrap()) };
|
||||||
while_running!({
|
while_running!({
|
||||||
perform_smtp_idle(&ctx.read().unwrap());
|
unsafe { dc_perform_smtp_idle(&ctx.read().unwrap()) };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -202,10 +227,12 @@ fn stop_threads(context: &Context) {
|
|||||||
println!("Stopping threads");
|
println!("Stopping threads");
|
||||||
IS_RUNNING.store(false, Ordering::Relaxed);
|
IS_RUNNING.store(false, Ordering::Relaxed);
|
||||||
|
|
||||||
interrupt_inbox_idle(context, true);
|
unsafe {
|
||||||
interrupt_mvbox_idle(context);
|
dc_interrupt_imap_idle(context);
|
||||||
interrupt_sentbox_idle(context);
|
dc_interrupt_mvbox_idle(context);
|
||||||
interrupt_smtp_idle(context);
|
dc_interrupt_sentbox_idle(context);
|
||||||
|
dc_interrupt_smtp_idle(context);
|
||||||
|
}
|
||||||
|
|
||||||
handle.handle_imap.take().unwrap().join().unwrap();
|
handle.handle_imap.take().unwrap().join().unwrap();
|
||||||
handle.handle_mvbox.take().unwrap().join().unwrap();
|
handle.handle_mvbox.take().unwrap().join().unwrap();
|
||||||
@@ -235,7 +262,7 @@ impl Completer for DcHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const IMEX_COMMANDS: [&str; 12] = [
|
const IMEX_COMMANDS: [&'static str; 12] = [
|
||||||
"initiate-key-transfer",
|
"initiate-key-transfer",
|
||||||
"get-setupcodebegin",
|
"get-setupcodebegin",
|
||||||
"continue-key-transfer",
|
"continue-key-transfer",
|
||||||
@@ -250,7 +277,7 @@ const IMEX_COMMANDS: [&str; 12] = [
|
|||||||
"stop",
|
"stop",
|
||||||
];
|
];
|
||||||
|
|
||||||
const DB_COMMANDS: [&str; 11] = [
|
const DB_COMMANDS: [&'static str; 11] = [
|
||||||
"info",
|
"info",
|
||||||
"open",
|
"open",
|
||||||
"close",
|
"close",
|
||||||
@@ -264,7 +291,7 @@ const DB_COMMANDS: [&str; 11] = [
|
|||||||
"housekeeping",
|
"housekeeping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHAT_COMMANDS: [&str; 24] = [
|
const CHAT_COMMANDS: [&'static str; 24] = [
|
||||||
"listchats",
|
"listchats",
|
||||||
"listarchived",
|
"listarchived",
|
||||||
"chat",
|
"chat",
|
||||||
@@ -290,7 +317,7 @@ const CHAT_COMMANDS: [&str; 24] = [
|
|||||||
"unarchive",
|
"unarchive",
|
||||||
"delchat",
|
"delchat",
|
||||||
];
|
];
|
||||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
const MESSAGE_COMMANDS: [&'static str; 8] = [
|
||||||
"listmsgs",
|
"listmsgs",
|
||||||
"msginfo",
|
"msginfo",
|
||||||
"listfresh",
|
"listfresh",
|
||||||
@@ -300,7 +327,7 @@ const MESSAGE_COMMANDS: [&str; 8] = [
|
|||||||
"unstar",
|
"unstar",
|
||||||
"delmsg",
|
"delmsg",
|
||||||
];
|
];
|
||||||
const CONTACT_COMMANDS: [&str; 6] = [
|
const CONTACT_COMMANDS: [&'static str; 6] = [
|
||||||
"listcontacts",
|
"listcontacts",
|
||||||
"listverified",
|
"listverified",
|
||||||
"addcontact",
|
"addcontact",
|
||||||
@@ -308,8 +335,8 @@ const CONTACT_COMMANDS: [&str; 6] = [
|
|||||||
"delcontact",
|
"delcontact",
|
||||||
"cleanupcontacts",
|
"cleanupcontacts",
|
||||||
];
|
];
|
||||||
const MISC_COMMANDS: [&str; 9] = [
|
const MISC_COMMANDS: [&'static str; 8] = [
|
||||||
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
|
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "help",
|
||||||
];
|
];
|
||||||
|
|
||||||
impl Hinter for DcHelper {
|
impl Hinter for DcHelper {
|
||||||
@@ -334,8 +361,8 @@ impl Hinter for DcHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static COLORED_PROMPT: &str = "\x1b[1;32m> \x1b[0m";
|
static COLORED_PROMPT: &'static str = "\x1b[1;32m> \x1b[0m";
|
||||||
static PROMPT: &str = "> ";
|
static PROMPT: &'static str = "> ";
|
||||||
|
|
||||||
impl Highlighter for DcHelper {
|
impl Highlighter for DcHelper {
|
||||||
fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
|
fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
|
||||||
@@ -362,15 +389,21 @@ impl Highlighter for DcHelper {
|
|||||||
impl Helper for DcHelper {}
|
impl Helper for DcHelper {}
|
||||||
|
|
||||||
fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
|
fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
|
||||||
if args.len() < 2 {
|
let mut context = dc_context_new(
|
||||||
|
Some(receive_event),
|
||||||
|
0 as *mut libc::c_void,
|
||||||
|
Some("CLI".into()),
|
||||||
|
);
|
||||||
|
|
||||||
|
unsafe { dc_cmdline_skip_auth() };
|
||||||
|
|
||||||
|
if args.len() == 2 {
|
||||||
|
if unsafe { !dc_open(&mut context, &args[1], None) } {
|
||||||
|
println!("Error: Cannot open {}.", args[0],);
|
||||||
|
}
|
||||||
|
} else if args.len() != 1 {
|
||||||
println!("Error: Bad arguments, expected [db-name].");
|
println!("Error: Bad arguments, expected [db-name].");
|
||||||
return Err(format_err!("No db-name specified"));
|
|
||||||
}
|
}
|
||||||
let context = Context::new(
|
|
||||||
Box::new(receive_event),
|
|
||||||
"CLI".into(),
|
|
||||||
Path::new(&args[1]).to_path_buf(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
println!("Delta Chat Core is awaiting your commands.");
|
println!("Delta Chat Core is awaiting your commands.");
|
||||||
|
|
||||||
@@ -403,7 +436,7 @@ fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
|
|||||||
// TODO: ignore "set mail_pw"
|
// TODO: ignore "set mail_pw"
|
||||||
rl.add_history_entry(line.as_str());
|
rl.add_history_entry(line.as_str());
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
match handle_cmd(line.trim(), ctx) {
|
match unsafe { handle_cmd(line.trim(), ctx) } {
|
||||||
Ok(ExitResult::Continue) => {}
|
Ok(ExitResult::Continue) => {}
|
||||||
Ok(ExitResult::Exit) => break,
|
Ok(ExitResult::Exit) => break,
|
||||||
Err(err) => println!("Error: {}", err),
|
Err(err) => println!("Error: {}", err),
|
||||||
@@ -423,6 +456,12 @@ fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
|
|||||||
println!("history saved");
|
println!("history saved");
|
||||||
{
|
{
|
||||||
stop_threads(&ctx.read().unwrap());
|
stop_threads(&ctx.read().unwrap());
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let mut ctx = ctx.write().unwrap();
|
||||||
|
dc_close(&mut ctx);
|
||||||
|
dc_context_unref(&mut ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -434,10 +473,15 @@ enum ExitResult {
|
|||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failure::Error> {
|
unsafe fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failure::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();
|
||||||
|
let arg1_c = if arg1.is_empty() {
|
||||||
|
std::ptr::null()
|
||||||
|
} else {
|
||||||
|
arg1.strdup()
|
||||||
|
};
|
||||||
|
|
||||||
match arg0 {
|
match arg0 {
|
||||||
"connect" => {
|
"connect" => {
|
||||||
@@ -450,19 +494,19 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
|
|||||||
if HANDLE.clone().lock().unwrap().is_some() {
|
if HANDLE.clone().lock().unwrap().is_some() {
|
||||||
println!("smtp-jobs are already running in a thread.",);
|
println!("smtp-jobs are already running in a thread.",);
|
||||||
} else {
|
} else {
|
||||||
perform_smtp_jobs(&ctx.read().unwrap());
|
dc_perform_smtp_jobs(&ctx.read().unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"imap-jobs" => {
|
"imap-jobs" => {
|
||||||
if HANDLE.clone().lock().unwrap().is_some() {
|
if HANDLE.clone().lock().unwrap().is_some() {
|
||||||
println!("inbox-jobs are already running in a thread.");
|
println!("imap-jobs are already running in a thread.");
|
||||||
} else {
|
} else {
|
||||||
perform_inbox_jobs(&ctx.read().unwrap());
|
dc_perform_imap_jobs(&ctx.read().unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"configure" => {
|
"configure" => {
|
||||||
start_threads(ctx.clone());
|
start_threads(ctx.clone());
|
||||||
configure(&ctx.read().unwrap());
|
dc_configure(&ctx.read().unwrap());
|
||||||
}
|
}
|
||||||
"oauth2" => {
|
"oauth2" => {
|
||||||
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
|
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
|
||||||
@@ -486,33 +530,38 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
|
|||||||
}
|
}
|
||||||
"getqr" | "getbadqr" => {
|
"getqr" | "getbadqr" => {
|
||||||
start_threads(ctx.clone());
|
start_threads(ctx.clone());
|
||||||
if let Some(mut qr) =
|
let qrstr =
|
||||||
dc_get_securejoin_qr(&ctx.read().unwrap(), arg1.parse().unwrap_or_default())
|
dc_get_securejoin_qr(&ctx.read().unwrap(), arg1.parse().unwrap_or_default());
|
||||||
{
|
if !qrstr.is_null() && 0 != *qrstr.offset(0isize) as libc::c_int {
|
||||||
if !qr.is_empty() {
|
if arg0 == "getbadqr" && strlen(qrstr) > 40 {
|
||||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
let mut i: libc::c_int = 12i32;
|
||||||
qr.replace_range(12..22, "0000000000")
|
while i < 22i32 {
|
||||||
|
*qrstr.offset(i as isize) = '0' as i32 as libc::c_char;
|
||||||
|
i += 1
|
||||||
}
|
}
|
||||||
println!("{}", qr);
|
|
||||||
let output = Command::new("qrencode")
|
|
||||||
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
|
||||||
.output()
|
|
||||||
.expect("failed to execute process");
|
|
||||||
io::stdout().write_all(&output.stdout).unwrap();
|
|
||||||
io::stderr().write_all(&output.stderr).unwrap();
|
|
||||||
}
|
}
|
||||||
|
println!("{}", to_string(qrstr as *const _));
|
||||||
|
let syscmd = dc_mprintf(
|
||||||
|
b"qrencode -t ansiutf8 \"%s\" -o -\x00" as *const u8 as *const libc::c_char,
|
||||||
|
qrstr,
|
||||||
|
);
|
||||||
|
system(syscmd);
|
||||||
|
free(syscmd as *mut libc::c_void);
|
||||||
}
|
}
|
||||||
|
free(qrstr as *mut libc::c_void);
|
||||||
}
|
}
|
||||||
"joinqr" => {
|
"joinqr" => {
|
||||||
start_threads(ctx.clone());
|
start_threads(ctx.clone());
|
||||||
if !arg0.is_empty() {
|
if !arg0.is_empty() {
|
||||||
dc_join_securejoin(&ctx.read().unwrap(), arg1);
|
dc_join_securejoin(&ctx.read().unwrap(), arg1_c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
"exit" => return Ok(ExitResult::Exit),
|
||||||
_ => dc_cmdline(&ctx.read().unwrap(), line)?,
|
_ => dc_cmdline(&ctx.read().unwrap(), line)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
free(arg1_c as *mut _);
|
||||||
|
|
||||||
Ok(ExitResult::Continue)
|
Ok(ExitResult::Continue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,115 +1,149 @@
|
|||||||
extern crate deltachat;
|
extern crate deltachat;
|
||||||
|
|
||||||
|
use std::ffi::CStr;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::{thread, time};
|
use std::{thread, time};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
use deltachat::chat;
|
|
||||||
use deltachat::chatlist::*;
|
use deltachat::chatlist::*;
|
||||||
use deltachat::config;
|
use deltachat::config;
|
||||||
use deltachat::configure::*;
|
use deltachat::constants::Event;
|
||||||
use deltachat::contact::*;
|
use deltachat::contact::*;
|
||||||
use deltachat::context::*;
|
use deltachat::context::*;
|
||||||
use deltachat::job::{
|
use deltachat::dc_chat::*;
|
||||||
perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle,
|
use deltachat::dc_configure::*;
|
||||||
perform_smtp_jobs,
|
use deltachat::dc_job::{
|
||||||
|
dc_perform_imap_fetch, dc_perform_imap_idle, dc_perform_imap_jobs, dc_perform_smtp_idle,
|
||||||
|
dc_perform_smtp_jobs,
|
||||||
};
|
};
|
||||||
use deltachat::Event;
|
use deltachat::dc_lot::*;
|
||||||
|
|
||||||
fn cb(_ctx: &Context, event: Event) -> usize {
|
extern "C" fn cb(_ctx: &Context, event: Event, data1: usize, data2: usize) -> usize {
|
||||||
print!("[{:?}]", event);
|
println!("[{:?}]", event);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::ConfigureProgress(progress) => {
|
Event::CONFIGURE_PROGRESS => {
|
||||||
print!(" progress: {}\n", progress);
|
println!(" progress: {}", data1);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
|
Event::INFO | Event::WARNING | Event::ERROR | Event::ERROR_NETWORK => {
|
||||||
print!(" {}\n", msg);
|
println!(
|
||||||
0
|
" {}",
|
||||||
}
|
unsafe { CStr::from_ptr(data2 as *const _) }
|
||||||
_ => {
|
.to_str()
|
||||||
print!("\n");
|
.unwrap()
|
||||||
|
);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let dir = tempdir().unwrap();
|
unsafe {
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
let ctx = dc_context_new(Some(cb), std::ptr::null_mut(), None);
|
||||||
println!("creating database {:?}", dbfile);
|
let running = Arc::new(RwLock::new(true));
|
||||||
let ctx =
|
let info = dc_get_info(&ctx);
|
||||||
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context");
|
let info_s = CStr::from_ptr(info);
|
||||||
let running = Arc::new(RwLock::new(true));
|
let duration = time::Duration::from_millis(4000);
|
||||||
let info = ctx.get_info();
|
println!("info: {}", info_s.to_str().unwrap());
|
||||||
let duration = time::Duration::from_millis(4000);
|
|
||||||
println!("info: {:#?}", info);
|
|
||||||
|
|
||||||
let ctx = Arc::new(ctx);
|
|
||||||
let ctx1 = ctx.clone();
|
|
||||||
let r1 = running.clone();
|
|
||||||
let t1 = thread::spawn(move || {
|
|
||||||
while *r1.read().unwrap() {
|
|
||||||
perform_inbox_jobs(&ctx1);
|
|
||||||
if *r1.read().unwrap() {
|
|
||||||
perform_inbox_fetch(&ctx1);
|
|
||||||
|
|
||||||
|
let ctx = Arc::new(ctx);
|
||||||
|
let ctx1 = ctx.clone();
|
||||||
|
let r1 = running.clone();
|
||||||
|
let t1 = thread::spawn(move || {
|
||||||
|
while *r1.read().unwrap() {
|
||||||
|
dc_perform_imap_jobs(&ctx1);
|
||||||
if *r1.read().unwrap() {
|
if *r1.read().unwrap() {
|
||||||
perform_inbox_idle(&ctx1);
|
dc_perform_imap_fetch(&ctx1);
|
||||||
|
|
||||||
|
if *r1.read().unwrap() {
|
||||||
|
dc_perform_imap_idle(&ctx1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
let ctx1 = ctx.clone();
|
let ctx1 = ctx.clone();
|
||||||
let r1 = running.clone();
|
let r1 = running.clone();
|
||||||
let t2 = thread::spawn(move || {
|
let t2 = thread::spawn(move || {
|
||||||
while *r1.read().unwrap() {
|
while *r1.read().unwrap() {
|
||||||
perform_smtp_jobs(&ctx1);
|
dc_perform_smtp_jobs(&ctx1);
|
||||||
if *r1.read().unwrap() {
|
if *r1.read().unwrap() {
|
||||||
perform_smtp_idle(&ctx1);
|
dc_perform_smtp_idle(&ctx1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let dbfile = dir.path().join("db.sqlite");
|
||||||
|
|
||||||
|
println!("opening database {:?}", dbfile);
|
||||||
|
|
||||||
|
assert!(dc_open(&ctx, dbfile.to_str().unwrap(), None));
|
||||||
|
|
||||||
|
println!("configuring");
|
||||||
|
let args = std::env::args().collect::<Vec<String>>();
|
||||||
|
assert_eq!(args.len(), 2, "missing password");
|
||||||
|
let pw = args[1].clone();
|
||||||
|
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
|
||||||
|
.unwrap();
|
||||||
|
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
|
||||||
|
dc_configure(&ctx);
|
||||||
|
|
||||||
|
thread::sleep(duration);
|
||||||
|
|
||||||
|
println!("sending a message");
|
||||||
|
let contact_id =
|
||||||
|
Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
|
||||||
|
let chat_id = dc_create_chat_by_contact_id(&ctx, contact_id);
|
||||||
|
dc_send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into());
|
||||||
|
|
||||||
|
println!("fetching chats..");
|
||||||
|
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
|
||||||
|
|
||||||
|
for i in 0..chats.len() {
|
||||||
|
let summary = chats.get_summary(0, std::ptr::null_mut());
|
||||||
|
let text1 = dc_lot_get_text1(summary);
|
||||||
|
let text2 = dc_lot_get_text2(summary);
|
||||||
|
|
||||||
|
let text1_s = if !text1.is_null() {
|
||||||
|
Some(CStr::from_ptr(text1))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let text2_s = if !text2.is_null() {
|
||||||
|
Some(CStr::from_ptr(text2))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
println!("chat: {} - {:?} - {:?}", i, text1_s, text2_s,);
|
||||||
|
dc_lot_unref(summary);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
println!("configuring");
|
thread::sleep(duration);
|
||||||
let args = std::env::args().collect::<Vec<String>>();
|
|
||||||
assert_eq!(args.len(), 2, "missing password");
|
|
||||||
let pw = args[1].clone();
|
|
||||||
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
|
|
||||||
.unwrap();
|
|
||||||
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
|
|
||||||
configure(&ctx);
|
|
||||||
|
|
||||||
thread::sleep(duration);
|
// let msglist = dc_get_chat_msgs(&ctx, chat_id, 0, 0);
|
||||||
|
// for i in 0..dc_array_get_cnt(msglist) {
|
||||||
|
// let msg_id = dc_array_get_id(msglist, i);
|
||||||
|
// let msg = dc_get_msg(context, msg_id);
|
||||||
|
// let text = CStr::from_ptr(dc_msg_get_text(msg)).unwrap();
|
||||||
|
// println!("Message {}: {}\n", i + 1, text.to_str().unwrap());
|
||||||
|
// dc_msg_unref(msg);
|
||||||
|
// }
|
||||||
|
// dc_array_unref(msglist);
|
||||||
|
|
||||||
println!("sending a message");
|
println!("stopping threads");
|
||||||
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
|
|
||||||
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
|
|
||||||
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
|
|
||||||
|
|
||||||
println!("fetching chats..");
|
*running.clone().write().unwrap() = false;
|
||||||
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
|
deltachat::dc_job::dc_interrupt_imap_idle(&ctx);
|
||||||
|
deltachat::dc_job::dc_interrupt_smtp_idle(&ctx);
|
||||||
|
|
||||||
for i in 0..chats.len() {
|
println!("joining");
|
||||||
let summary = chats.get_summary(&ctx, 0, None);
|
t1.join().unwrap();
|
||||||
let text1 = summary.get_text1();
|
t2.join().unwrap();
|
||||||
let text2 = summary.get_text2();
|
|
||||||
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
|
println!("closing");
|
||||||
|
dc_close(&ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
thread::sleep(duration);
|
|
||||||
|
|
||||||
println!("stopping threads");
|
|
||||||
|
|
||||||
*running.write().unwrap() = false;
|
|
||||||
deltachat::job::interrupt_inbox_idle(&ctx, true);
|
|
||||||
deltachat::job::interrupt_smtp_idle(&ctx);
|
|
||||||
|
|
||||||
println!("joining");
|
|
||||||
t1.join().unwrap();
|
|
||||||
t2.join().unwrap();
|
|
||||||
|
|
||||||
println!("closing");
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
misc.c
Normal file
52
misc.c
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "misc.h"
|
||||||
|
|
||||||
|
|
||||||
|
static char* internal_dc_strdup(const char* s) /* strdup(NULL) is undefined, save_strdup(NULL) returns an empty string in this case */
|
||||||
|
{
|
||||||
|
char* ret = NULL;
|
||||||
|
if (s) {
|
||||||
|
if ((ret=strdup(s))==NULL) {
|
||||||
|
exit(16); /* cannot allocate (little) memory, unrecoverable error */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ((ret=(char*)calloc(1, 1))==NULL) {
|
||||||
|
exit(17); /* cannot allocate little memory, unrecoverable error */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* dc_mprintf(const char* format, ...)
|
||||||
|
{
|
||||||
|
char testbuf[1];
|
||||||
|
char* buf = NULL;
|
||||||
|
int char_cnt_without_zero = 0;
|
||||||
|
|
||||||
|
va_list argp;
|
||||||
|
va_list argp_copy;
|
||||||
|
va_start(argp, format);
|
||||||
|
va_copy(argp_copy, argp);
|
||||||
|
|
||||||
|
char_cnt_without_zero = vsnprintf(testbuf, 0, format, argp);
|
||||||
|
va_end(argp);
|
||||||
|
if (char_cnt_without_zero < 0) {
|
||||||
|
va_end(argp_copy);
|
||||||
|
return internal_dc_strdup("ErrFmt");
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = malloc(char_cnt_without_zero+2 /* +1 would be enough, however, protect against off-by-one-errors */);
|
||||||
|
if (buf==NULL) {
|
||||||
|
va_end(argp_copy);
|
||||||
|
return internal_dc_strdup("ErrMem");
|
||||||
|
}
|
||||||
|
|
||||||
|
vsnprintf(buf, char_cnt_without_zero+1, format, argp_copy);
|
||||||
|
va_end(argp_copy);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
1
misc.h
Normal file
1
misc.h
Normal file
@@ -0,0 +1 @@
|
|||||||
|
char* dc_mprintf (const char* format, ...); /* The result must be free()'d. */
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Seeds for failure cases proptest has generated in the past. It is
|
|
||||||
# automatically read and these particular cases re-run before any
|
|
||||||
# novel cases are generated.
|
|
||||||
#
|
|
||||||
# It is recommended to check this file in to source control so that
|
|
||||||
# everyone who runs the test benefits from these saved cases.
|
|
||||||
cc 03cab93c6d1f3a8245f63cf84dacb307944294fe6333c1e38f078a6600659c7a # shrinks to data = "a\t0aA\ta\t0 \ta\t0 \ta a\t\ta A\tAA0a0a 0\t a\t aA \t a\t A0\t AAa\taA0\taAAaA\t0\taa0a\ta Aa Aaaa A0A\t a aA 0\t A\t0\t0\t\t\t\t\t\tA \t\t a\tA Aa aAA0A0AA0aaA A\t\t aa0\ta\t \tAa\taA\t00 AA A a\tA a aAAa \t 00\t0 \t\t a A 0\t\t\t aAA Aa \taAAAA0a A0A\t\t1\\E$\t$R\tc\t^=\t\"\tQ<Uk\t\t>A\t\t&\t}v&\tM^\'|\tW5?dn\t\t+\t\tP\te`\t\t>:Brlq--?\t$#Q\tK=zJ\tb\"9*.\"\t`\tF&T*\tBs,\tg\'*\\\t:\t?l$\t\t|A\"HR:Hk\t\\KkV\t\t{=&!^e%:|_*0wV\t[$`\t:\t$%f\t\t[!\"Y. \tP\t\th\'?\'/?%:++NfQo#@\"+?\t(\\??{\t\'\'$Dzj\t0.?{s4?&?Y=/yj]Z=\t4n\t?Ja\"\t{I\t$\t;I}_8V\t&\t?N\'\tI2/\t9.\tFT%9%`\'\tz\to7Y\t|AXP&@G12g\t\'w\t\t%??\t\"h$?F\"\"6%q\\\\{\tT\t\"]$87$}h\'\t<\t$\tc%U:mT2:<v\t#Rl!;U\t\t\"^D\tRZ(BZ{n\t%[$fgH`\t{B}:*\t\t%%*F`%W\t//B}PQ\t\tsu2\tLz<1*!p-X\tnKv&&0\thm4<n\\.\\/.w\'\t<)E1g3#ood\t`?\t\\({N?,8_?h\ty\t0%\t*$A\t\t*w-ViQUj\tTiH\t%\t%&0p\'\'\tA%r**Fo\'Z\\\tNI*ely4=I?}$.o`\t$\ts\'<\t\",:~=Nv/0O%=+<LS\t%P\'\t$r%d.\t{G{/L:\t&I&8-`cy*\"{\t/%fP9.P<\t\t\'/`\t\t`\t\t`!t:\t::\t\tW\'X^\t@.uL&a\tN\t\t\t.\t?0*\tvUK>UJ\\\tQbj%w]P=Js\t\"R\t&s^?</=%\t\'VI:\" kT`{b*<\t\tF&?Y\t\t:/\t!C\'e0`\t\t\tx-\t*\\N\\wwu.I4\tb/y\t\"P:P%\"\t\tQ][\\\t^x\t\t):\t\t&s\t$1-\t\t\tXt`0\t;\t/UeCP*\"G\t\t\':\tk@91Hf&#\t(Uzz&`p2;\t{]\t\"I_%=\\%,XD\"\'06QfCd<;$:q^\t8:\"*\"H\t\to\t&xK/\t\ty0/<}<j<|*`~::b\t=S.=}=Ki\t<Y.\'{\tf\t{Ub*H%(_4*?*\tn2\t/\'\t\t\t/,4]\tt\t<y\t\t\tWi\t\tT&\"\t\t\t\t\t=/1Wu?\t\'A\"W-P\t$?e\\\t`\t6`vD\t8`\t\tccD J\tY&jI//?_\t\\j\t_\tsiq$\t?9\tQ\t.^%&..?%Jm??A%+<\tN&*\t.g\tS$W\"\"\tMpq\t\t:&\\\thTz&<iz%/%T&\'F\t\\]&\t\t}\t\t\tXr=dtp`:+<,\t%60Y*<(&K*kJ\todO\t=<V?&\tMU/\"\t= Y!)<\tV\t9\t)\t&v8q{\t\t&pT\t3\ttB,qcq\'i$;gv%j_%M_{[\"&;\t\t\t.B;6Y\\%\t\"\tY/a\t\\`:?\t<\t?]\taNwD;\\\t%l*74%5T?QS :~yR/E*R\t\t=u\t\\\t\t.Q<;\\\t_S/{&F$^\tw_>\'h=|%\t\t:W!\\<U2\'$\tb&`\t=|=L9\t\t\t\\WZ:! }{\t ;\t;\t\t 0.*\t.%\"\'r%{<Mh_t[[}\t-\tJo\"b/AC*-=`=T\tz$2\tC\t\t/k{47\"\t\t,q%\tZ\tT3\t\tf>%\t\'?%@^tx\t7\"1Bk{b{n\t\"Pj3\tHc\t\tt\tY<\t#?\tSh\\yk/N\\\t8 7=R4*9Cw&m\t\\-\'f\t|\'#t(Etu.Hdu(E\t%&v:\'aqW~A5\t\t w.s{J%lX<\"\t\'*%m<&:/B<&\':U}$&`.{)\t\t6S\t:/$*kQ-Z\t^\'t${/tH*\'v\t3\t=\t\tDyp:B\t`I_R&4SO\t\t&-j=*.\t87&\'e*( \t\t\t\'<$\\DJ<$p?{}\'&\tv\t\\Xk<Y:Y`!$K{\tF&\tzd\t\t*i$\tj\'\t<)R*\t%?\t!.\t=\"@#~:=*\t\tXO=_T,1\"\'.%%\"`{\\:\t\"\tfkeOb/\'$I~\ta\t|&\t[\\KK\"1&Z\t<k\t\t)%\'-~\"2n\tj\tW?*<@w{g%d\ta\\\'\'I\t;:ySR%ke:4\tc\t$=\t&9P]x4\tJ=\t6C6%a\t`0\tF\tm-\tTr\t}\t\tQum\t&@\typ|w2&\t\t3`i&t\t\tT5\"\t.&b&e*/==1.\'*\\[U*\tqPL%?$-0/}~|q`\t\t}\t$\tq==o+T$\'!H\t\ti&um\"?\"%%\t/\'p\tg>?{0{J{\t\t/\t\t{zKZ&>=\t[\"1h<H%z/8,/]s\tv{7\t\t:j*H,M//\t\t\td\'.)\t"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Seeds for failure cases proptest has generated in the past. It is
|
|
||||||
# automatically read and these particular cases re-run before any
|
|
||||||
# novel cases are generated.
|
|
||||||
#
|
|
||||||
# It is recommended to check this file in to source control so that
|
|
||||||
# everyone who runs the test benefits from these saved cases.
|
|
||||||
cc 679506fe9ac59df773f8cfa800fdab5f0a32fe49d2ab370394000a1aa5bc2a72 # shrinks to buf = "%0A"
|
|
||||||
cc e34960438edb2426904b44fb4215154e7e2880f2fd1c3183b98bfcc76fec4882 # shrinks to input = " 0"
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Seeds for failure cases proptest has generated in the past. It is
|
|
||||||
# automatically read and these particular cases re-run before any
|
|
||||||
# novel cases are generated.
|
|
||||||
#
|
|
||||||
# It is recommended to check this file in to source control so that
|
|
||||||
# everyone who runs the test benefits from these saved cases.
|
|
||||||
cc c310754465ee0261807b96fa9bcc4861ff9aa286e94667524b5960c69f9b6620 # shrinks to buf = "", approx_chars = 0, do_unwrap = false
|
|
||||||
cc 5fd8d730b0a9cdf7308ce58818ca9aefc0255c9ba2a0878944fc48d43a67315b # shrinks to buf = "𑒀ὐ¢🜀\u{1e01b}A a🟠", approx_chars = 0, do_unwrap = false
|
|
||||||
cc c6a0029a54137a4b9efc9ef2ea6d9a7dd1d60d1c937bb472b66a174618ba8013 # shrinks to buf = "𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ ", approx_chars = 0, do_unwrap = false
|
|
||||||
@@ -15,7 +15,7 @@ without any "build-from-source" steps.
|
|||||||
1. `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
1. `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
||||||
then create a fresh python environment and activate it in your shell::
|
then create a fresh python environment and activate it in your shell::
|
||||||
|
|
||||||
virtualenv venv # or: python -m venv
|
virtualenv -p python3 venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
||||||
Afterwards, invoking ``python`` or ``pip install`` will only
|
Afterwards, invoking ``python`` or ``pip install`` will only
|
||||||
@@ -39,12 +39,6 @@ and push them to a python package index. To install the latest github ``master``
|
|||||||
|
|
||||||
pip install -i https://m.devpi.net/dc/master deltachat
|
pip install -i https://m.devpi.net/dc/master deltachat
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If you can help to automate the building of wheels for Mac or Windows,
|
|
||||||
that'd be much appreciated! please then get
|
|
||||||
`in contact with us <https://delta.chat/en/contribute>`_.
|
|
||||||
|
|
||||||
|
|
||||||
Installing bindings from source
|
Installing bindings from source
|
||||||
===============================
|
===============================
|
||||||
@@ -54,55 +48,34 @@ to core deltachat library::
|
|||||||
|
|
||||||
git clone https://github.com/deltachat/deltachat-core-rust
|
git clone https://github.com/deltachat/deltachat-core-rust
|
||||||
cd deltachat-core-rust
|
cd deltachat-core-rust
|
||||||
|
cargo build -p deltachat_ffi --release
|
||||||
|
|
||||||
|
This will result in a ``libdeltachat.so`` and ``libdeltachat.a`` files
|
||||||
|
in the ``target/release`` directory. These files are needed for
|
||||||
|
creating the python bindings for deltachat::
|
||||||
|
|
||||||
cd python
|
cd python
|
||||||
|
DCC_RS_DEV=`pwd`/.. pip install -e .
|
||||||
|
|
||||||
If you don't have one active, create and activate a python "virtualenv":
|
Now test if the bindings find the correct library::
|
||||||
|
|
||||||
python virtualenv venv # or python -m venv
|
python -c 'import deltachat ; print(deltachat.__version__)'
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
Afterwards ``which python`` tells you that it comes out of the "venv"
|
This should print your deltachat bindings version.
|
||||||
directory that contains all python install artifacts. Let's first
|
|
||||||
install test tools::
|
|
||||||
|
|
||||||
pip install pytest pytest-timeout pytest-rerunfailures requests
|
|
||||||
|
|
||||||
then cargo-build and install the deltachat bindings::
|
|
||||||
|
|
||||||
python install_python_bindings.py
|
|
||||||
|
|
||||||
The bindings will be installed in release mode but with debug symbols.
|
|
||||||
The release mode is necessary because some tests generate RSA keys
|
|
||||||
which is prohibitively slow in debug mode.
|
|
||||||
|
|
||||||
After successful binding installation you can finally run the tests::
|
|
||||||
|
|
||||||
pytest -v tests
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Some tests are sometimes failing/hanging because of
|
If you can help to automate the building of wheels for Mac or Windows,
|
||||||
https://github.com/deltachat/deltachat-core-rust/issues/331
|
that'd be much appreciated! please then get
|
||||||
and
|
`in contact with us <https://delta.chat/en/contribute>`_.
|
||||||
https://github.com/deltachat/deltachat-core-rust/issues/326
|
|
||||||
|
|
||||||
|
Using a system-installed deltachat-core-rust
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
running "live" tests (experimental)
|
When calling ``pip`` without specifying the ``DCC_RS_DEV`` environment
|
||||||
-----------------------------------
|
variable cffi will try to use a ``deltachat.h`` from a system location
|
||||||
|
like ``/usr/local/include`` and will try to dynamically link against a
|
||||||
If you want to run "liveconfig" functional tests you can set
|
``libdeltachat.so`` in a similar location (e.g. ``/usr/local/lib``).
|
||||||
``DCC_PY_LIVECONFIG`` to:
|
|
||||||
|
|
||||||
- a particular https-url that you can ask for from the delta
|
|
||||||
chat devs.
|
|
||||||
|
|
||||||
- or the path of a file that contains two lines, each describing
|
|
||||||
via "addr=... mail_pw=..." a test account login that will
|
|
||||||
be used for the live tests.
|
|
||||||
|
|
||||||
With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real
|
|
||||||
e-mail accounts and run through all functional "liveconfig" tests.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Code examples
|
Code examples
|
||||||
@@ -111,34 +84,68 @@ Code examples
|
|||||||
You may look at `examples <https://py.delta.chat/examples.html>`_.
|
You may look at `examples <https://py.delta.chat/examples.html>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Running tests
|
||||||
|
=============
|
||||||
|
|
||||||
|
Get a checkout of the `deltachat-core-rust github repository`_ and type::
|
||||||
|
|
||||||
|
pip install tox
|
||||||
|
./run-integration-tests.sh
|
||||||
|
|
||||||
|
If you want to run functional tests with real
|
||||||
|
e-mail test accounts, generate a "liveconfig" file where each
|
||||||
|
lines contains test account settings, for example::
|
||||||
|
|
||||||
|
# 'liveconfig' file specifying imap/smtp accounts
|
||||||
|
addr=some-email@example.org mail_pw=password
|
||||||
|
addr=other-email@example.org mail_pw=otherpassword
|
||||||
|
|
||||||
|
The "keyword=value" style allows to specify any
|
||||||
|
`deltachat account config setting <https://c.delta.chat/classdc__context__t.html#aff3b894f6cfca46cab5248fdffdf083d>`_ so you can also specify smtp or imap servers, ports, ssl modes etc.
|
||||||
|
Typically DC's automatic configuration allows to not specify these settings.
|
||||||
|
|
||||||
|
The ``run-integration-tests.sh`` script will automatically use
|
||||||
|
``python/liveconfig`` if it exists, to manually run tests with this
|
||||||
|
``liveconfig`` file use::
|
||||||
|
|
||||||
|
tox -- --liveconfig liveconfig
|
||||||
|
|
||||||
|
|
||||||
.. _`deltachat-core-rust github repository`: https://github.com/deltachat/deltachat-core-rust
|
.. _`deltachat-core-rust github repository`: https://github.com/deltachat/deltachat-core-rust
|
||||||
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
|
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
|
||||||
|
|
||||||
|
Running test using a debug build
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
If you need to examine e.g. a coredump you may want to run the tests
|
||||||
|
using a debug build::
|
||||||
|
|
||||||
|
DCC_RS_TARGET=debug ./run-integration-tests.sh -e py37 -- -x -v -k failing_test
|
||||||
|
|
||||||
|
|
||||||
Building manylinux1 wheels
|
Building manylinux1 wheels
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This section may not fully work.
|
|
||||||
|
|
||||||
Building portable manylinux1 wheels which come with libdeltachat.so
|
Building portable manylinux1 wheels which come with libdeltachat.so
|
||||||
and all it's dependencies is easy using the provided docker tooling.
|
and all it's dependencies is easy using the provided docker tooling.
|
||||||
|
|
||||||
using docker pull / premade images
|
using docker pull / premade images
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
We publish a build environment under the ``deltachat/coredeps`` tag so
|
We publish a build environment under the ``deltachat/wheel`` tag so
|
||||||
that you can pull it from the ``hub.docker.com`` site's "deltachat"
|
that you can pull it from the ``hub.docker.com`` site's "deltachat"
|
||||||
organization::
|
organization::
|
||||||
|
|
||||||
$ docker pull deltachat/coredeps
|
$ docker pull deltachat/wheel
|
||||||
|
|
||||||
This docker image can be used to run tests and build Python wheels for all interpreters::
|
The ``deltachat/wheel`` image can be used to build both libdeltachat.so
|
||||||
|
and the Python wheels::
|
||||||
|
|
||||||
$ bash ci_scripts/ci_run.sh
|
$ docker run --rm -it -v $(pwd):/io/ deltachat/wheel /io/python/wheelbuilder/build-wheels.sh
|
||||||
|
|
||||||
This command runs tests and build-wheel scripts in a docker container.
|
This command runs a script within the image, after mounting ``$(pwd)`` as ``/io`` within
|
||||||
|
the docker image. The script is specified as a path within the docker image's filesystem.
|
||||||
|
The resulting wheel files will be in ``python/wheelhouse``.
|
||||||
|
|
||||||
|
|
||||||
Optionally build your own docker image
|
Optionally build your own docker image
|
||||||
@@ -147,10 +154,10 @@ Optionally build your own docker image
|
|||||||
If you want to build your own custom docker image you can do this::
|
If you want to build your own custom docker image you can do this::
|
||||||
|
|
||||||
$ cd deltachat-core # cd to deltachat-core checkout directory
|
$ cd deltachat-core # cd to deltachat-core checkout directory
|
||||||
$ docker build -t deltachat/coredeps ci_scripts/docker_coredeps
|
$ docker build -t deltachat/wheel python/wheelbuilder/
|
||||||
|
|
||||||
This will use the ``ci_scripts/docker_coredeps/Dockerfile`` to build
|
This will use the ``python/wheelbuilder/Dockerfile`` to build
|
||||||
up docker image called ``deltachat/coredeps``. You can afterwards
|
up docker image called ``deltachat/wheel``. You can afterwards
|
||||||
find it with::
|
find it with::
|
||||||
|
|
||||||
$ docker images
|
$ docker images
|
||||||
|
|||||||
1
python/doc/_templates/globaltoc.html
vendored
1
python/doc/_templates/globaltoc.html
vendored
@@ -6,6 +6,7 @@
|
|||||||
<li><a href="{{ pathto('install') }}">install</a></li>
|
<li><a href="{{ pathto('install') }}">install</a></li>
|
||||||
<li><a href="{{ pathto('api') }}">high level API</a></li>
|
<li><a href="{{ pathto('api') }}">high level API</a></li>
|
||||||
<li><a href="{{ pathto('lapi') }}">low level API</a></li>
|
<li><a href="{{ pathto('lapi') }}">low level API</a></li>
|
||||||
|
<li><a href="{{ pathto('capi') }}">C deltachat.h</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<b>external links:</b>
|
<b>external links:</b>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ high level API reference
|
|||||||
|
|
||||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
- :class:`deltachat.account.Account` (your main entry point, creates the
|
||||||
other classes)
|
other classes)
|
||||||
- :class:`deltachat.contact.Contact`
|
- :class:`deltachat.chatting.Contact`
|
||||||
- :class:`deltachat.chat.Chat`
|
- :class:`deltachat.chatting.Chat`
|
||||||
- :class:`deltachat.message.Message`
|
- :class:`deltachat.message.Message`
|
||||||
|
|
||||||
Account
|
Account
|
||||||
@@ -22,13 +22,13 @@ Account
|
|||||||
Contact
|
Contact
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. autoclass:: deltachat.contact.Contact
|
.. autoclass:: deltachat.chatting.Contact
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Chat
|
Chat
|
||||||
----
|
----
|
||||||
|
|
||||||
.. autoclass:: deltachat.chat.Chat
|
.. autoclass:: deltachat.chatting.Chat
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Message
|
Message
|
||||||
@@ -37,3 +37,16 @@ Message
|
|||||||
.. autoclass:: deltachat.message.Message
|
.. autoclass:: deltachat.message.Message
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
MessageType
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.message.MessageType
|
||||||
|
:members:
|
||||||
|
|
||||||
|
MessageState
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.message.MessageState
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ Playing around on the commandline
|
|||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
Once you have :doc:`installed deltachat bindings <install>`
|
Once you have :doc:`installed deltachat bindings <install>`
|
||||||
you can start playing from the python interpreter commandline.
|
you can start playing from the python interpreter commandline::
|
||||||
|
|
||||||
For example you can type ``python`` and then::
|
For example you can type ``python`` and then::
|
||||||
|
|
||||||
# instantiate and configure deltachat account
|
# instantiate and configure deltachat account
|
||||||
@@ -22,7 +23,7 @@ For example you can type ``python`` and then::
|
|||||||
# create a contact and send a message
|
# create a contact and send a message
|
||||||
contact = ac.create_contact("someother@email.address")
|
contact = ac.create_contact("someother@email.address")
|
||||||
chat = ac.create_chat_by_contact(contact)
|
chat = ac.create_chat_by_contact(contact)
|
||||||
chat.send_text("hi from the python interpreter command line")
|
chat.send_text_message("hi from the python interpreter command line")
|
||||||
|
|
||||||
Checkout our :doc:`api` for the various high-level things you can do
|
Checkout our :doc:`api` for the various high-level things you can do
|
||||||
to send/receive messages, create contacts and chats.
|
to send/receive messages, create contacts and chats.
|
||||||
|
|||||||
@@ -2,13 +2,7 @@
|
|||||||
low level API reference
|
low level API reference
|
||||||
===================================
|
===================================
|
||||||
|
|
||||||
for full doxygen-generated C-docs, defines and functions please checkout
|
for full C-docs, defines and function checkout :doc:`capi`
|
||||||
|
|
||||||
https://c.delta.chat
|
|
||||||
|
|
||||||
|
|
||||||
Python low-level capi calls
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: deltachat.capi.lib
|
.. automodule:: deltachat.capi.lib
|
||||||
|
|||||||
@@ -6,26 +6,31 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
target = os.environ.get("DCC_RS_TARGET")
|
os.environ["DCC_RS_TARGET"] = target = "release"
|
||||||
if target is None:
|
|
||||||
os.environ["DCC_RS_TARGET"] = target = "release"
|
toml = os.path.join(os.getcwd(), "..", "Cargo.toml")
|
||||||
if "DCC_RS_DEV" not in os.environ:
|
assert os.path.exists(toml)
|
||||||
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
with open(toml) as f:
|
||||||
os.environ["DCC_RS_DEV"] = dn
|
s = orig = f.read()
|
||||||
|
s += "\n"
|
||||||
|
s += "[profile.release]\n"
|
||||||
|
s += "debug = true\n"
|
||||||
|
with open(toml, "w") as f:
|
||||||
|
f.write(s)
|
||||||
|
print("temporarily modifying Cargo.toml to provide release build with debug symbols ")
|
||||||
|
try:
|
||||||
|
subprocess.check_call([
|
||||||
|
"cargo", "build", "-p", "deltachat_ffi", "--" + target
|
||||||
|
])
|
||||||
|
finally:
|
||||||
|
with open(toml, "w") as f:
|
||||||
|
f.write(orig)
|
||||||
|
print("\nreseted Cargo.toml to previous original state")
|
||||||
|
|
||||||
# build the core library in release + debug mode because
|
|
||||||
# as of Nov 2019 rPGP generates RSA keys which take
|
|
||||||
# prohibitively long for non-release installs
|
|
||||||
os.environ["RUSTFLAGS"] = "-g"
|
|
||||||
subprocess.check_call([
|
|
||||||
"cargo", "build", "-p", "deltachat_ffi", "--" + target
|
|
||||||
])
|
|
||||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
|
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
|
||||||
|
|
||||||
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
|
subprocess.check_call([
|
||||||
subprocess.check_call([
|
"pip", "install", "-e", "."
|
||||||
sys.executable, "-m", "pip", "install", "-e", "."
|
])
|
||||||
])
|
|
||||||
|
|||||||
@@ -34,14 +34,13 @@ def py_dc_callback(ctx, evt, data1, data2):
|
|||||||
if data1 and event_sig_types & 1:
|
if data1 and event_sig_types & 1:
|
||||||
data1 = ffi.string(ffi.cast('char*', data1)).decode("utf8")
|
data1 = ffi.string(ffi.cast('char*', data1)).decode("utf8")
|
||||||
if data2 and event_sig_types & 2:
|
if data2 and event_sig_types & 2:
|
||||||
data2 = ffi.string(ffi.cast('char*', data2)).decode("utf8")
|
|
||||||
try:
|
try:
|
||||||
if isinstance(data2, bytes):
|
data2 = ffi.string(ffi.cast('char*', data2)).decode("utf8")
|
||||||
data2 = data2.decode("utf8")
|
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
# XXX ignoring the decode error is not quite correct but for now
|
# XXX ignoring this error is not quite correct but for now
|
||||||
# i don't want to hunt down encoding problems in the c lib
|
# i don't want to hunt down encoding problems in the c lib
|
||||||
pass
|
data2 = ffi.string(ffi.cast('char*', data2))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ret = callback(ctx, evt_name, data1, data2)
|
ret = callback(ctx, evt_name, data1, data2)
|
||||||
if ret is None:
|
if ret is None:
|
||||||
|
|||||||
@@ -6,15 +6,10 @@ import platform
|
|||||||
import os
|
import os
|
||||||
import cffi
|
import cffi
|
||||||
import shutil
|
import shutil
|
||||||
from os.path import dirname as dn
|
|
||||||
from os.path import abspath
|
|
||||||
|
|
||||||
|
|
||||||
def ffibuilder():
|
def ffibuilder():
|
||||||
projdir = os.environ.get('DCC_RS_DEV')
|
projdir = os.environ.get('DCC_RS_DEV')
|
||||||
if not projdir:
|
|
||||||
p = dn(dn(dn(dn(abspath(__file__)))))
|
|
||||||
projdir = os.environ["DCC_RS_DEV"] = p
|
|
||||||
target = os.environ.get('DCC_RS_TARGET', 'release')
|
target = os.environ.get('DCC_RS_TARGET', 'release')
|
||||||
if projdir:
|
if projdir:
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == 'Darwin':
|
||||||
@@ -29,10 +24,7 @@ def ffibuilder():
|
|||||||
extra_link_args = []
|
extra_link_args = []
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
|
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
|
||||||
target_dir = os.environ.get("CARGO_TARGET_DIR")
|
objs = [os.path.join(projdir, 'target', target, 'libdeltachat.a')]
|
||||||
if target_dir is None:
|
|
||||||
target_dir = os.path.join(projdir, 'target')
|
|
||||||
objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
|
||||||
assert os.path.exists(objs[0]), objs
|
assert os.path.exists(objs[0]), objs
|
||||||
incs = [os.path.join(projdir, 'deltachat-ffi')]
|
incs = [os.path.join(projdir, 'deltachat-ffi')]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
""" Account class implementation. """
|
""" Account class implementation. """
|
||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import atexit
|
|
||||||
import threading
|
import threading
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from array import array
|
from array import array
|
||||||
@@ -14,18 +14,16 @@ except ImportError:
|
|||||||
import deltachat
|
import deltachat
|
||||||
from . import const
|
from . import const
|
||||||
from .capi import ffi, lib
|
from .capi import ffi, lib
|
||||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||||
from .chat import Chat
|
from .chatting import Contact, Chat, Message
|
||||||
from .message import Message
|
|
||||||
from .contact import Contact
|
|
||||||
|
|
||||||
|
|
||||||
class Account(object):
|
class Account(object):
|
||||||
""" Each account is tied to a sqlite database file which is fully managed
|
""" Each account is tied to a sqlite database file which is fully managed
|
||||||
by the underlying deltachat core library. All public Account methods are
|
by the underlying deltachat c-library. All public Account methods are
|
||||||
meant to be memory-safe and return memory-safe objects.
|
meant to be memory-safe and return memory-safe objects.
|
||||||
"""
|
"""
|
||||||
def __init__(self, db_path, logid=None, eventlogging=True, debug=True):
|
def __init__(self, db_path, logid=None, eventlogging=True):
|
||||||
""" initialize account object.
|
""" initialize account object.
|
||||||
|
|
||||||
:param db_path: a path to the account database. The database
|
:param db_path: a path to the account database. The database
|
||||||
@@ -33,14 +31,13 @@ class Account(object):
|
|||||||
:param logid: an optional logging prefix that should be used with
|
:param logid: an optional logging prefix that should be used with
|
||||||
the default internal logging.
|
the default internal logging.
|
||||||
:param eventlogging: if False no eventlogging and no context callback will be configured
|
:param eventlogging: if False no eventlogging and no context callback will be configured
|
||||||
:param debug: turn on debug logging for events.
|
|
||||||
"""
|
"""
|
||||||
self._dc_context = ffi.gc(
|
self._dc_context = ffi.gc(
|
||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||||
_destroy_dc_context,
|
_destroy_dc_context,
|
||||||
)
|
)
|
||||||
if eventlogging:
|
if eventlogging:
|
||||||
self._evlogger = EventLogger(self._dc_context, logid, debug)
|
self._evlogger = EventLogger(self._dc_context, logid)
|
||||||
deltachat.set_context_callback(self._dc_context, self._process_event)
|
deltachat.set_context_callback(self._dc_context, self._process_event)
|
||||||
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
||||||
else:
|
else:
|
||||||
@@ -51,11 +48,10 @@ class Account(object):
|
|||||||
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
||||||
raise ValueError("Could not dc_open: {}".format(db_path))
|
raise ValueError("Could not dc_open: {}".format(db_path))
|
||||||
self._configkeys = self.get_config("sys.config_keys").split()
|
self._configkeys = self.get_config("sys.config_keys").split()
|
||||||
self._imex_events = Queue()
|
self._imex_completed = threading.Event()
|
||||||
atexit.register(self.shutdown)
|
|
||||||
|
|
||||||
# def __del__(self):
|
def __del__(self):
|
||||||
# self.shutdown()
|
self.shutdown()
|
||||||
|
|
||||||
def _check_config_key(self, name):
|
def _check_config_key(self, name):
|
||||||
if name not in self._configkeys:
|
if name not in self._configkeys:
|
||||||
@@ -73,18 +69,6 @@ class Account(object):
|
|||||||
d[key.lower()] = value
|
d[key.lower()] = value
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def set_stock_translation(self, id, string):
|
|
||||||
""" set stock translation string.
|
|
||||||
|
|
||||||
:param id: id of stock string (const.DC_STR_*)
|
|
||||||
:param value: string to set as new transalation
|
|
||||||
:returns: None
|
|
||||||
"""
|
|
||||||
string = string.encode("utf8")
|
|
||||||
res = lib.dc_set_stock_translation(self._dc_context, id, string)
|
|
||||||
if res == 0:
|
|
||||||
raise ValueError("could not set translation string")
|
|
||||||
|
|
||||||
def set_config(self, name, value):
|
def set_config(self, name, value):
|
||||||
""" set configuration values.
|
""" set configuration values.
|
||||||
|
|
||||||
@@ -137,30 +121,11 @@ class Account(object):
|
|||||||
if not self.is_configured():
|
if not self.is_configured():
|
||||||
raise ValueError("need to configure first")
|
raise ValueError("need to configure first")
|
||||||
|
|
||||||
def empty_server_folders(self, inbox=False, mvbox=False):
|
|
||||||
""" empty server folders. """
|
|
||||||
flags = 0
|
|
||||||
if inbox:
|
|
||||||
flags |= const.DC_EMPTY_INBOX
|
|
||||||
if mvbox:
|
|
||||||
flags |= const.DC_EMPTY_MVBOX
|
|
||||||
if not flags:
|
|
||||||
raise ValueError("no flags set")
|
|
||||||
lib.dc_empty_server(self._dc_context, flags)
|
|
||||||
|
|
||||||
def get_infostring(self):
|
def get_infostring(self):
|
||||||
""" return info of the configured account. """
|
""" return info of the configured account. """
|
||||||
self.check_is_configured()
|
self.check_is_configured()
|
||||||
return from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
return from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||||
|
|
||||||
def get_latest_backupfile(self, backupdir):
|
|
||||||
""" return the latest backup file in a given directory.
|
|
||||||
"""
|
|
||||||
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
|
|
||||||
if res == ffi.NULL:
|
|
||||||
return None
|
|
||||||
return from_dc_charpointer(res)
|
|
||||||
|
|
||||||
def get_blobdir(self):
|
def get_blobdir(self):
|
||||||
""" return the directory for files.
|
""" return the directory for files.
|
||||||
|
|
||||||
@@ -170,9 +135,9 @@ class Account(object):
|
|||||||
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
||||||
|
|
||||||
def get_self_contact(self):
|
def get_self_contact(self):
|
||||||
""" return this account's identity as a :class:`deltachat.contact.Contact`.
|
""" return this account's identity as a :class:`deltachat.chatting.Contact`.
|
||||||
|
|
||||||
:returns: :class:`deltachat.contact.Contact`
|
:returns: :class:`deltachat.chatting.Contact`
|
||||||
"""
|
"""
|
||||||
self.check_is_configured()
|
self.check_is_configured()
|
||||||
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
|
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
|
||||||
@@ -184,7 +149,7 @@ class Account(object):
|
|||||||
|
|
||||||
:param email: email-address (text type)
|
:param email: email-address (text type)
|
||||||
:param name: display name for this contact (optional)
|
:param name: display name for this contact (optional)
|
||||||
:returns: :class:`deltachat.contact.Contact` instance.
|
:returns: :class:`deltachat.chatting.Contact` instance.
|
||||||
"""
|
"""
|
||||||
name = as_dc_charpointer(name)
|
name = as_dc_charpointer(name)
|
||||||
email = as_dc_charpointer(email)
|
email = as_dc_charpointer(email)
|
||||||
@@ -210,7 +175,7 @@ class Account(object):
|
|||||||
whose name or e-mail matches query.
|
whose name or e-mail matches query.
|
||||||
:param only_verified: if true only return verified contacts.
|
:param only_verified: if true only return verified contacts.
|
||||||
:param with_self: if true the self-contact is also returned.
|
:param with_self: if true the self-contact is also returned.
|
||||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
:returns: list of :class:`deltachat.message.Message` objects.
|
||||||
"""
|
"""
|
||||||
flags = 0
|
flags = 0
|
||||||
query = as_dc_charpointer(query)
|
query = as_dc_charpointer(query)
|
||||||
@@ -228,7 +193,7 @@ class Account(object):
|
|||||||
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||||
|
|
||||||
:param contact: chat_id (int) or contact object.
|
:param contact: chat_id (int) or contact object.
|
||||||
:returns: a :class:`deltachat.chat.Chat` object.
|
:returns: a :class:`deltachat.chatting.Chat` object.
|
||||||
"""
|
"""
|
||||||
if hasattr(contact, "id"):
|
if hasattr(contact, "id"):
|
||||||
if contact._dc_context != self._dc_context:
|
if contact._dc_context != self._dc_context:
|
||||||
@@ -245,7 +210,7 @@ class Account(object):
|
|||||||
the specified message.
|
the specified message.
|
||||||
|
|
||||||
:param message: messsage id or message instance.
|
:param message: messsage id or message instance.
|
||||||
:returns: a :class:`deltachat.chat.Chat` object.
|
:returns: a :class:`deltachat.chatting.Chat` object.
|
||||||
"""
|
"""
|
||||||
if hasattr(message, "id"):
|
if hasattr(message, "id"):
|
||||||
if self._dc_context != message._dc_context:
|
if self._dc_context != message._dc_context:
|
||||||
@@ -263,16 +228,16 @@ class Account(object):
|
|||||||
Chats are unpromoted until the first message is sent.
|
Chats are unpromoted until the first message is sent.
|
||||||
|
|
||||||
:param verified: if true only verified contacts can be added.
|
:param verified: if true only verified contacts can be added.
|
||||||
:returns: a :class:`deltachat.chat.Chat` object.
|
:returns: a :class:`deltachat.chatting.Chat` object.
|
||||||
"""
|
"""
|
||||||
bytes_name = name.encode("utf8")
|
bytes_name = name.encode("utf8")
|
||||||
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
|
chat_id = lib.dc_create_group_chat(self._dc_context, verified, bytes_name)
|
||||||
return Chat(self, chat_id)
|
return Chat(self, chat_id)
|
||||||
|
|
||||||
def get_chats(self):
|
def get_chats(self):
|
||||||
""" return list of chats.
|
""" return list of chats.
|
||||||
|
|
||||||
:returns: a list of :class:`deltachat.chat.Chat` objects.
|
:returns: a list of :class:`deltachat.chatting.Chat` objects.
|
||||||
"""
|
"""
|
||||||
dc_chatlist = ffi.gc(
|
dc_chatlist = ffi.gc(
|
||||||
lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0),
|
lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0),
|
||||||
@@ -290,24 +255,9 @@ class Account(object):
|
|||||||
return Chat(self, const.DC_CHAT_ID_DEADDROP)
|
return Chat(self, const.DC_CHAT_ID_DEADDROP)
|
||||||
|
|
||||||
def get_message_by_id(self, msg_id):
|
def get_message_by_id(self, msg_id):
|
||||||
""" return Message instance.
|
""" return Message instance. """
|
||||||
:param msg_id: integer id of this message.
|
|
||||||
:returns: :class:`deltachat.message.Message` instance.
|
|
||||||
"""
|
|
||||||
return Message.from_db(self, msg_id)
|
return Message.from_db(self, msg_id)
|
||||||
|
|
||||||
def get_chat_by_id(self, chat_id):
|
|
||||||
""" return Chat instance.
|
|
||||||
:param chat_id: integer id of this chat.
|
|
||||||
:returns: :class:`deltachat.chat.Chat` instance.
|
|
||||||
:raises: ValueError if chat does not exist.
|
|
||||||
"""
|
|
||||||
res = lib.dc_get_chat(self._dc_context, chat_id)
|
|
||||||
if res == ffi.NULL:
|
|
||||||
raise ValueError("cannot get chat with id={}".format(chat_id))
|
|
||||||
lib.dc_chat_unref(res)
|
|
||||||
return Chat(self, chat_id)
|
|
||||||
|
|
||||||
def mark_seen_messages(self, messages):
|
def mark_seen_messages(self, messages):
|
||||||
""" mark the given set of messages as seen.
|
""" mark the given set of messages as seen.
|
||||||
|
|
||||||
@@ -324,7 +274,7 @@ class Account(object):
|
|||||||
""" Forward list of messages to a chat.
|
""" Forward list of messages to a chat.
|
||||||
|
|
||||||
:param messages: list of :class:`deltachat.message.Message` object.
|
:param messages: list of :class:`deltachat.message.Message` object.
|
||||||
:param chat: :class:`deltachat.chat.Chat` object.
|
:param chat: :class:`deltachat.chatting.Chat` object.
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
msg_ids = [msg.id for msg in messages]
|
msg_ids = [msg.id for msg in messages]
|
||||||
@@ -339,64 +289,31 @@ class Account(object):
|
|||||||
msg_ids = [msg.id for msg in messages]
|
msg_ids = [msg.id for msg in messages]
|
||||||
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
||||||
|
|
||||||
def export_self_keys(self, path):
|
def export_to_dir(self, backupdir):
|
||||||
""" export public and private keys to the specified directory. """
|
"""return after all delta chat state is exported to a new file in
|
||||||
return self._export(path, imex_cmd=1)
|
the specified directory.
|
||||||
|
|
||||||
def export_all(self, path):
|
|
||||||
"""return new file containing a backup of all database state
|
|
||||||
(chats, contacts, keys, media, ...). The file is created in the
|
|
||||||
the `path` directory.
|
|
||||||
"""
|
"""
|
||||||
export_files = self._export(path, 11)
|
snap_files = os.listdir(backupdir)
|
||||||
if len(export_files) != 1:
|
self._imex_completed.clear()
|
||||||
raise RuntimeError("found more than one new file")
|
lib.dc_imex(self._dc_context, 11, as_dc_charpointer(backupdir), ffi.NULL)
|
||||||
return export_files[0]
|
|
||||||
|
|
||||||
def _imex_events_clear(self):
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
self._imex_events.get_nowait()
|
|
||||||
except Empty:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _export(self, path, imex_cmd):
|
|
||||||
self._imex_events_clear()
|
|
||||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
|
||||||
if not self._threads.is_started():
|
if not self._threads.is_started():
|
||||||
lib.dc_perform_imap_jobs(self._dc_context)
|
lib.dc_perform_imap_jobs(self._dc_context)
|
||||||
files_written = []
|
self._imex_completed.wait()
|
||||||
while True:
|
for x in os.listdir(backupdir):
|
||||||
ev = self._imex_events.get()
|
if x not in snap_files:
|
||||||
if isinstance(ev, str):
|
return os.path.join(backupdir, x)
|
||||||
files_written.append(ev)
|
|
||||||
elif isinstance(ev, bool):
|
|
||||||
if not ev:
|
|
||||||
raise ValueError("export failed, exp-files: {}".format(files_written))
|
|
||||||
return files_written
|
|
||||||
|
|
||||||
def import_self_keys(self, path):
|
def import_from_file(self, path):
|
||||||
""" Import private keys found in the `path` directory.
|
"""import delta chat state from the specified backup file.
|
||||||
The last imported key is made the default keys unless its name
|
|
||||||
contains the string legacy. Public keys are not imported.
|
|
||||||
"""
|
|
||||||
self._import(path, imex_cmd=2)
|
|
||||||
|
|
||||||
def import_all(self, path):
|
|
||||||
"""import delta chat state from the specified backup `path` (a file).
|
|
||||||
|
|
||||||
The account must be in unconfigured state for import to attempted.
|
The account must be in unconfigured state for import to attempted.
|
||||||
"""
|
"""
|
||||||
assert not self.is_configured(), "cannot import into configured account"
|
assert not self.is_configured(), "cannot import into configured account"
|
||||||
self._import(path, imex_cmd=12)
|
self._imex_completed.clear()
|
||||||
|
lib.dc_imex(self._dc_context, 12, as_dc_charpointer(path), ffi.NULL)
|
||||||
def _import(self, path, imex_cmd):
|
|
||||||
self._imex_events_clear()
|
|
||||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
|
||||||
if not self._threads.is_started():
|
if not self._threads.is_started():
|
||||||
lib.dc_perform_imap_jobs(self._dc_context)
|
lib.dc_perform_imap_jobs(self._dc_context)
|
||||||
if not self._imex_events.get():
|
self._imex_completed.wait()
|
||||||
raise ValueError("import from path '{}' failed".format(path))
|
|
||||||
|
|
||||||
def initiate_key_transfer(self):
|
def initiate_key_transfer(self):
|
||||||
"""return setup code after a Autocrypt setup message
|
"""return setup code after a Autocrypt setup message
|
||||||
@@ -411,69 +328,7 @@ class Account(object):
|
|||||||
raise RuntimeError("could not send out autocrypt setup message")
|
raise RuntimeError("could not send out autocrypt setup message")
|
||||||
return from_dc_charpointer(res)
|
return from_dc_charpointer(res)
|
||||||
|
|
||||||
def get_setup_contact_qr(self):
|
def start_threads(self):
|
||||||
""" get/create Setup-Contact QR Code as ascii-string.
|
|
||||||
|
|
||||||
this string needs to be transferred to another DC account
|
|
||||||
in a second channel (typically used by mobiles with QRcode-show + scan UX)
|
|
||||||
where qr_setup_contact(qr) is called.
|
|
||||||
"""
|
|
||||||
res = lib.dc_get_securejoin_qr(self._dc_context, 0)
|
|
||||||
return from_dc_charpointer(res)
|
|
||||||
|
|
||||||
def check_qr(self, qr):
|
|
||||||
""" check qr code and return :class:`ScannedQRCode` instance representing the result"""
|
|
||||||
res = ffi.gc(
|
|
||||||
lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)),
|
|
||||||
lib.dc_lot_unref
|
|
||||||
)
|
|
||||||
lot = DCLot(res)
|
|
||||||
if lot.state() == const.DC_QR_ERROR:
|
|
||||||
raise ValueError("invalid or unknown QR code: {}".format(lot.text1()))
|
|
||||||
return ScannedQRCode(lot)
|
|
||||||
|
|
||||||
def qr_setup_contact(self, qr):
|
|
||||||
""" setup contact and return a Chat after contact is established.
|
|
||||||
|
|
||||||
Note that this function may block for a long time as messages are exchanged
|
|
||||||
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
|
|
||||||
is returned.
|
|
||||||
:param qr: valid "setup contact" QR code (all other QR codes will result in an exception)
|
|
||||||
"""
|
|
||||||
assert self.check_qr(qr).is_ask_verifycontact()
|
|
||||||
chat_id = lib.dc_join_securejoin(self._dc_context, as_dc_charpointer(qr))
|
|
||||||
if chat_id == 0:
|
|
||||||
raise ValueError("could not setup secure contact")
|
|
||||||
return Chat(self, chat_id)
|
|
||||||
|
|
||||||
def qr_join_chat(self, qr):
|
|
||||||
""" join a chat group through a QR code.
|
|
||||||
|
|
||||||
Note that this function may block for a long time as messages are exchanged
|
|
||||||
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
|
|
||||||
is returned which is the chat that we just joined.
|
|
||||||
|
|
||||||
:param qr: valid "join-group" QR code (all other QR codes will result in an exception)
|
|
||||||
"""
|
|
||||||
assert self.check_qr(qr).is_ask_verifygroup()
|
|
||||||
chat_id = lib.dc_join_securejoin(self._dc_context, as_dc_charpointer(qr))
|
|
||||||
if chat_id == 0:
|
|
||||||
raise ValueError("could not join group")
|
|
||||||
return Chat(self, chat_id)
|
|
||||||
|
|
||||||
def stop_ongoing(self):
|
|
||||||
lib.dc_stop_ongoing_process(self._dc_context)
|
|
||||||
|
|
||||||
#
|
|
||||||
# meta API for start/stop and event based processing
|
|
||||||
#
|
|
||||||
|
|
||||||
def wait_next_incoming_message(self):
|
|
||||||
""" wait for and return next incoming message. """
|
|
||||||
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
return self.get_message_by_id(ev[2])
|
|
||||||
|
|
||||||
def start_threads(self, mvbox=False, sentbox=False):
|
|
||||||
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
|
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
|
||||||
|
|
||||||
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
|
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
|
||||||
@@ -481,24 +336,21 @@ class Account(object):
|
|||||||
"""
|
"""
|
||||||
if not self.is_configured():
|
if not self.is_configured():
|
||||||
self.configure()
|
self.configure()
|
||||||
self._threads.start(mvbox=mvbox, sentbox=sentbox)
|
self._threads.start()
|
||||||
|
|
||||||
def stop_threads(self, wait=True):
|
def stop_threads(self, wait=True):
|
||||||
""" stop IMAP/SMTP threads. """
|
""" stop IMAP/SMTP threads. """
|
||||||
if self._threads.is_started():
|
lib.dc_stop_ongoing_process(self._dc_context)
|
||||||
self.stop_ongoing()
|
self._threads.stop(wait=wait)
|
||||||
self._threads.stop(wait=wait)
|
|
||||||
|
|
||||||
def shutdown(self, wait=True):
|
def shutdown(self, wait=True):
|
||||||
""" stop threads and close and remove underlying dc_context and callbacks. """
|
""" stop threads and close and remove underlying dc_context and callbacks. """
|
||||||
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
|
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
|
||||||
# print("SHUTDOWN", self)
|
self.stop_threads(wait=False) # to interrupt idle and tell python threads to stop
|
||||||
self.stop_threads(wait=False)
|
|
||||||
lib.dc_close(self._dc_context)
|
lib.dc_close(self._dc_context)
|
||||||
self.stop_threads(wait=wait) # to wait for threads
|
self.stop_threads(wait=wait) # to wait for threads
|
||||||
deltachat.clear_context_callback(self._dc_context)
|
deltachat.clear_context_callback(self._dc_context)
|
||||||
del self._dc_context
|
del self._dc_context
|
||||||
atexit.unregister(self.shutdown)
|
|
||||||
|
|
||||||
def _process_event(self, ctx, evt_name, data1, data2):
|
def _process_event(self, ctx, evt_name, data1, data2):
|
||||||
assert ctx == self._dc_context
|
assert ctx == self._dc_context
|
||||||
@@ -511,26 +363,7 @@ class Account(object):
|
|||||||
|
|
||||||
def on_dc_event_imex_progress(self, data1, data2):
|
def on_dc_event_imex_progress(self, data1, data2):
|
||||||
if data1 == 1000:
|
if data1 == 1000:
|
||||||
self._imex_events.put(True)
|
self._imex_completed.set()
|
||||||
elif data1 == 0:
|
|
||||||
self._imex_events.put(False)
|
|
||||||
|
|
||||||
def on_dc_event_imex_file_written(self, data1, data2):
|
|
||||||
self._imex_events.put(data1)
|
|
||||||
|
|
||||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
|
||||||
"""set a new location. It effects all chats where we currently
|
|
||||||
have enabled location streaming.
|
|
||||||
|
|
||||||
:param latitude: float (use 0.0 if not known)
|
|
||||||
:param longitude: float (use 0.0 if not known)
|
|
||||||
:param accuracy: float (use 0.0 if not known)
|
|
||||||
:raises: ValueError if no chat is currently streaming locations
|
|
||||||
:returns: None
|
|
||||||
"""
|
|
||||||
dc_res = lib.dc_set_location(self._dc_context, latitude, longitude, accuracy)
|
|
||||||
if dc_res == 0:
|
|
||||||
raise ValueError("no chat is streaming locations")
|
|
||||||
|
|
||||||
|
|
||||||
class IOThreads:
|
class IOThreads:
|
||||||
@@ -543,14 +376,10 @@ class IOThreads:
|
|||||||
def is_started(self):
|
def is_started(self):
|
||||||
return len(self._name2thread) > 0
|
return len(self._name2thread) > 0
|
||||||
|
|
||||||
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
|
def start(self, imap=True, smtp=True):
|
||||||
assert not self.is_started()
|
assert not self.is_started()
|
||||||
if imap:
|
if imap:
|
||||||
self._start_one_thread("inbox", self.imap_thread_run)
|
self._start_one_thread("imap", self.imap_thread_run)
|
||||||
if mvbox:
|
|
||||||
self._start_one_thread("mvbox", self.mvbox_thread_run)
|
|
||||||
if sentbox:
|
|
||||||
self._start_one_thread("sentbox", self.sentbox_thread_run)
|
|
||||||
if smtp:
|
if smtp:
|
||||||
self._start_one_thread("smtp", self.smtp_thread_run)
|
self._start_one_thread("smtp", self.smtp_thread_run)
|
||||||
|
|
||||||
@@ -563,37 +392,17 @@ class IOThreads:
|
|||||||
self._thread_quitflag = True
|
self._thread_quitflag = True
|
||||||
lib.dc_interrupt_imap_idle(self._dc_context)
|
lib.dc_interrupt_imap_idle(self._dc_context)
|
||||||
lib.dc_interrupt_smtp_idle(self._dc_context)
|
lib.dc_interrupt_smtp_idle(self._dc_context)
|
||||||
lib.dc_interrupt_mvbox_idle(self._dc_context)
|
|
||||||
lib.dc_interrupt_sentbox_idle(self._dc_context)
|
|
||||||
if wait:
|
if wait:
|
||||||
for name, thread in self._name2thread.items():
|
for name, thread in self._name2thread.items():
|
||||||
thread.join()
|
thread.join()
|
||||||
|
|
||||||
def imap_thread_run(self):
|
def imap_thread_run(self):
|
||||||
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
|
self._log_event("py-bindings-info", 0, "IMAP THREAD START")
|
||||||
while not self._thread_quitflag:
|
while not self._thread_quitflag:
|
||||||
lib.dc_perform_imap_jobs(self._dc_context)
|
lib.dc_perform_imap_jobs(self._dc_context)
|
||||||
if not self._thread_quitflag:
|
lib.dc_perform_imap_fetch(self._dc_context)
|
||||||
lib.dc_perform_imap_fetch(self._dc_context)
|
lib.dc_perform_imap_idle(self._dc_context)
|
||||||
if not self._thread_quitflag:
|
self._log_event("py-bindings-info", 0, "IMAP THREAD FINISHED")
|
||||||
lib.dc_perform_imap_idle(self._dc_context)
|
|
||||||
self._log_event("py-bindings-info", 0, "INBOX THREAD FINISHED")
|
|
||||||
|
|
||||||
def mvbox_thread_run(self):
|
|
||||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
|
|
||||||
while not self._thread_quitflag:
|
|
||||||
lib.dc_perform_mvbox_jobs(self._dc_context)
|
|
||||||
lib.dc_perform_mvbox_fetch(self._dc_context)
|
|
||||||
lib.dc_perform_mvbox_idle(self._dc_context)
|
|
||||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
|
|
||||||
|
|
||||||
def sentbox_thread_run(self):
|
|
||||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
|
|
||||||
while not self._thread_quitflag:
|
|
||||||
lib.dc_perform_sentbox_jobs(self._dc_context)
|
|
||||||
lib.dc_perform_sentbox_fetch(self._dc_context)
|
|
||||||
lib.dc_perform_sentbox_idle(self._dc_context)
|
|
||||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
|
|
||||||
|
|
||||||
def smtp_thread_run(self):
|
def smtp_thread_run(self):
|
||||||
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
|
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
|
||||||
@@ -623,10 +432,6 @@ class EventLogger:
|
|||||||
def set_timeout(self, timeout):
|
def set_timeout(self, timeout):
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
|
||||||
def consume_events(self, check_error=True):
|
|
||||||
while not self._event_queue.empty():
|
|
||||||
self.get()
|
|
||||||
|
|
||||||
def get(self, timeout=None, check_error=True):
|
def get(self, timeout=None, check_error=True):
|
||||||
timeout = timeout or self._timeout
|
timeout = timeout or self._timeout
|
||||||
ev = self._event_queue.get(timeout=timeout)
|
ev = self._event_queue.get(timeout=timeout)
|
||||||
@@ -645,11 +450,11 @@ class EventLogger:
|
|||||||
else:
|
else:
|
||||||
assert not rex.match(ev[0]), "event found {}".format(ev)
|
assert not rex.match(ev[0]), "event found {}".format(ev)
|
||||||
|
|
||||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
def get_matching(self, event_name_regex, check_error=True):
|
||||||
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
||||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||||
while 1:
|
while 1:
|
||||||
ev = self.get(timeout=timeout, check_error=check_error)
|
ev = self.get()
|
||||||
if rex.match(ev[0]):
|
if rex.match(ev[0]):
|
||||||
return ev
|
return ev
|
||||||
|
|
||||||
@@ -686,18 +491,3 @@ def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
|||||||
# we are deep into Python Interpreter shutdown,
|
# we are deep into Python Interpreter shutdown,
|
||||||
# so no need to clear the callback context mapping.
|
# so no need to clear the callback context mapping.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ScannedQRCode:
|
|
||||||
def __init__(self, dc_lot):
|
|
||||||
self._dc_lot = dc_lot
|
|
||||||
|
|
||||||
def is_ask_verifycontact(self):
|
|
||||||
return self._dc_lot.state() == const.DC_QR_ASK_VERIFYCONTACT
|
|
||||||
|
|
||||||
def is_ask_verifygroup(self):
|
|
||||||
return self._dc_lot.state() == const.DC_QR_ASK_VERIFYGROUP
|
|
||||||
|
|
||||||
@property
|
|
||||||
def contact_id(self):
|
|
||||||
return self._dc_lot.id()
|
|
||||||
|
|||||||
@@ -1,16 +1,57 @@
|
|||||||
""" Chat and Location related API. """
|
""" chatting related objects: Contact, Chat, Message. """
|
||||||
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import calendar
|
from . import props
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
import os
|
|
||||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||||
from .capi import lib, ffi
|
from .capi import lib, ffi
|
||||||
from . import const
|
from . import const
|
||||||
from .message import Message
|
from .message import Message
|
||||||
|
|
||||||
|
|
||||||
|
class Contact(object):
|
||||||
|
""" Delta-Chat Contact.
|
||||||
|
|
||||||
|
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||||
|
"""
|
||||||
|
def __init__(self, dc_context, id):
|
||||||
|
self._dc_context = dc_context
|
||||||
|
self.id = id
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self._dc_context == other._dc_context and self.id == other.id
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _dc_contact(self):
|
||||||
|
return ffi.gc(
|
||||||
|
lib.dc_get_contact(self._dc_context, self.id),
|
||||||
|
lib.dc_contact_unref
|
||||||
|
)
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def addr(self):
|
||||||
|
""" normalized e-mail address for this account. """
|
||||||
|
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def display_name(self):
|
||||||
|
""" display name for this contact. """
|
||||||
|
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||||
|
|
||||||
|
def is_blocked(self):
|
||||||
|
""" Return True if the contact is blocked. """
|
||||||
|
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||||
|
|
||||||
|
def is_verified(self):
|
||||||
|
""" Return True if the contact is verified. """
|
||||||
|
return lib.dc_contact_is_verified(self._dc_contact)
|
||||||
|
|
||||||
|
|
||||||
class Chat(object):
|
class Chat(object):
|
||||||
""" Chat object which manages members and through which you can send and retrieve messages.
|
""" Chat object which manages members and through which you can send and retrieve messages.
|
||||||
|
|
||||||
@@ -67,13 +108,6 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
||||||
|
|
||||||
def is_verified(self):
|
|
||||||
""" return True if this chat is a verified group.
|
|
||||||
|
|
||||||
:returns: True if chat is verified, False otherwise.
|
|
||||||
"""
|
|
||||||
return lib.dc_chat_is_verified(self._dc_chat)
|
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
""" return name of this chat.
|
""" return name of this chat.
|
||||||
|
|
||||||
@@ -97,16 +131,6 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
return lib.dc_chat_get_type(self._dc_chat)
|
return lib.dc_chat_get_type(self._dc_chat)
|
||||||
|
|
||||||
def get_join_qr(self):
|
|
||||||
""" get/create Join-Group QR Code as ascii-string.
|
|
||||||
|
|
||||||
this string needs to be transferred to another DC account
|
|
||||||
in a second channel (typically used by mobiles with QRcode-show + scan UX)
|
|
||||||
where account.join_with_qrcode(qr) needs to be called.
|
|
||||||
"""
|
|
||||||
res = lib.dc_get_securejoin_qr(self._dc_context, self.id)
|
|
||||||
return from_dc_charpointer(res)
|
|
||||||
|
|
||||||
# ------ chat messaging API ------------------------------
|
# ------ chat messaging API ------------------------------
|
||||||
|
|
||||||
def send_text(self, text):
|
def send_text(self, text):
|
||||||
@@ -243,12 +267,6 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
return lib.dc_marknoticed_chat(self._dc_context, self.id)
|
return lib.dc_marknoticed_chat(self._dc_context, self.id)
|
||||||
|
|
||||||
def get_summary(self):
|
|
||||||
""" return dictionary with summary information. """
|
|
||||||
dc_res = lib.dc_chat_get_info_json(self._dc_context, self.id)
|
|
||||||
s = from_dc_charpointer(dc_res)
|
|
||||||
return json.loads(s)
|
|
||||||
|
|
||||||
# ------ group management API ------------------------------
|
# ------ group management API ------------------------------
|
||||||
|
|
||||||
def add_contact(self, contact):
|
def add_contact(self, contact):
|
||||||
@@ -276,10 +294,10 @@ class Chat(object):
|
|||||||
def get_contacts(self):
|
def get_contacts(self):
|
||||||
""" get all contacts for this chat.
|
""" get all contacts for this chat.
|
||||||
:params: contact object.
|
:params: contact object.
|
||||||
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
|
:raises ValueError: if contact could not be added
|
||||||
|
:returns: list of :class:`deltachat.chatting.Contact` objects for this chat
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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._dc_context, self.id),
|
||||||
lib.dc_array_unref
|
lib.dc_array_unref
|
||||||
@@ -287,123 +305,3 @@ class Chat(object):
|
|||||||
return list(iter_array(
|
return list(iter_array(
|
||||||
dc_array, lambda id: Contact(self._dc_context, id))
|
dc_array, lambda id: Contact(self._dc_context, id))
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_profile_image(self, img_path):
|
|
||||||
"""Set group profile image.
|
|
||||||
|
|
||||||
If the group is already promoted (any message was sent to the group),
|
|
||||||
all group members are informed by a special status message that is sent
|
|
||||||
automatically by this function.
|
|
||||||
:params img_path: path to image object
|
|
||||||
:raises ValueError: if profile image could not be set
|
|
||||||
:returns: None
|
|
||||||
"""
|
|
||||||
assert os.path.exists(img_path), img_path
|
|
||||||
p = as_dc_charpointer(img_path)
|
|
||||||
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, p)
|
|
||||||
if res != 1:
|
|
||||||
raise ValueError("Setting Profile Image {!r} failed".format(p))
|
|
||||||
|
|
||||||
def remove_profile_image(self):
|
|
||||||
"""remove group profile image.
|
|
||||||
|
|
||||||
If the group is already promoted (any message was sent to the group),
|
|
||||||
all group members are informed by a special status message that is sent
|
|
||||||
automatically by this function.
|
|
||||||
:raises ValueError: if profile image could not be reset
|
|
||||||
:returns: None
|
|
||||||
"""
|
|
||||||
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, ffi.NULL)
|
|
||||||
if res != 1:
|
|
||||||
raise ValueError("Removing Profile Image failed")
|
|
||||||
|
|
||||||
def get_profile_image(self):
|
|
||||||
"""Get group profile image.
|
|
||||||
|
|
||||||
For groups, this is the image set by any group member using
|
|
||||||
set_chat_profile_image(). For normal chats, this is the image
|
|
||||||
set by each remote user on their own using dc_set_config(context,
|
|
||||||
"selfavatar", image).
|
|
||||||
:returns: path to profile image, None if no profile image exists.
|
|
||||||
"""
|
|
||||||
dc_res = lib.dc_chat_get_profile_image(self._dc_chat)
|
|
||||||
if dc_res == ffi.NULL:
|
|
||||||
return None
|
|
||||||
return from_dc_charpointer(dc_res)
|
|
||||||
|
|
||||||
def get_color(self):
|
|
||||||
"""return the color of the chat.
|
|
||||||
:returns: color as 0x00rrggbb
|
|
||||||
"""
|
|
||||||
return lib.dc_chat_get_color(self._dc_chat)
|
|
||||||
|
|
||||||
def get_subtitle(self):
|
|
||||||
"""return the subtitle of the chat
|
|
||||||
:returns: the subtitle
|
|
||||||
"""
|
|
||||||
return from_dc_charpointer(lib.dc_chat_get_subtitle(self._dc_chat))
|
|
||||||
|
|
||||||
# ------ location streaming API ------------------------------
|
|
||||||
|
|
||||||
def is_sending_locations(self):
|
|
||||||
"""return True if this chat has location-sending enabled currently.
|
|
||||||
:returns: True if location sending is enabled.
|
|
||||||
"""
|
|
||||||
return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id)
|
|
||||||
|
|
||||||
def is_archived(self):
|
|
||||||
"""return True if this chat is archived.
|
|
||||||
:returns: True if archived.
|
|
||||||
"""
|
|
||||||
return lib.dc_chat_get_archived(self._dc_chat)
|
|
||||||
|
|
||||||
def enable_sending_locations(self, seconds):
|
|
||||||
"""enable sending locations for this chat.
|
|
||||||
|
|
||||||
all subsequent messages will carry a location with them.
|
|
||||||
"""
|
|
||||||
lib.dc_send_locations_to_chat(self._dc_context, self.id, seconds)
|
|
||||||
|
|
||||||
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
|
|
||||||
"""return list of locations for the given contact in the given timespan.
|
|
||||||
|
|
||||||
:param contact: the contact for which locations shall be returned.
|
|
||||||
:param timespan_from: a datetime object or None (indicating "since beginning")
|
|
||||||
:param timespan_to: a datetime object or None (indicating up till now)
|
|
||||||
:returns: list of :class:`deltachat.chat.Location` objects.
|
|
||||||
"""
|
|
||||||
if timestamp_from is None:
|
|
||||||
time_from = 0
|
|
||||||
else:
|
|
||||||
time_from = calendar.timegm(timestamp_from.utctimetuple())
|
|
||||||
if timestamp_to is None:
|
|
||||||
time_to = 0
|
|
||||||
else:
|
|
||||||
time_to = calendar.timegm(timestamp_to.utctimetuple())
|
|
||||||
|
|
||||||
if contact is None:
|
|
||||||
contact_id = 0
|
|
||||||
else:
|
|
||||||
contact_id = contact.id
|
|
||||||
|
|
||||||
dc_array = lib.dc_get_locations(self._dc_context, self.id, contact_id, time_from, time_to)
|
|
||||||
return [
|
|
||||||
Location(
|
|
||||||
latitude=lib.dc_array_get_latitude(dc_array, i),
|
|
||||||
longitude=lib.dc_array_get_longitude(dc_array, i),
|
|
||||||
accuracy=lib.dc_array_get_accuracy(dc_array, i),
|
|
||||||
timestamp=datetime.utcfromtimestamp(lib.dc_array_get_timestamp(dc_array, i)))
|
|
||||||
for i in range(lib.dc_array_get_cnt(dc_array))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Location:
|
|
||||||
def __init__(self, latitude, longitude, accuracy, timestamp):
|
|
||||||
assert isinstance(timestamp, datetime)
|
|
||||||
self.latitude = latitude
|
|
||||||
self.longitude = longitude
|
|
||||||
self.accuracy = accuracy
|
|
||||||
self.timestamp = timestamp
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.__dict__ == other.__dict__
|
|
||||||
@@ -13,15 +13,6 @@ DC_GCL_NO_SPECIALS = 0x02
|
|||||||
DC_GCL_ADD_ALLDONE_HINT = 0x04
|
DC_GCL_ADD_ALLDONE_HINT = 0x04
|
||||||
DC_GCL_VERIFIED_ONLY = 0x01
|
DC_GCL_VERIFIED_ONLY = 0x01
|
||||||
DC_GCL_ADD_SELF = 0x02
|
DC_GCL_ADD_SELF = 0x02
|
||||||
DC_QR_ASK_VERIFYCONTACT = 200
|
|
||||||
DC_QR_ASK_VERIFYGROUP = 202
|
|
||||||
DC_QR_FPR_OK = 210
|
|
||||||
DC_QR_FPR_MISMATCH = 220
|
|
||||||
DC_QR_FPR_WITHOUT_ADDR = 230
|
|
||||||
DC_QR_ADDR = 320
|
|
||||||
DC_QR_TEXT = 330
|
|
||||||
DC_QR_URL = 332
|
|
||||||
DC_QR_ERROR = 400
|
|
||||||
DC_CHAT_ID_DEADDROP = 1
|
DC_CHAT_ID_DEADDROP = 1
|
||||||
DC_CHAT_ID_TRASH = 3
|
DC_CHAT_ID_TRASH = 3
|
||||||
DC_CHAT_ID_MSGS_IN_CREATION = 4
|
DC_CHAT_ID_MSGS_IN_CREATION = 4
|
||||||
@@ -47,40 +38,19 @@ DC_STATE_OUT_FAILED = 24
|
|||||||
DC_STATE_OUT_DELIVERED = 26
|
DC_STATE_OUT_DELIVERED = 26
|
||||||
DC_STATE_OUT_MDN_RCVD = 28
|
DC_STATE_OUT_MDN_RCVD = 28
|
||||||
DC_CONTACT_ID_SELF = 1
|
DC_CONTACT_ID_SELF = 1
|
||||||
DC_CONTACT_ID_INFO = 2
|
DC_CONTACT_ID_DEVICE = 2
|
||||||
DC_CONTACT_ID_DEVICE = 5
|
|
||||||
DC_CONTACT_ID_LAST_SPECIAL = 9
|
DC_CONTACT_ID_LAST_SPECIAL = 9
|
||||||
DC_MSG_TEXT = 10
|
DC_MSG_TEXT = 10
|
||||||
DC_MSG_IMAGE = 20
|
DC_MSG_IMAGE = 20
|
||||||
DC_MSG_GIF = 21
|
DC_MSG_GIF = 21
|
||||||
DC_MSG_STICKER = 23
|
|
||||||
DC_MSG_AUDIO = 40
|
DC_MSG_AUDIO = 40
|
||||||
DC_MSG_VOICE = 41
|
DC_MSG_VOICE = 41
|
||||||
DC_MSG_VIDEO = 50
|
DC_MSG_VIDEO = 50
|
||||||
DC_MSG_FILE = 60
|
DC_MSG_FILE = 60
|
||||||
DC_LP_AUTH_OAUTH2 = 0x2
|
|
||||||
DC_LP_AUTH_NORMAL = 0x4
|
|
||||||
DC_LP_IMAP_SOCKET_STARTTLS = 0x100
|
|
||||||
DC_LP_IMAP_SOCKET_SSL = 0x200
|
|
||||||
DC_LP_IMAP_SOCKET_PLAIN = 0x400
|
|
||||||
DC_LP_SMTP_SOCKET_STARTTLS = 0x10000
|
|
||||||
DC_LP_SMTP_SOCKET_SSL = 0x20000
|
|
||||||
DC_LP_SMTP_SOCKET_PLAIN = 0x40000
|
|
||||||
DC_CERTCK_AUTO = 0
|
|
||||||
DC_CERTCK_STRICT = 1
|
|
||||||
DC_CERTCK_ACCEPT_INVALID_HOSTNAMES = 2
|
|
||||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3
|
|
||||||
DC_EMPTY_MVBOX = 0x01
|
|
||||||
DC_EMPTY_INBOX = 0x02
|
|
||||||
DC_EVENT_INFO = 100
|
DC_EVENT_INFO = 100
|
||||||
DC_EVENT_SMTP_CONNECTED = 101
|
DC_EVENT_SMTP_CONNECTED = 101
|
||||||
DC_EVENT_IMAP_CONNECTED = 102
|
DC_EVENT_IMAP_CONNECTED = 102
|
||||||
DC_EVENT_SMTP_MESSAGE_SENT = 103
|
DC_EVENT_SMTP_MESSAGE_SENT = 103
|
||||||
DC_EVENT_IMAP_MESSAGE_DELETED = 104
|
|
||||||
DC_EVENT_IMAP_MESSAGE_MOVED = 105
|
|
||||||
DC_EVENT_IMAP_FOLDER_EMPTIED = 106
|
|
||||||
DC_EVENT_NEW_BLOB_FILE = 150
|
|
||||||
DC_EVENT_DELETED_BLOB_FILE = 151
|
|
||||||
DC_EVENT_WARNING = 300
|
DC_EVENT_WARNING = 300
|
||||||
DC_EVENT_ERROR = 400
|
DC_EVENT_ERROR = 400
|
||||||
DC_EVENT_ERROR_NETWORK = 401
|
DC_EVENT_ERROR_NETWORK = 401
|
||||||
@@ -98,67 +68,16 @@ DC_EVENT_IMEX_PROGRESS = 2051
|
|||||||
DC_EVENT_IMEX_FILE_WRITTEN = 2052
|
DC_EVENT_IMEX_FILE_WRITTEN = 2052
|
||||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
|
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
|
||||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
|
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
|
||||||
DC_EVENT_SECUREJOIN_MEMBER_ADDED = 2062
|
DC_EVENT_GET_STRING = 2091
|
||||||
|
DC_EVENT_HTTP_GET = 2100
|
||||||
|
DC_EVENT_HTTP_POST = 2110
|
||||||
DC_EVENT_FILE_COPIED = 2055
|
DC_EVENT_FILE_COPIED = 2055
|
||||||
DC_EVENT_IS_OFFLINE = 2081
|
DC_EVENT_IS_OFFLINE = 2081
|
||||||
DC_EVENT_GET_STRING = 2091
|
|
||||||
DC_STR_SELFNOTINGRP = 21
|
|
||||||
DC_PROVIDER_STATUS_OK = 1
|
|
||||||
DC_PROVIDER_STATUS_PREPARATION = 2
|
|
||||||
DC_PROVIDER_STATUS_BROKEN = 3
|
|
||||||
DC_STR_NOMESSAGES = 1
|
|
||||||
DC_STR_SELF = 2
|
|
||||||
DC_STR_DRAFT = 3
|
|
||||||
DC_STR_MEMBER = 4
|
|
||||||
DC_STR_CONTACT = 6
|
|
||||||
DC_STR_VOICEMESSAGE = 7
|
|
||||||
DC_STR_DEADDROP = 8
|
|
||||||
DC_STR_IMAGE = 9
|
|
||||||
DC_STR_VIDEO = 10
|
|
||||||
DC_STR_AUDIO = 11
|
|
||||||
DC_STR_FILE = 12
|
|
||||||
DC_STR_STATUSLINE = 13
|
|
||||||
DC_STR_NEWGROUPDRAFT = 14
|
|
||||||
DC_STR_MSGGRPNAME = 15
|
|
||||||
DC_STR_MSGGRPIMGCHANGED = 16
|
|
||||||
DC_STR_MSGADDMEMBER = 17
|
|
||||||
DC_STR_MSGDELMEMBER = 18
|
|
||||||
DC_STR_MSGGROUPLEFT = 19
|
|
||||||
DC_STR_GIF = 23
|
|
||||||
DC_STR_ENCRYPTEDMSG = 24
|
|
||||||
DC_STR_E2E_AVAILABLE = 25
|
|
||||||
DC_STR_ENCR_TRANSP = 27
|
|
||||||
DC_STR_ENCR_NONE = 28
|
|
||||||
DC_STR_CANTDECRYPT_MSG_BODY = 29
|
|
||||||
DC_STR_FINGERPRINTS = 30
|
|
||||||
DC_STR_READRCPT = 31
|
|
||||||
DC_STR_READRCPT_MAILBODY = 32
|
|
||||||
DC_STR_MSGGRPIMGDELETED = 33
|
|
||||||
DC_STR_E2E_PREFERRED = 34
|
|
||||||
DC_STR_CONTACT_VERIFIED = 35
|
|
||||||
DC_STR_CONTACT_NOT_VERIFIED = 36
|
|
||||||
DC_STR_CONTACT_SETUP_CHANGED = 37
|
|
||||||
DC_STR_ARCHIVEDCHATS = 40
|
|
||||||
DC_STR_STARREDMSGS = 41
|
|
||||||
DC_STR_AC_SETUP_MSG_SUBJECT = 42
|
|
||||||
DC_STR_AC_SETUP_MSG_BODY = 43
|
|
||||||
DC_STR_SELFTALK_SUBTITLE = 50
|
|
||||||
DC_STR_CANNOT_LOGIN = 60
|
|
||||||
DC_STR_SERVER_RESPONSE = 61
|
|
||||||
DC_STR_MSGACTIONBYUSER = 62
|
|
||||||
DC_STR_MSGACTIONBYME = 63
|
|
||||||
DC_STR_MSGLOCATIONENABLED = 64
|
|
||||||
DC_STR_MSGLOCATIONDISABLED = 65
|
|
||||||
DC_STR_LOCATION = 66
|
|
||||||
DC_STR_STICKER = 67
|
|
||||||
DC_STR_DEVICE_MESSAGES = 68
|
|
||||||
DC_STR_COUNT = 68
|
|
||||||
# end const generated
|
# end const generated
|
||||||
|
|
||||||
|
|
||||||
def read_event_defines(f):
|
def read_event_defines(f):
|
||||||
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
|
rex = re.compile(r'#define\s+((?:DC_EVENT_|DC_MSG|DC_STATE_|DC_CONTACT_ID_|DC_GCL|DC_CHAT)\S+)\s+([x\d]+).*')
|
||||||
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER)_\S+)\s+([x\d]+).*')
|
|
||||||
for line in f:
|
for line in f:
|
||||||
m = rex.match(line)
|
m = rex.match(line)
|
||||||
if m:
|
if m:
|
||||||
@@ -171,7 +90,7 @@ if __name__ == "__main__":
|
|||||||
if len(sys.argv) >= 2:
|
if len(sys.argv) >= 2:
|
||||||
deltah = sys.argv[1]
|
deltah = sys.argv[1]
|
||||||
else:
|
else:
|
||||||
deltah = joinpath(dirname(dirname(dirname(here_dir))), "deltachat-ffi", "deltachat.h")
|
deltah = joinpath(dirname(dirname(dirname(here_dir))), "src", "deltachat.h")
|
||||||
assert os.path.exists(deltah)
|
assert os.path.exists(deltah)
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
""" Contact object. """
|
|
||||||
|
|
||||||
from . import props
|
|
||||||
from .cutil import from_dc_charpointer
|
|
||||||
from .capi import lib, ffi
|
|
||||||
|
|
||||||
|
|
||||||
class Contact(object):
|
|
||||||
""" Delta-Chat Contact.
|
|
||||||
|
|
||||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
|
||||||
"""
|
|
||||||
def __init__(self, dc_context, id):
|
|
||||||
self._dc_context = dc_context
|
|
||||||
self.id = id
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self._dc_context == other._dc_context and self.id == other.id
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not (self == other)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _dc_contact(self):
|
|
||||||
return ffi.gc(
|
|
||||||
lib.dc_get_contact(self._dc_context, self.id),
|
|
||||||
lib.dc_contact_unref
|
|
||||||
)
|
|
||||||
|
|
||||||
@props.with_doc
|
|
||||||
def addr(self):
|
|
||||||
""" normalized e-mail address for this account. """
|
|
||||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
|
||||||
|
|
||||||
@props.with_doc
|
|
||||||
def display_name(self):
|
|
||||||
""" display name for this contact. """
|
|
||||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
|
||||||
|
|
||||||
def is_blocked(self):
|
|
||||||
""" Return True if the contact is blocked. """
|
|
||||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
|
||||||
|
|
||||||
def is_verified(self):
|
|
||||||
""" Return True if the contact is verified. """
|
|
||||||
return lib.dc_contact_is_verified(self._dc_contact)
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from .capi import lib
|
from .capi import lib
|
||||||
from .capi import ffi
|
from .capi import ffi
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
def as_dc_charpointer(obj):
|
def as_dc_charpointer(obj):
|
||||||
@@ -17,30 +16,4 @@ def iter_array(dc_array_t, constructor):
|
|||||||
|
|
||||||
|
|
||||||
def from_dc_charpointer(obj):
|
def from_dc_charpointer(obj):
|
||||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
return ffi.string(obj).decode("utf8")
|
||||||
|
|
||||||
|
|
||||||
class DCLot:
|
|
||||||
def __init__(self, dc_lot):
|
|
||||||
self._dc_lot = dc_lot
|
|
||||||
|
|
||||||
def id(self):
|
|
||||||
return lib.dc_lot_get_id(self._dc_lot)
|
|
||||||
|
|
||||||
def state(self):
|
|
||||||
return lib.dc_lot_get_state(self._dc_lot)
|
|
||||||
|
|
||||||
def text1(self):
|
|
||||||
return from_dc_charpointer(lib.dc_lot_get_text1(self._dc_lot))
|
|
||||||
|
|
||||||
def text1_meaning(self):
|
|
||||||
return lib.dc_lot_get_text1_meaning(self._dc_lot)
|
|
||||||
|
|
||||||
def text2(self):
|
|
||||||
return from_dc_charpointer(lib.dc_lot_get_text2(self._dc_lot))
|
|
||||||
|
|
||||||
def timestamp(self):
|
|
||||||
ts = lib.dc_lot_get_timestamp(self._dc_lot)
|
|
||||||
if ts == 0:
|
|
||||||
return None
|
|
||||||
return datetime.utcfromtimestamp(ts)
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
""" The Message object. """
|
""" chatting related objects: Contact, Chat, Message. """
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from . import props
|
from . import props
|
||||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||||
from .capi import lib, ffi
|
from .capi import lib, ffi
|
||||||
@@ -12,7 +13,7 @@ class Message(object):
|
|||||||
""" Message object.
|
""" Message object.
|
||||||
|
|
||||||
You obtain instances of it through :class:`deltachat.account.Account` or
|
You obtain instances of it through :class:`deltachat.account.Account` or
|
||||||
:class:`deltachat.chat.Chat`.
|
:class:`deltachat.chatting.Chat`.
|
||||||
"""
|
"""
|
||||||
def __init__(self, account, dc_msg):
|
def __init__(self, account, dc_msg):
|
||||||
self.account = account
|
self.account = account
|
||||||
@@ -57,6 +58,8 @@ class Message(object):
|
|||||||
|
|
||||||
def set_text(self, text):
|
def set_text(self, text):
|
||||||
"""set text of this message. """
|
"""set text of this message. """
|
||||||
|
assert self.id > 0, "message not prepared"
|
||||||
|
assert self.is_out_preparing()
|
||||||
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
|
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
@@ -69,6 +72,19 @@ class Message(object):
|
|||||||
mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type)
|
mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type)
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
raise ValueError("path does not exist: {!r}".format(path))
|
raise ValueError("path does not exist: {!r}".format(path))
|
||||||
|
blobdir = self.account.get_blobdir()
|
||||||
|
if not path.startswith(blobdir):
|
||||||
|
for i in range(50):
|
||||||
|
ext = "" if i == 0 else "-" + str(i)
|
||||||
|
dest = os.path.join(blobdir, os.path.basename(path) + ext)
|
||||||
|
if os.path.exists(dest):
|
||||||
|
continue
|
||||||
|
shutil.copyfile(path, dest)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("could not create blobdir-path for {}".format(path))
|
||||||
|
path = dest
|
||||||
|
assert path.startswith(blobdir), path
|
||||||
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
@@ -85,18 +101,6 @@ class Message(object):
|
|||||||
""" return True if this message is a setup message. """
|
""" return True if this message is a setup message. """
|
||||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||||
|
|
||||||
def get_setupcodebegin(self):
|
|
||||||
""" return the first characters of a setup code in a setup message. """
|
|
||||||
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
|
|
||||||
|
|
||||||
def is_encrypted(self):
|
|
||||||
""" return True if this message was encrypted. """
|
|
||||||
return bool(lib.dc_msg_get_showpadlock(self._dc_msg))
|
|
||||||
|
|
||||||
def is_forwarded(self):
|
|
||||||
""" return True if this message was forwarded. """
|
|
||||||
return bool(lib.dc_msg_is_forwarded(self._dc_msg))
|
|
||||||
|
|
||||||
def get_message_info(self):
|
def get_message_info(self):
|
||||||
""" Return informational text for a single message.
|
""" Return informational text for a single message.
|
||||||
|
|
||||||
@@ -106,13 +110,7 @@ class Message(object):
|
|||||||
|
|
||||||
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(
|
lib.dc_continue_key_transfer(self._dc_context, self.id, as_dc_charpointer(setup_code))
|
||||||
self._dc_context,
|
|
||||||
self.id,
|
|
||||||
as_dc_charpointer(setup_code)
|
|
||||||
)
|
|
||||||
if res == 0:
|
|
||||||
raise ValueError("could not decrypt")
|
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
def time_sent(self):
|
def time_sent(self):
|
||||||
@@ -144,27 +142,27 @@ class Message(object):
|
|||||||
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._dc_context, self.id)
|
||||||
if mime_headers:
|
if mime_headers:
|
||||||
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
|
s = ffi.string(mime_headers)
|
||||||
if isinstance(s, bytes):
|
if isinstance(s, bytes):
|
||||||
return email.message_from_bytes(s)
|
s = s.decode("ascii")
|
||||||
return email.message_from_string(s)
|
return email.message_from_string(s)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chat(self):
|
def chat(self):
|
||||||
"""chat this message was posted in.
|
"""chat this message was posted in.
|
||||||
|
|
||||||
:returns: :class:`deltachat.chat.Chat` object
|
:returns: :class:`deltachat.chatting.Chat` object
|
||||||
"""
|
"""
|
||||||
from .chat import Chat
|
from .chatting import Chat
|
||||||
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
||||||
return Chat(self.account, chat_id)
|
return Chat(self.account, chat_id)
|
||||||
|
|
||||||
def get_sender_contact(self):
|
def get_sender_contact(self):
|
||||||
"""return the contact of who wrote the message.
|
"""return the contact of who wrote the message.
|
||||||
|
|
||||||
:returns: :class:`deltachat.chat.Contact` instance
|
:returns: :class:`deltachat.chatting.Contact` instance
|
||||||
"""
|
"""
|
||||||
from .contact import Contact
|
from .chatting import Contact
|
||||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||||
return Contact(self._dc_context, contact_id)
|
return Contact(self._dc_context, contact_id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
"""Provider info class."""
|
|
||||||
|
|
||||||
from .capi import ffi, lib
|
|
||||||
from .cutil import as_dc_charpointer, from_dc_charpointer
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderNotFoundError(Exception):
|
|
||||||
"""The provider information was not found."""
|
|
||||||
|
|
||||||
|
|
||||||
class Provider(object):
|
|
||||||
"""Provider information.
|
|
||||||
|
|
||||||
:param domain: The domain to get the provider info for, this is
|
|
||||||
normally the part following the `@` of the domain.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, domain):
|
|
||||||
provider = ffi.gc(
|
|
||||||
lib.dc_provider_new_from_domain(as_dc_charpointer(domain)),
|
|
||||||
lib.dc_provider_unref,
|
|
||||||
)
|
|
||||||
if provider == ffi.NULL:
|
|
||||||
raise ProviderNotFoundError("Provider not found")
|
|
||||||
self._provider = provider
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_email(cls, email):
|
|
||||||
"""Create provider info from an email address.
|
|
||||||
|
|
||||||
:param email: Email address to get provider info for.
|
|
||||||
"""
|
|
||||||
return cls(email.split('@')[-1])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def overview_page(self):
|
|
||||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
|
||||||
return from_dc_charpointer(
|
|
||||||
lib.dc_provider_get_overview_page(self._provider))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
"""The name of the provider."""
|
|
||||||
return from_dc_charpointer(lib.dc_provider_get_name(self._provider))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def markdown(self):
|
|
||||||
"""Content of the information page, formatted as markdown."""
|
|
||||||
return from_dc_charpointer(
|
|
||||||
lib.dc_provider_get_markdown(self._provider))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status_date(self):
|
|
||||||
"""The date the provider info was last updated, as a string."""
|
|
||||||
return from_dc_charpointer(
|
|
||||||
lib.dc_provider_get_status_date(self._provider))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self):
|
|
||||||
"""The status of the provider information.
|
|
||||||
|
|
||||||
This is one of the
|
|
||||||
:attr:`deltachat.const.DC_PROVIDER_STATUS_OK`,
|
|
||||||
:attr:`deltachat.const.DC_PROVIDER_STATUS_PREPARATION` or
|
|
||||||
:attr:`deltachat.const.DC_PROVIDER_STATUS_BROKEN` constants.
|
|
||||||
"""
|
|
||||||
return lib.dc_provider_get_status(self._provider)
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
|
||||||
import time
|
import time
|
||||||
from deltachat import Account
|
from deltachat import Account
|
||||||
from deltachat import const
|
from deltachat import props
|
||||||
from deltachat.capi import lib
|
from deltachat.capi import lib
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
@@ -25,9 +24,18 @@ def pytest_configure(config):
|
|||||||
config.option.liveconfig = cfg
|
config.option.liveconfig = cfg
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_header(config, startdir):
|
@pytest.hookimpl(trylast=True)
|
||||||
summary = []
|
def pytest_runtest_call(item):
|
||||||
|
# perform early finalization because we otherwise get cloberred
|
||||||
|
# output from concurrent threads printing between execution
|
||||||
|
# of the test function and the teardown phase of that test function
|
||||||
|
if "acfactory" in item.funcargs:
|
||||||
|
print("*"*30, "finalizing", "*"*30)
|
||||||
|
acfactory = item.funcargs["acfactory"]
|
||||||
|
acfactory.finalize()
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_report_header(config, startdir):
|
||||||
t = tempfile.mktemp()
|
t = tempfile.mktemp()
|
||||||
try:
|
try:
|
||||||
ac = Account(t, eventlogging=False)
|
ac = Account(t, eventlogging=False)
|
||||||
@@ -35,18 +43,13 @@ def pytest_report_header(config, startdir):
|
|||||||
ac.shutdown()
|
ac.shutdown()
|
||||||
finally:
|
finally:
|
||||||
os.remove(t)
|
os.remove(t)
|
||||||
summary.extend(['Deltachat core={} sqlite={}'.format(
|
summary = ['Deltachat core={} sqlite={}'.format(
|
||||||
info['deltachat_core_version'],
|
info['deltachat_core_version'],
|
||||||
info['sqlite_version'],
|
info['sqlite_version'],
|
||||||
)])
|
)]
|
||||||
|
cfg = config.getoption('--liveconfig')
|
||||||
cfg = config.option.liveconfig
|
|
||||||
if cfg:
|
if cfg:
|
||||||
if "#" in cfg:
|
summary.append('Liveconfig: {}'.format(os.path.abspath(cfg)))
|
||||||
url, token = cfg.split("#", 1)
|
|
||||||
summary.append('Liveconfig provider: {}#<token ommitted>'.format(url))
|
|
||||||
else:
|
|
||||||
summary.append('Liveconfig file: {}'.format(cfg))
|
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
|
||||||
@@ -63,56 +66,9 @@ def data():
|
|||||||
return Data()
|
return Data()
|
||||||
|
|
||||||
|
|
||||||
class SessionLiveConfigFromFile:
|
|
||||||
def __init__(self, fn):
|
|
||||||
self.fn = fn
|
|
||||||
self.configlist = []
|
|
||||||
for line in open(fn):
|
|
||||||
if line.strip() and not line.strip().startswith('#'):
|
|
||||||
d = {}
|
|
||||||
for part in line.split():
|
|
||||||
name, value = part.split("=")
|
|
||||||
d[name] = value
|
|
||||||
self.configlist.append(d)
|
|
||||||
|
|
||||||
def get(self, index):
|
|
||||||
return self.configlist[index]
|
|
||||||
|
|
||||||
def exists(self):
|
|
||||||
return bool(self.configlist)
|
|
||||||
|
|
||||||
|
|
||||||
class SessionLiveConfigFromURL:
|
|
||||||
def __init__(self, url, create_token):
|
|
||||||
self.configlist = []
|
|
||||||
for i in range(2):
|
|
||||||
res = requests.post(url, json={"token_create_user": int(create_token)})
|
|
||||||
if res.status_code != 200:
|
|
||||||
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
|
||||||
d = res.json()
|
|
||||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
|
||||||
self.configlist.append(config)
|
|
||||||
|
|
||||||
def get(self, index):
|
|
||||||
return self.configlist[index]
|
|
||||||
|
|
||||||
def exists(self):
|
|
||||||
return bool(self.configlist)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def session_liveconfig(request):
|
|
||||||
liveconfig_opt = request.config.option.liveconfig
|
|
||||||
if liveconfig_opt:
|
|
||||||
if liveconfig_opt.startswith("http"):
|
|
||||||
url, create_token = liveconfig_opt.split("#", 1)
|
|
||||||
return SessionLiveConfigFromURL(url, create_token)
|
|
||||||
else:
|
|
||||||
return SessionLiveConfigFromFile(liveconfig_opt)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
|
def acfactory(pytestconfig, tmpdir, request):
|
||||||
|
fn = pytestconfig.getoption("--liveconfig")
|
||||||
|
|
||||||
class AccountMaker:
|
class AccountMaker:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -126,17 +82,25 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
|
|||||||
fin = self._finalizers.pop()
|
fin = self._finalizers.pop()
|
||||||
fin()
|
fin()
|
||||||
|
|
||||||
def make_account(self, path, logid):
|
@props.cached
|
||||||
ac = Account(path, logid=logid)
|
def configlist(self):
|
||||||
self._finalizers.append(ac.shutdown)
|
configlist = []
|
||||||
return ac
|
for line in open(fn):
|
||||||
|
if line.strip() and not line.strip().startswith('#'):
|
||||||
|
d = {}
|
||||||
|
for part in line.split():
|
||||||
|
name, value = part.split("=")
|
||||||
|
d[name] = value
|
||||||
|
configlist.append(d)
|
||||||
|
return configlist
|
||||||
|
|
||||||
def get_unconfigured_account(self):
|
def get_unconfigured_account(self):
|
||||||
self.offline_count += 1
|
self.offline_count += 1
|
||||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
ac = Account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||||
ac._evlogger.init_time = self.init_time
|
ac._evlogger.init_time = self.init_time
|
||||||
ac._evlogger.set_timeout(2)
|
ac._evlogger.set_timeout(2)
|
||||||
|
self._finalizers.append(ac.shutdown)
|
||||||
return ac
|
return ac
|
||||||
|
|
||||||
def get_configured_offline_account(self):
|
def get_configured_offline_account(self):
|
||||||
@@ -151,63 +115,32 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
|
|||||||
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
||||||
return ac
|
return ac
|
||||||
|
|
||||||
def peek_online_config(self):
|
def get_online_configuring_account(self):
|
||||||
if not session_liveconfig:
|
if not fn:
|
||||||
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
|
pytest.skip("specify a --liveconfig file to run tests with real accounts")
|
||||||
return session_liveconfig.get(self.live_count)
|
|
||||||
|
|
||||||
def get_online_config(self):
|
|
||||||
if not session_liveconfig:
|
|
||||||
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
|
|
||||||
configdict = session_liveconfig.get(self.live_count)
|
|
||||||
self.live_count += 1
|
self.live_count += 1
|
||||||
if "e2ee_enabled" not in configdict:
|
configdict = self.configlist.pop(0)
|
||||||
configdict["e2ee_enabled"] = "1"
|
|
||||||
|
|
||||||
# Enable strict certificate checks for online accounts
|
|
||||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
|
||||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
|
||||||
|
|
||||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
ac = Account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||||
ac._evlogger.init_time = self.init_time
|
ac._evlogger.init_time = self.init_time
|
||||||
ac._evlogger.set_timeout(30)
|
ac._evlogger.set_timeout(30)
|
||||||
return ac, dict(configdict)
|
|
||||||
|
|
||||||
def get_online_configuring_account(self, mvbox=False, sentbox=False):
|
|
||||||
ac, configdict = self.get_online_config()
|
|
||||||
ac.configure(**configdict)
|
ac.configure(**configdict)
|
||||||
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
|
ac.start_threads()
|
||||||
|
self._finalizers.append(ac.shutdown)
|
||||||
return ac
|
return ac
|
||||||
|
|
||||||
def get_one_online_account(self):
|
|
||||||
ac1 = self.get_online_configuring_account()
|
|
||||||
wait_successful_IMAP_SMTP_connection(ac1)
|
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
return ac1
|
|
||||||
|
|
||||||
def get_two_online_accounts(self):
|
|
||||||
ac1 = self.get_online_configuring_account()
|
|
||||||
ac2 = self.get_online_configuring_account()
|
|
||||||
wait_successful_IMAP_SMTP_connection(ac1)
|
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
wait_successful_IMAP_SMTP_connection(ac2)
|
|
||||||
wait_configuration_progress(ac2, 1000)
|
|
||||||
return ac1, ac2
|
|
||||||
|
|
||||||
def clone_online_account(self, account):
|
def clone_online_account(self, account):
|
||||||
self.live_count += 1
|
self.live_count += 1
|
||||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
ac = Account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||||
ac._evlogger.init_time = self.init_time
|
ac._evlogger.init_time = self.init_time
|
||||||
ac._evlogger.set_timeout(30)
|
ac._evlogger.set_timeout(30)
|
||||||
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
|
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
|
||||||
ac.start_threads()
|
ac.start_threads()
|
||||||
|
self._finalizers.append(ac.shutdown)
|
||||||
return ac
|
return ac
|
||||||
|
|
||||||
am = AccountMaker()
|
return AccountMaker()
|
||||||
request.addfinalizer(am.finalize)
|
|
||||||
return am
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -227,22 +160,12 @@ def lp():
|
|||||||
return Printer()
|
return Printer()
|
||||||
|
|
||||||
|
|
||||||
def wait_configuration_progress(account, min_target, max_target=1001):
|
def wait_configuration_progress(account, target):
|
||||||
min_target = min(min_target, max_target)
|
|
||||||
while 1:
|
while 1:
|
||||||
evt_name, data1, data2 = \
|
evt_name, data1, data2 = \
|
||||||
account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
||||||
if data1 >= min_target and data1 <= max_target:
|
if data1 >= target:
|
||||||
print("** CONFIG PROGRESS {}".format(min_target), account)
|
print("** CONFIG PROGRESS {}".format(target), account)
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def wait_securejoin_inviter_progress(account, target):
|
|
||||||
while 1:
|
|
||||||
evt_name, data1, data2 = \
|
|
||||||
account._evlogger.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
|
||||||
if data2 >= target:
|
|
||||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import pytest
|
import pytest
|
||||||
import os
|
import os
|
||||||
import queue
|
|
||||||
import time
|
|
||||||
from deltachat import const, Account
|
from deltachat import const, Account
|
||||||
from deltachat.message import Message
|
from deltachat.message import Message
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection, wait_securejoin_inviter_progress
|
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection
|
||||||
|
|
||||||
|
|
||||||
class TestOfflineAccountBasic:
|
class TestOfflineAccountBasic:
|
||||||
@@ -21,7 +19,6 @@ class TestOfflineAccountBasic:
|
|||||||
d = ac1.get_info()
|
d = ac1.get_info()
|
||||||
assert d["arch"]
|
assert d["arch"]
|
||||||
assert d["number_of_chats"] == "0"
|
assert d["number_of_chats"] == "0"
|
||||||
assert d["bcc_self"] == "0"
|
|
||||||
|
|
||||||
def test_is_not_configured(self, acfactory):
|
def test_is_not_configured(self, acfactory):
|
||||||
ac1 = acfactory.get_unconfigured_account()
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
@@ -40,11 +37,6 @@ class TestOfflineAccountBasic:
|
|||||||
ac1 = acfactory.get_unconfigured_account()
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
assert "save_mime_headers" in ac1.get_config("sys.config_keys").split()
|
assert "save_mime_headers" in ac1.get_config("sys.config_keys").split()
|
||||||
|
|
||||||
def test_has_bccself(self, acfactory):
|
|
||||||
ac1 = acfactory.get_unconfigured_account()
|
|
||||||
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
|
|
||||||
assert ac1.get_config("bcc_self") == "0"
|
|
||||||
|
|
||||||
def test_selfcontact_if_unconfigured(self, acfactory):
|
def test_selfcontact_if_unconfigured(self, acfactory):
|
||||||
ac1 = acfactory.get_unconfigured_account()
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@@ -101,9 +93,8 @@ class TestOfflineContact:
|
|||||||
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)
|
||||||
msg = chat.send_text("one message")
|
chat.send_text("one messae")
|
||||||
assert not ac1.delete_contact(contact1)
|
assert not ac1.delete_contact(contact1)
|
||||||
assert not msg.filemime
|
|
||||||
|
|
||||||
|
|
||||||
class TestOfflineChat:
|
class TestOfflineChat:
|
||||||
@@ -115,19 +106,13 @@ class TestOfflineChat:
|
|||||||
def chat1(self, ac1):
|
def chat1(self, ac1):
|
||||||
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||||
chat = ac1.create_chat_by_contact(contact1)
|
chat = ac1.create_chat_by_contact(contact1)
|
||||||
assert chat.id > const.DC_CHAT_ID_LAST_SPECIAL, chat.id
|
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL, chat.id
|
||||||
return chat
|
return chat
|
||||||
|
|
||||||
def test_display(self, chat1):
|
def test_display(self, chat1):
|
||||||
str(chat1)
|
str(chat1)
|
||||||
repr(chat1)
|
repr(chat1)
|
||||||
|
|
||||||
def test_chat_by_id(self, chat1):
|
|
||||||
chat2 = chat1.account.get_chat_by_id(chat1.id)
|
|
||||||
assert chat2 == chat1
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
chat1.account.get_chat_by_id(123123)
|
|
||||||
|
|
||||||
def test_chat_idempotent(self, chat1, ac1):
|
def test_chat_idempotent(self, chat1, ac1):
|
||||||
contact1 = chat1.get_contacts()[0]
|
contact1 = chat1.get_contacts()[0]
|
||||||
chat2 = ac1.create_chat_by_contact(contact1.id)
|
chat2 = ac1.create_chat_by_contact(contact1.id)
|
||||||
@@ -155,55 +140,6 @@ class TestOfflineChat:
|
|||||||
chat.set_name("title2")
|
chat.set_name("title2")
|
||||||
assert chat.get_name() == "title2"
|
assert chat.get_name() == "title2"
|
||||||
|
|
||||||
d = chat.get_summary()
|
|
||||||
print(d)
|
|
||||||
assert d["id"] == chat.id
|
|
||||||
assert d["type"] == chat.get_type()
|
|
||||||
assert d["name"] == chat.get_name()
|
|
||||||
assert d["archived"] == chat.is_archived()
|
|
||||||
# assert d["param"] == chat.param
|
|
||||||
assert d["color"] == chat.get_color()
|
|
||||||
assert d["profile_image"] == "" if chat.get_profile_image() is None else chat.get_profile_image()
|
|
||||||
assert d["subtitle"] == chat.get_subtitle()
|
|
||||||
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
|
|
||||||
|
|
||||||
def test_group_chat_creation_with_translation(self, ac1):
|
|
||||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
|
|
||||||
ac1._evlogger.consume_events()
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %2$s")
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_WARNING")
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
ac1.set_stock_translation(500, "xyz %1$s")
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_WARNING")
|
|
||||||
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
|
||||||
contact2 = ac1.create_contact("some2@hello.com", name="some2")
|
|
||||||
chat = ac1.create_group_chat(name="title1")
|
|
||||||
chat.add_contact(contact1)
|
|
||||||
chat.add_contact(contact2)
|
|
||||||
assert chat.get_name() == "title1"
|
|
||||||
assert contact1 in chat.get_contacts()
|
|
||||||
assert contact2 in chat.get_contacts()
|
|
||||||
assert not chat.is_promoted()
|
|
||||||
msg = chat.get_draft()
|
|
||||||
assert msg.text == "xyz title1"
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("verified", [True, False])
|
|
||||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
|
||||||
ac2 = acfactory.get_configured_offline_account()
|
|
||||||
chat = ac1.create_group_chat(name="title1", verified=verified)
|
|
||||||
qr = chat.get_join_qr()
|
|
||||||
assert ac2.check_qr(qr).is_ask_verifygroup
|
|
||||||
|
|
||||||
def test_get_set_profile_image_simple(self, ac1, data):
|
|
||||||
chat = ac1.create_group_chat(name="title1")
|
|
||||||
p = data.get_path("d.png")
|
|
||||||
chat.set_profile_image(p)
|
|
||||||
p2 = chat.get_profile_image()
|
|
||||||
assert open(p, "rb").read() == open(p2, "rb").read()
|
|
||||||
chat.remove_profile_image()
|
|
||||||
assert chat.get_profile_image() is None
|
|
||||||
|
|
||||||
def test_delete_and_send_fails(self, ac1, chat1):
|
def test_delete_and_send_fails(self, ac1, chat1):
|
||||||
chat1.delete()
|
chat1.delete()
|
||||||
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
@@ -269,9 +205,7 @@ class TestOfflineChat:
|
|||||||
chat1.send_image(path="notexists")
|
chat1.send_image(path="notexists")
|
||||||
fn = data.get_path("d.png")
|
fn = data.get_path("d.png")
|
||||||
lp.sec("sending image")
|
lp.sec("sending image")
|
||||||
chat1.account._evlogger.consume_events()
|
|
||||||
msg = chat1.send_image(fn)
|
msg = chat1.send_image(fn)
|
||||||
chat1.account._evlogger.get_matching("DC_EVENT_NEW_BLOB_FILE")
|
|
||||||
assert msg.is_image()
|
assert msg.is_image()
|
||||||
assert msg
|
assert msg
|
||||||
assert msg.id > 0
|
assert msg.id > 0
|
||||||
@@ -343,10 +277,10 @@ class TestOfflineChat:
|
|||||||
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_to_dir(backupdir.strpath)
|
||||||
assert os.path.exists(path)
|
assert os.path.exists(path)
|
||||||
ac2 = acfactory.get_unconfigured_account()
|
ac2 = acfactory.get_unconfigured_account()
|
||||||
ac2.import_all(path)
|
ac2.import_from_file(path)
|
||||||
contacts = ac2.get_contacts(query="some1")
|
contacts = ac2.get_contacts(query="some1")
|
||||||
assert len(contacts) == 1
|
assert len(contacts) == 1
|
||||||
contact2 = contacts[0]
|
contact2 = contacts[0]
|
||||||
@@ -372,254 +306,87 @@ class TestOfflineChat:
|
|||||||
chat1.set_draft(None)
|
chat1.set_draft(None)
|
||||||
assert chat1.get_draft() is None
|
assert chat1.get_draft() is None
|
||||||
|
|
||||||
def test_qr_setup_contact(self, acfactory, lp):
|
|
||||||
ac1 = acfactory.get_configured_offline_account()
|
|
||||||
ac2 = acfactory.get_configured_offline_account()
|
|
||||||
qr = ac1.get_setup_contact_qr()
|
|
||||||
assert qr.startswith("OPENPGP4FPR:")
|
|
||||||
res = ac2.check_qr(qr)
|
|
||||||
assert res.is_ask_verifycontact()
|
|
||||||
assert not res.is_ask_verifygroup()
|
|
||||||
assert res.contact_id == 10
|
|
||||||
|
|
||||||
|
|
||||||
class TestOnlineAccount:
|
class TestOnlineAccount:
|
||||||
def get_chat(self, ac1, ac2, both_created=False):
|
def test_one_account_init(self, acfactory):
|
||||||
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
|
||||||
chat = ac1.create_chat_by_contact(c2)
|
|
||||||
assert chat.id > const.DC_CHAT_ID_LAST_SPECIAL
|
|
||||||
if both_created:
|
|
||||||
ac2.create_chat_by_contact(ac2.create_contact(email=ac1.get_config("addr")))
|
|
||||||
return chat
|
|
||||||
|
|
||||||
def test_configure_canceled(self, acfactory):
|
|
||||||
ac1 = acfactory.get_online_configuring_account()
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
wait_configuration_progress(ac1, 200)
|
|
||||||
ac1.stop_ongoing()
|
|
||||||
wait_configuration_progress(ac1, 0, 0)
|
|
||||||
|
|
||||||
def test_export_import_self_keys(self, acfactory, tmpdir):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
dir = tmpdir.mkdir("exportdir")
|
|
||||||
export_files = ac1.export_self_keys(dir.strpath)
|
|
||||||
assert len(export_files) == 2
|
|
||||||
for x in export_files:
|
|
||||||
assert x.startswith(dir.strpath)
|
|
||||||
ac1._evlogger.consume_events()
|
|
||||||
ac1.import_self_keys(dir.strpath)
|
|
||||||
|
|
||||||
def test_one_account_send_bcc_setting(self, acfactory, lp):
|
|
||||||
ac1 = acfactory.get_online_configuring_account()
|
|
||||||
ac2_config = acfactory.peek_online_config()
|
|
||||||
c2 = ac1.create_contact(email=ac2_config["addr"])
|
|
||||||
chat = ac1.create_chat_by_contact(c2)
|
|
||||||
assert chat.id > const.DC_CHAT_ID_LAST_SPECIAL
|
|
||||||
wait_successful_IMAP_SMTP_connection(ac1)
|
wait_successful_IMAP_SMTP_connection(ac1)
|
||||||
wait_configuration_progress(ac1, 1000)
|
wait_configuration_progress(ac1, 1000)
|
||||||
|
|
||||||
lp.sec("ac1: setting bcc_self=1")
|
def test_one_account_send(self, acfactory):
|
||||||
ac1.set_config("bcc_self", "1")
|
|
||||||
|
|
||||||
lp.sec("send out message with bcc to ourselves")
|
|
||||||
msg_out = chat.send_text("message2")
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
|
||||||
assert ev[2] == msg_out.id
|
|
||||||
# wait for send out (BCC)
|
|
||||||
assert ac1.get_config("bcc_self") == "1"
|
|
||||||
self_addr = ac1.get_config("addr")
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
|
||||||
assert self_addr in ev[2]
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
|
|
||||||
|
|
||||||
ac1._evlogger.consume_events()
|
|
||||||
lp.sec("send out message without bcc")
|
|
||||||
ac1.set_config("bcc_self", "0")
|
|
||||||
msg_out = chat.send_text("message3")
|
|
||||||
assert not msg_out.is_forwarded()
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
|
||||||
assert ev[2] == msg_out.id
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
|
||||||
assert self_addr not in ev[2]
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
|
|
||||||
|
|
||||||
def test_send_file_twice_unicode_filename_mangling(self, tmpdir, acfactory, lp):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
chat = self.get_chat(ac1, ac2)
|
|
||||||
|
|
||||||
basename = "somedäüta.html.zip"
|
|
||||||
p = os.path.join(tmpdir.strpath, basename)
|
|
||||||
with open(p, "w") as f:
|
|
||||||
f.write("some data")
|
|
||||||
|
|
||||||
def send_and_receive_message():
|
|
||||||
lp.sec("ac1: prepare and send attachment + text to ac2")
|
|
||||||
msg1 = Message.new_empty(ac1, "file")
|
|
||||||
msg1.set_text("withfile")
|
|
||||||
msg1.set_file(p)
|
|
||||||
message = chat.prepare_message(msg1)
|
|
||||||
assert message.is_out_preparing()
|
|
||||||
assert message.text == "withfile"
|
|
||||||
chat.send_prepared(message)
|
|
||||||
|
|
||||||
lp.sec("ac2: receive message")
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
|
||||||
assert ev[2] > const.DC_CHAT_ID_LAST_SPECIAL
|
|
||||||
return ac2.get_message_by_id(ev[2])
|
|
||||||
|
|
||||||
msg = send_and_receive_message()
|
|
||||||
assert msg.text == "withfile"
|
|
||||||
assert open(msg.filename).read() == "some data"
|
|
||||||
assert msg.filename.endswith(basename)
|
|
||||||
|
|
||||||
msg2 = send_and_receive_message()
|
|
||||||
assert msg2.text == "withfile"
|
|
||||||
assert open(msg2.filename).read() == "some data"
|
|
||||||
assert msg2.filename.endswith("html.zip")
|
|
||||||
assert msg.filename != msg2.filename
|
|
||||||
|
|
||||||
def test_send_file_html_attachment(self, tmpdir, acfactory, lp):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
chat = self.get_chat(ac1, ac2)
|
|
||||||
|
|
||||||
basename = "test.html"
|
|
||||||
content = "<html><body>text</body>data"
|
|
||||||
|
|
||||||
p = os.path.join(tmpdir.strpath, basename)
|
|
||||||
with open(p, "w") as f:
|
|
||||||
# write wrong html to see if core tries to parse it
|
|
||||||
# (it shouldn't as it's a file attachment)
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
lp.sec("ac1: prepare and send attachment + text to ac2")
|
|
||||||
chat.send_file(p, mime_type="text/html")
|
|
||||||
|
|
||||||
lp.sec("ac2: receive message")
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
|
||||||
assert ev[2] > const.DC_CHAT_ID_LAST_SPECIAL
|
|
||||||
msg = ac2.get_message_by_id(ev[2])
|
|
||||||
|
|
||||||
assert open(msg.filename).read() == content
|
|
||||||
assert msg.filename.endswith(basename)
|
|
||||||
|
|
||||||
def test_mvbox_sentbox_threads(self, acfactory, lp):
|
|
||||||
lp.sec("ac1: start with mvbox thread")
|
|
||||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, sentbox=True)
|
|
||||||
|
|
||||||
lp.sec("ac2: start without mvbox/sentbox threads")
|
|
||||||
ac2 = acfactory.get_online_configuring_account()
|
|
||||||
|
|
||||||
lp.sec("ac2: waiting for configuration")
|
|
||||||
wait_configuration_progress(ac2, 1000)
|
|
||||||
|
|
||||||
lp.sec("ac1: waiting for configuration")
|
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
|
|
||||||
lp.sec("ac1: send message and wait for ac2 to receive it")
|
|
||||||
chat = self.get_chat(ac1, ac2)
|
|
||||||
chat.send_text("message1")
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
|
||||||
assert ev[2] > const.DC_CHAT_ID_LAST_SPECIAL
|
|
||||||
lp.sec("test finished")
|
|
||||||
|
|
||||||
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)
|
c2 = ac1.create_contact(email=ac1.get_config("addr"))
|
||||||
wait_configuration_progress(ac2, 1000)
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
wait_successful_IMAP_SMTP_connection(ac1)
|
||||||
wait_configuration_progress(ac1, 1000)
|
wait_configuration_progress(ac1, 1000)
|
||||||
chat = self.get_chat(ac1, ac2)
|
|
||||||
chat.send_text("message1")
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
|
||||||
assert ev[2] > const.DC_CHAT_ID_LAST_SPECIAL
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
|
||||||
|
|
||||||
def test_move_works_on_self_sent(self, acfactory):
|
msg_out = chat.send_text("message2")
|
||||||
ac1 = acfactory.get_online_configuring_account(mvbox=True)
|
# wait for own account to receive
|
||||||
ac1.set_config("bcc_self", "1")
|
ev = ac1._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||||
|
assert ev[1] == msg_out.id
|
||||||
|
|
||||||
|
def test_two_accounts_send_receive(self, acfactory):
|
||||||
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
ac2 = acfactory.get_online_configuring_account()
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
wait_configuration_progress(ac2, 1000)
|
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||||
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
wait_successful_IMAP_SMTP_connection(ac1)
|
||||||
wait_configuration_progress(ac1, 1000)
|
wait_configuration_progress(ac1, 1000)
|
||||||
chat = self.get_chat(ac1, ac2)
|
wait_successful_IMAP_SMTP_connection(ac2)
|
||||||
chat.send_text("message1")
|
wait_configuration_progress(ac2, 1000)
|
||||||
chat.send_text("message2")
|
|
||||||
chat.send_text("message3")
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
|
||||||
|
|
||||||
def test_forward_messages(self, acfactory, lp):
|
msg_out = chat.send_text("message1")
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
chat = self.get_chat(ac1, ac2)
|
# wait for other account to receive
|
||||||
|
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||||
|
assert ev[2] == msg_out.id
|
||||||
|
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||||
|
assert msg_in.text == "message1"
|
||||||
|
|
||||||
|
def test_forward_messages(self, acfactory):
|
||||||
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
|
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||||
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
wait_successful_IMAP_SMTP_connection(ac1)
|
||||||
|
wait_configuration_progress(ac1, 1000)
|
||||||
|
wait_successful_IMAP_SMTP_connection(ac2)
|
||||||
|
wait_configuration_progress(ac2, 1000)
|
||||||
|
|
||||||
lp.sec("ac1: send message to ac2")
|
|
||||||
msg_out = chat.send_text("message2")
|
msg_out = chat.send_text("message2")
|
||||||
|
|
||||||
lp.sec("ac2: wait for receive")
|
# wait for other account to receive
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||||
assert ev[2] == msg_out.id
|
assert ev[2] == msg_out.id
|
||||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||||
assert msg_in.text == "message2"
|
assert msg_in.text == "message2"
|
||||||
|
|
||||||
lp.sec("ac2: check that the message arrive in deaddrop")
|
# check the message arrived in contact-requests/deaddrop
|
||||||
chat2 = msg_in.chat
|
chat2 = msg_in.chat
|
||||||
assert msg_in in chat2.get_messages()
|
assert msg_in in chat2.get_messages()
|
||||||
assert not msg_in.is_forwarded()
|
|
||||||
assert chat2.is_deaddrop()
|
assert chat2.is_deaddrop()
|
||||||
assert chat2 == ac2.get_deaddrop_chat()
|
assert chat2 == ac2.get_deaddrop_chat()
|
||||||
|
|
||||||
lp.sec("ac2: create new chat and forward message to it")
|
|
||||||
chat3 = ac2.create_group_chat("newgroup")
|
chat3 = ac2.create_group_chat("newgroup")
|
||||||
assert not chat3.is_promoted()
|
assert not chat3.is_promoted()
|
||||||
ac2.forward_messages([msg_in], chat3)
|
ac2.forward_messages([msg_in], chat3)
|
||||||
|
|
||||||
lp.sec("ac2: check new chat has a forwarded message")
|
|
||||||
assert chat3.is_promoted()
|
assert chat3.is_promoted()
|
||||||
messages = chat3.get_messages()
|
messages = chat3.get_messages()
|
||||||
msg = messages[-1]
|
|
||||||
assert msg.is_forwarded()
|
|
||||||
ac2.delete_messages(messages)
|
ac2.delete_messages(messages)
|
||||||
assert not chat3.get_messages()
|
assert not chat3.get_messages()
|
||||||
|
|
||||||
def test_forward_own_message(self, acfactory, lp):
|
def test_send_and_receive_message(self, acfactory, lp):
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
lp.sec("starting accounts, waiting for configuration")
|
||||||
chat = self.get_chat(ac1, ac2, both_created=True)
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
|
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||||
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
|
||||||
lp.sec("sending message")
|
wait_configuration_progress(ac1, 1000)
|
||||||
msg_out = chat.send_text("message2")
|
wait_configuration_progress(ac2, 1000)
|
||||||
|
|
||||||
lp.sec("receiving message")
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
msg_in = ac2.get_message_by_id(ev[2])
|
|
||||||
assert msg_in.text == "message2"
|
|
||||||
assert not msg_in.is_forwarded()
|
|
||||||
|
|
||||||
lp.sec("ac1: creating group chat, and forward own message")
|
|
||||||
group = ac1.create_group_chat("newgroup2")
|
|
||||||
group.add_contact(ac1.create_contact(ac2.get_config("addr")))
|
|
||||||
ac1.forward_messages([msg_out], group)
|
|
||||||
|
|
||||||
# wait for other account to receive
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
msg_in = ac2.get_message_by_id(ev[2])
|
|
||||||
assert msg_in.text == "message2"
|
|
||||||
assert msg_in.is_forwarded()
|
|
||||||
|
|
||||||
def test_send_self_message_and_empty_folder(self, acfactory, lp):
|
|
||||||
ac1 = acfactory.get_one_online_account()
|
|
||||||
lp.sec("ac1: create self chat")
|
|
||||||
chat = ac1.create_chat_by_contact(ac1.get_self_contact())
|
|
||||||
chat.send_text("hello")
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
|
||||||
ac1.empty_server_folders(inbox=True, mvbox=True)
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED")
|
|
||||||
assert ev[2] == "DeltaChat"
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED")
|
|
||||||
assert ev[2] == "INBOX"
|
|
||||||
|
|
||||||
def test_send_and_receive_message_markseen(self, acfactory, lp):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
|
|
||||||
lp.sec("ac1: create chat with ac2")
|
|
||||||
chat = self.get_chat(ac1, ac2)
|
|
||||||
|
|
||||||
lp.sec("sending text message from ac1 to ac2")
|
lp.sec("sending text message from ac1 to ac2")
|
||||||
msg_out = chat.send_text("message1")
|
msg_out = chat.send_text("message1")
|
||||||
@@ -634,7 +401,6 @@ class TestOnlineAccount:
|
|||||||
assert ev[2] == msg_out.id
|
assert ev[2] == msg_out.id
|
||||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||||
assert msg_in.text == "message1"
|
assert msg_in.text == "message1"
|
||||||
assert not msg_in.is_forwarded()
|
|
||||||
|
|
||||||
lp.sec("check the message arrived in contact-requets/deaddrop")
|
lp.sec("check the message arrived in contact-requets/deaddrop")
|
||||||
chat2 = msg_in.chat
|
chat2 = msg_in.chat
|
||||||
@@ -656,127 +422,20 @@ class TestOnlineAccount:
|
|||||||
ac2.mark_seen_messages([msg_in])
|
ac2.mark_seen_messages([msg_in])
|
||||||
lp.step("1")
|
lp.step("1")
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_READ")
|
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_READ")
|
||||||
assert ev[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
assert ev[1] >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
assert ev[2] > const.DC_MSG_ID_LAST_SPECIAL
|
assert ev[2] >= const.DC_MSG_ID_LAST_SPECIAL
|
||||||
lp.step("2")
|
lp.step("2")
|
||||||
assert msg_out.is_out_mdn_received()
|
assert msg_out.is_out_mdn_received()
|
||||||
|
|
||||||
lp.sec("check that a second call to mark_seen does not create change or smtp job")
|
|
||||||
ac2._evlogger.consume_events()
|
|
||||||
ac2.mark_seen_messages([msg_in])
|
|
||||||
try:
|
|
||||||
ac2._evlogger.get_matching("DC_EVENT_MSG_READ", timeout=0.01)
|
|
||||||
except queue.Empty:
|
|
||||||
pass # mark_seen_messages() has generated events before it returns
|
|
||||||
|
|
||||||
def test_send_and_receive_will_encrypt_decrypt(self, acfactory, lp):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
|
|
||||||
lp.sec("ac1: create chat with ac2")
|
|
||||||
chat = self.get_chat(ac1, ac2)
|
|
||||||
|
|
||||||
lp.sec("sending text message from ac1 to ac2")
|
|
||||||
msg_out = chat.send_text("message1")
|
|
||||||
assert not msg_out.is_encrypted()
|
|
||||||
|
|
||||||
lp.sec("wait for ac2 to receive message")
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
|
||||||
assert ev[2] == msg_out.id
|
|
||||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
|
||||||
assert msg_in.text == "message1"
|
|
||||||
|
|
||||||
lp.sec("create new chat with contact and send back (encrypted) message")
|
|
||||||
chat2b = ac2.create_chat_by_message(msg_in)
|
|
||||||
chat2b.send_text("message-back")
|
|
||||||
|
|
||||||
lp.sec("wait for ac1 to receive message")
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
assert ev[1] == chat.id
|
|
||||||
assert ev[2] > msg_out.id
|
|
||||||
msg_back = ac1.get_message_by_id(ev[2])
|
|
||||||
assert msg_back.text == "message-back"
|
|
||||||
assert msg_back.is_encrypted()
|
|
||||||
|
|
||||||
lp.sec("create group chat with two members, one of which has no encrypt state")
|
|
||||||
chat = ac1.create_group_chat("encryption test")
|
|
||||||
chat.add_contact(ac1.create_contact(ac2.get_config("addr")))
|
|
||||||
chat.add_contact(ac1.create_contact("notexisting@testrun.org"))
|
|
||||||
msg = chat.send_text("test not encrypt")
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
|
||||||
assert not msg.is_encrypted()
|
|
||||||
|
|
||||||
def test_send_first_message_as_long_unicode_with_cr(self, acfactory, lp):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
ac2.set_config("save_mime_headers", "1")
|
|
||||||
|
|
||||||
lp.sec("ac1: create chat with ac2")
|
|
||||||
chat = self.get_chat(ac1, ac2, both_created=True)
|
|
||||||
|
|
||||||
lp.sec("sending multi-line non-unicode message from ac1 to ac2")
|
|
||||||
text1 = "hello\nworld"
|
|
||||||
msg_out = chat.send_text(text1)
|
|
||||||
assert not msg_out.is_encrypted()
|
|
||||||
|
|
||||||
lp.sec("sending multi-line unicode text message from ac1 to ac2")
|
|
||||||
text2 = "äalis\nthis is ßßÄ"
|
|
||||||
msg_out = chat.send_text(text2)
|
|
||||||
assert not msg_out.is_encrypted()
|
|
||||||
|
|
||||||
lp.sec("wait for ac2 to receive multi-line non-unicode message")
|
|
||||||
msg_in = ac2.wait_next_incoming_message()
|
|
||||||
assert msg_in.text == text1
|
|
||||||
|
|
||||||
lp.sec("wait for ac2 to receive multi-line unicode message")
|
|
||||||
msg_in = ac2.wait_next_incoming_message()
|
|
||||||
assert msg_in.text == text2
|
|
||||||
assert ac1.get_config("addr") in msg_in.chat.get_name()
|
|
||||||
|
|
||||||
def test_reply_encrypted(self, acfactory, lp):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
|
|
||||||
lp.sec("ac1: create chat with ac2")
|
|
||||||
chat = self.get_chat(ac1, ac2)
|
|
||||||
|
|
||||||
lp.sec("sending text message from ac1 to ac2")
|
|
||||||
msg_out = chat.send_text("message1")
|
|
||||||
assert not msg_out.is_encrypted()
|
|
||||||
|
|
||||||
lp.sec("wait for ac2 to receive message")
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
|
||||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
|
||||||
assert msg_in.text == "message1"
|
|
||||||
assert not msg_in.is_encrypted()
|
|
||||||
|
|
||||||
lp.sec("create new chat with contact and send back (encrypted) message")
|
|
||||||
chat2b = ac2.create_chat_by_message(msg_in)
|
|
||||||
chat2b.send_text("message-back")
|
|
||||||
|
|
||||||
lp.sec("wait for ac1 to receive message")
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
assert ev[1] == chat.id
|
|
||||||
msg_back = ac1.get_message_by_id(ev[2])
|
|
||||||
assert msg_back.text == "message-back"
|
|
||||||
assert msg_back.is_encrypted()
|
|
||||||
|
|
||||||
lp.sec("ac1: e2ee_enabled=0 and see if reply is encrypted")
|
|
||||||
print("ac1: e2ee_enabled={}".format(ac1.get_config("e2ee_enabled")))
|
|
||||||
print("ac2: e2ee_enabled={}".format(ac2.get_config("e2ee_enabled")))
|
|
||||||
ac1.set_config("e2ee_enabled", "0")
|
|
||||||
chat.send_text("message2 -- should be encrypted")
|
|
||||||
|
|
||||||
lp.sec("wait for ac2 to receive message")
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
msg_in = ac2.get_message_by_id(ev[2])
|
|
||||||
assert msg_in.text == "message2 -- should be encrypted"
|
|
||||||
assert msg_in.is_encrypted()
|
|
||||||
|
|
||||||
def test_saved_mime_on_received_message(self, acfactory, lp):
|
def test_saved_mime_on_received_message(self, acfactory, lp):
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
lp.sec("starting accounts, waiting for configuration")
|
||||||
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
lp.sec("configure ac2 to save mime headers, create ac1/ac2 chat")
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
ac2.set_config("save_mime_headers", "1")
|
ac2.set_config("save_mime_headers", "1")
|
||||||
chat = self.get_chat(ac1, ac2)
|
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||||
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
wait_configuration_progress(ac1, 1000)
|
||||||
|
wait_configuration_progress(ac2, 1000)
|
||||||
lp.sec("sending text message from ac1 to ac2")
|
lp.sec("sending text message from ac1 to ac2")
|
||||||
msg_out = chat.send_text("message1")
|
msg_out = chat.send_text("message1")
|
||||||
ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||||
@@ -790,8 +449,14 @@ class TestOnlineAccount:
|
|||||||
assert mime.get_all("Received")
|
assert mime.get_all("Received")
|
||||||
|
|
||||||
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()
|
lp.sec("starting accounts, waiting for configuration")
|
||||||
chat = self.get_chat(ac1, ac2)
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
|
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||||
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
|
||||||
|
wait_configuration_progress(ac1, 1000)
|
||||||
|
wait_configuration_progress(ac2, 1000)
|
||||||
|
|
||||||
lp.sec("sending image message from ac1 to ac2")
|
lp.sec("sending image message from ac1 to ac2")
|
||||||
path = data.get_path("d.png")
|
path = data.get_path("d.png")
|
||||||
@@ -810,30 +475,19 @@ class TestOnlineAccount:
|
|||||||
assert os.path.exists(msg_in.filename)
|
assert os.path.exists(msg_in.filename)
|
||||||
assert os.stat(msg_in.filename).st_size == os.stat(path).st_size
|
assert os.stat(msg_in.filename).st_size == os.stat(path).st_size
|
||||||
|
|
||||||
def test_import_export_online_all(self, acfactory, tmpdir, lp):
|
def test_import_export_online(self, acfactory, tmpdir):
|
||||||
|
backupdir = tmpdir.mkdir("backup")
|
||||||
ac1 = acfactory.get_online_configuring_account()
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
wait_configuration_progress(ac1, 1000)
|
wait_configuration_progress(ac1, 1000)
|
||||||
|
|
||||||
lp.sec("create some chat content")
|
|
||||||
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||||
chat = ac1.create_chat_by_contact(contact1)
|
chat = ac1.create_chat_by_contact(contact1)
|
||||||
chat.send_text("msg1")
|
chat.send_text("msg1")
|
||||||
backupdir = tmpdir.mkdir("backup")
|
path = ac1.export_to_dir(backupdir.strpath)
|
||||||
|
|
||||||
lp.sec("export all to {}".format(backupdir))
|
|
||||||
path = ac1.export_all(backupdir.strpath)
|
|
||||||
assert os.path.exists(path)
|
assert os.path.exists(path)
|
||||||
t = time.time()
|
|
||||||
|
|
||||||
lp.sec("get fresh empty account")
|
|
||||||
ac2 = acfactory.get_unconfigured_account()
|
ac2 = acfactory.get_unconfigured_account()
|
||||||
|
ac2.import_from_file(path)
|
||||||
lp.sec("get latest backup file")
|
|
||||||
path2 = ac2.get_latest_backupfile(backupdir.strpath)
|
|
||||||
assert path2 == path
|
|
||||||
|
|
||||||
lp.sec("import backup and check it's proper")
|
|
||||||
ac2.import_all(path)
|
|
||||||
contacts = ac2.get_contacts(query="some1")
|
contacts = ac2.get_contacts(query="some1")
|
||||||
assert len(contacts) == 1
|
assert len(contacts) == 1
|
||||||
contact2 = contacts[0]
|
contact2 = contacts[0]
|
||||||
@@ -843,18 +497,7 @@ class TestOnlineAccount:
|
|||||||
assert len(messages) == 1
|
assert len(messages) == 1
|
||||||
assert messages[0].text == "msg1"
|
assert messages[0].text == "msg1"
|
||||||
|
|
||||||
# wait until a second passed since last backup
|
def test_ac_setup_message(self, acfactory):
|
||||||
# because get_latest_backupfile() shall return the latest backup
|
|
||||||
# from a UI it's unlikely anyone manages to export two
|
|
||||||
# backups in one second.
|
|
||||||
time.sleep(max(0, 1 - (time.time() - t)))
|
|
||||||
lp.sec("Second-time export all to {}".format(backupdir))
|
|
||||||
path2 = ac1.export_all(backupdir.strpath)
|
|
||||||
assert os.path.exists(path2)
|
|
||||||
assert path2 != path
|
|
||||||
assert ac2.get_latest_backupfile(backupdir.strpath) == path2
|
|
||||||
|
|
||||||
def test_ac_setup_message(self, acfactory, lp):
|
|
||||||
# note that the receiving account needs to be configured and running
|
# note that the receiving account needs to be configured and running
|
||||||
# before ther setup message is send. DC does not read old messages
|
# before ther setup message is send. DC does not read old messages
|
||||||
# as of Jul2019
|
# as of Jul2019
|
||||||
@@ -862,228 +505,13 @@ class TestOnlineAccount:
|
|||||||
ac2 = acfactory.clone_online_account(ac1)
|
ac2 = acfactory.clone_online_account(ac1)
|
||||||
wait_configuration_progress(ac2, 1000)
|
wait_configuration_progress(ac2, 1000)
|
||||||
wait_configuration_progress(ac1, 1000)
|
wait_configuration_progress(ac1, 1000)
|
||||||
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()
|
||||||
ac2._evlogger.set_timeout(30)
|
ac2._evlogger.set_timeout(30)
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||||
msg = ac2.get_message_by_id(ev[2])
|
msg = ac2.get_message_by_id(ev[2])
|
||||||
assert msg.is_setup_message()
|
assert msg.is_setup_message()
|
||||||
assert msg.get_setupcodebegin() == setup_code[:2]
|
|
||||||
lp.sec("try a bad setup code")
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
msg.continue_key_transfer(str(reversed(setup_code)))
|
|
||||||
lp.sec("try a good setup code")
|
|
||||||
print("*************** Incoming ASM File at: ", msg.filename)
|
print("*************** Incoming ASM File at: ", msg.filename)
|
||||||
print("*************** Setup Code: ", setup_code)
|
print("*************** Setup Code: ", setup_code)
|
||||||
msg.continue_key_transfer(setup_code)
|
msg.continue_key_transfer(setup_code)
|
||||||
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
||||||
|
|
||||||
def test_ac_setup_message_twice(self, acfactory, lp):
|
|
||||||
ac1 = acfactory.get_online_configuring_account()
|
|
||||||
ac2 = acfactory.clone_online_account(ac1)
|
|
||||||
ac2._evlogger.set_timeout(30)
|
|
||||||
wait_configuration_progress(ac2, 1000)
|
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
|
|
||||||
lp.sec("trigger ac setup message but ignore")
|
|
||||||
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
|
|
||||||
ac1.initiate_key_transfer()
|
|
||||||
ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
|
||||||
|
|
||||||
lp.sec("trigger second ac setup message, wait for receive ")
|
|
||||||
setup_code2 = ac1.initiate_key_transfer()
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
|
||||||
msg = ac2.get_message_by_id(ev[2])
|
|
||||||
assert msg.is_setup_message()
|
|
||||||
assert msg.get_setupcodebegin() == setup_code2[:2]
|
|
||||||
|
|
||||||
lp.sec("process second setup message")
|
|
||||||
msg.continue_key_transfer(setup_code2)
|
|
||||||
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
|
||||||
|
|
||||||
def test_qr_setup_contact(self, acfactory, lp):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
|
|
||||||
qr = ac1.get_setup_contact_qr()
|
|
||||||
lp.sec("ac2: start QR-code based setup contact protocol")
|
|
||||||
ch = ac2.qr_setup_contact(qr)
|
|
||||||
assert ch.id >= 10
|
|
||||||
wait_securejoin_inviter_progress(ac1, 1000)
|
|
||||||
|
|
||||||
def test_qr_join_chat(self, acfactory, lp):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
|
|
||||||
chat = ac1.create_group_chat("hello")
|
|
||||||
qr = chat.get_join_qr()
|
|
||||||
lp.sec("ac2: start QR-code based join-group protocol")
|
|
||||||
ch = ac2.qr_join_chat(qr)
|
|
||||||
assert ch.id >= 10
|
|
||||||
# check that at least some of the handshake messages are deleted
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
|
||||||
ac2._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
|
||||||
wait_securejoin_inviter_progress(ac1, 1000)
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_SECUREJOIN_MEMBER_ADDED")
|
|
||||||
|
|
||||||
def test_qr_verified_group_and_chatting(self, acfactory, lp):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
|
||||||
chat1 = ac1.create_group_chat("hello", verified=True)
|
|
||||||
assert chat1.is_verified()
|
|
||||||
qr = chat1.get_join_qr()
|
|
||||||
lp.sec("ac2: start QR-code based join-group protocol")
|
|
||||||
chat2 = ac2.qr_join_chat(qr)
|
|
||||||
assert chat2.id >= 10
|
|
||||||
wait_securejoin_inviter_progress(ac1, 1000)
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_SECUREJOIN_MEMBER_ADDED")
|
|
||||||
|
|
||||||
lp.sec("ac2: read member added message")
|
|
||||||
msg = ac2.wait_next_incoming_message()
|
|
||||||
assert msg.is_encrypted()
|
|
||||||
assert "added" in msg.text.lower()
|
|
||||||
|
|
||||||
lp.sec("ac1: send message")
|
|
||||||
msg_out = chat1.send_text("hello")
|
|
||||||
assert msg_out.is_encrypted()
|
|
||||||
|
|
||||||
lp.sec("ac2: read message and check it's verified chat")
|
|
||||||
msg = ac2.wait_next_incoming_message()
|
|
||||||
assert msg.text == "hello"
|
|
||||||
assert msg.chat.is_verified()
|
|
||||||
assert msg.is_encrypted()
|
|
||||||
|
|
||||||
lp.sec("ac2: send message and let ac1 read it")
|
|
||||||
chat2.send_text("world")
|
|
||||||
msg = ac1.wait_next_incoming_message()
|
|
||||||
assert msg.text == "world"
|
|
||||||
assert msg.is_encrypted()
|
|
||||||
|
|
||||||
def test_set_get_profile_image(self, acfactory, data, lp):
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
|
|
||||||
lp.sec("create unpromoted group chat")
|
|
||||||
chat = ac1.create_group_chat("hello")
|
|
||||||
p = data.get_path("d.png")
|
|
||||||
|
|
||||||
lp.sec("ac1: set profile image on unpromoted chat")
|
|
||||||
chat.set_profile_image(p)
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_CHAT_MODIFIED")
|
|
||||||
assert not chat.is_promoted()
|
|
||||||
|
|
||||||
lp.sec("ac1: send text to promote chat (XXX without contact added)")
|
|
||||||
# XXX first promote the chat before adding contact
|
|
||||||
# because DC does not send out profile images for unpromoted chats
|
|
||||||
# otherwise
|
|
||||||
chat.send_text("ac1: initial message to promote chat (workaround)")
|
|
||||||
assert chat.is_promoted()
|
|
||||||
|
|
||||||
lp.sec("ac2: add ac1 to a chat so the message does not land in DEADDROP")
|
|
||||||
c1 = ac2.create_contact(email=ac1.get_config("addr"))
|
|
||||||
ac2.create_chat_by_contact(c1)
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
|
||||||
|
|
||||||
lp.sec("ac1: add ac2 to promoted group chat")
|
|
||||||
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
|
||||||
chat.add_contact(c2)
|
|
||||||
|
|
||||||
lp.sec("ac1: send a first message to ac2")
|
|
||||||
chat.send_text("hi")
|
|
||||||
assert chat.is_promoted()
|
|
||||||
|
|
||||||
lp.sec("ac2: wait for receiving message from ac1")
|
|
||||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
msg_in = ac2.get_message_by_id(ev[2])
|
|
||||||
assert not msg_in.chat.is_deaddrop()
|
|
||||||
|
|
||||||
lp.sec("ac2: create chat and read profile image")
|
|
||||||
chat2 = ac2.create_chat_by_message(msg_in)
|
|
||||||
p2 = chat2.get_profile_image()
|
|
||||||
assert p2 is not None
|
|
||||||
assert open(p2, "rb").read() == open(p, "rb").read()
|
|
||||||
|
|
||||||
ac2._evlogger.consume_events()
|
|
||||||
ac1._evlogger.consume_events()
|
|
||||||
lp.sec("ac2: delete profile image from chat")
|
|
||||||
chat2.remove_profile_image()
|
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
assert ev[1] == chat.id
|
|
||||||
chat1b = ac1.create_chat_by_message(ev[2])
|
|
||||||
assert chat1b.get_profile_image() is None
|
|
||||||
assert chat.get_profile_image() is None
|
|
||||||
|
|
||||||
def test_send_receive_locations(self, acfactory, lp):
|
|
||||||
now = datetime.utcnow()
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
|
||||||
|
|
||||||
lp.sec("ac1: create chat with ac2")
|
|
||||||
chat1 = self.get_chat(ac1, ac2)
|
|
||||||
chat2 = self.get_chat(ac2, ac1)
|
|
||||||
|
|
||||||
assert not chat1.is_sending_locations()
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
ac1.set_location(latitude=0.0, longitude=10.0)
|
|
||||||
|
|
||||||
ac1._evlogger.consume_events()
|
|
||||||
ac2._evlogger.consume_events()
|
|
||||||
|
|
||||||
lp.sec("ac1: enable location sending in chat")
|
|
||||||
chat1.enable_sending_locations(seconds=100)
|
|
||||||
assert chat1.is_sending_locations()
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
|
||||||
|
|
||||||
ac1.set_location(latitude=2.0, longitude=3.0, accuracy=0.5)
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_LOCATION_CHANGED")
|
|
||||||
chat1.send_text("hello")
|
|
||||||
ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
|
||||||
|
|
||||||
lp.sec("ac2: wait for incoming location message")
|
|
||||||
ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG") # "enabled-location streaming"
|
|
||||||
|
|
||||||
# currently core emits location changed before event_incoming message
|
|
||||||
ac2._evlogger.get_matching("DC_EVENT_LOCATION_CHANGED")
|
|
||||||
ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG") # text message with location
|
|
||||||
|
|
||||||
locations = chat2.get_locations()
|
|
||||||
assert len(locations) == 1
|
|
||||||
assert locations[0].latitude == 2.0
|
|
||||||
assert locations[0].longitude == 3.0
|
|
||||||
assert locations[0].accuracy == 0.5
|
|
||||||
assert locations[0].timestamp > now
|
|
||||||
|
|
||||||
contact = ac2.create_contact(ac1.get_config("addr"))
|
|
||||||
locations2 = chat2.get_locations(contact=contact)
|
|
||||||
assert len(locations2) == 1
|
|
||||||
assert locations2 == locations
|
|
||||||
|
|
||||||
contact = ac2.create_contact("nonexisting@example.org")
|
|
||||||
locations3 = chat2.get_locations(contact=contact)
|
|
||||||
assert not locations3
|
|
||||||
|
|
||||||
|
|
||||||
class TestOnlineConfigureFails:
|
|
||||||
def test_invalid_password(self, acfactory):
|
|
||||||
ac1, configdict = acfactory.get_online_config()
|
|
||||||
ac1.configure(addr=configdict["addr"], mail_pw="123")
|
|
||||||
ac1.start_threads()
|
|
||||||
wait_configuration_progress(ac1, 500)
|
|
||||||
ev1 = ac1._evlogger.get_matching("DC_EVENT_ERROR_NETWORK")
|
|
||||||
assert "cannot login" in ev1[2].lower()
|
|
||||||
wait_configuration_progress(ac1, 0, 0)
|
|
||||||
|
|
||||||
def test_invalid_user(self, acfactory):
|
|
||||||
ac1, configdict = acfactory.get_online_config()
|
|
||||||
ac1.configure(addr="x" + configdict["addr"], mail_pw=configdict["mail_pw"])
|
|
||||||
ac1.start_threads()
|
|
||||||
wait_configuration_progress(ac1, 500)
|
|
||||||
ev1 = ac1._evlogger.get_matching("DC_EVENT_ERROR_NETWORK")
|
|
||||||
assert "cannot login" in ev1[2].lower()
|
|
||||||
wait_configuration_progress(ac1, 0, 0)
|
|
||||||
|
|
||||||
def test_invalid_domain(self, acfactory):
|
|
||||||
ac1, configdict = acfactory.get_online_config()
|
|
||||||
ac1.configure(addr=configdict["addr"] + "x", mail_pw=configdict["mail_pw"])
|
|
||||||
ac1.start_threads()
|
|
||||||
wait_configuration_progress(ac1, 500)
|
|
||||||
ev1 = ac1._evlogger.get_matching("DC_EVENT_ERROR_NETWORK")
|
|
||||||
assert "could not connect" in ev1[2].lower()
|
|
||||||
wait_configuration_progress(ac1, 0, 0)
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from deltachat import const
|
|||||||
from conftest import wait_configuration_progress, wait_msgs_changed
|
from conftest import wait_configuration_progress, wait_msgs_changed
|
||||||
|
|
||||||
|
|
||||||
class TestOnlineInCreation:
|
class TestInCreation:
|
||||||
def test_forward_increation(self, acfactory, data, lp):
|
def test_forward_increation(self, acfactory, data, lp):
|
||||||
ac1 = acfactory.get_online_configuring_account()
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
ac2 = acfactory.get_online_configuring_account()
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
@@ -57,13 +57,13 @@ class TestOnlineInCreation:
|
|||||||
|
|
||||||
lp.sec("wait1 for original or forwarded messages to arrive")
|
lp.sec("wait1 for original or forwarded messages to arrive")
|
||||||
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
assert ev1[1] >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
received_original = ac2.get_message_by_id(ev1[2])
|
received_original = ac2.get_message_by_id(ev1[2])
|
||||||
assert cmp(received_original.filename, path, False)
|
assert cmp(received_original.filename, path, False)
|
||||||
|
|
||||||
lp.sec("wait2 for original or forwarded messages to arrive")
|
lp.sec("wait2 for original or forwarded messages to arrive")
|
||||||
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
assert ev2[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
assert ev2[1] >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
assert ev2[1] != ev1[1]
|
assert ev2[1] != ev1[1]
|
||||||
received_copy = ac2.get_message_by_id(ev2[2])
|
received_copy = ac2.get_message_by_id(ev2[2])
|
||||||
assert cmp(received_copy.filename, path, False)
|
assert cmp(received_copy.filename, path, False)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
|
from deltachat import capi, const, set_context_callback, clear_context_callback
|
||||||
from deltachat.capi import ffi
|
from deltachat.capi import ffi
|
||||||
from deltachat.capi import lib
|
from deltachat.capi import lib
|
||||||
from deltachat.account import EventLogger
|
from deltachat.account import EventLogger
|
||||||
@@ -17,19 +17,11 @@ def test_callback_None2int():
|
|||||||
clear_context_callback(ctx)
|
clear_context_callback(ctx)
|
||||||
|
|
||||||
|
|
||||||
def test_dc_close_events(tmpdir):
|
def test_dc_close_events():
|
||||||
ctx = ffi.gc(
|
ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL)
|
||||||
capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
|
||||||
lib.dc_context_unref,
|
|
||||||
)
|
|
||||||
evlog = EventLogger(ctx)
|
evlog = EventLogger(ctx)
|
||||||
evlog.set_timeout(5)
|
evlog.set_timeout(5)
|
||||||
set_context_callback(
|
set_context_callback(ctx, lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2))
|
||||||
ctx,
|
|
||||||
lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)
|
|
||||||
)
|
|
||||||
p = tmpdir.join("hello.db")
|
|
||||||
lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL)
|
|
||||||
capi.lib.dc_close(ctx)
|
capi.lib.dc_close(ctx)
|
||||||
|
|
||||||
def find(info_string):
|
def find(info_string):
|
||||||
@@ -41,7 +33,7 @@ def test_dc_close_events(tmpdir):
|
|||||||
else:
|
else:
|
||||||
print("skipping event", *ev)
|
print("skipping event", *ev)
|
||||||
|
|
||||||
find("disconnecting inbox-thread")
|
find("disconnecting INBOX-watch")
|
||||||
find("disconnecting sentbox-thread")
|
find("disconnecting sentbox-thread")
|
||||||
find("disconnecting mvbox-thread")
|
find("disconnecting mvbox-thread")
|
||||||
find("disconnecting SMTP")
|
find("disconnecting SMTP")
|
||||||
@@ -59,16 +51,6 @@ def test_wrong_db(tmpdir):
|
|||||||
assert not lib.dc_open(dc_context, p.strpath.encode("ascii"), ffi.NULL)
|
assert not lib.dc_open(dc_context, p.strpath.encode("ascii"), ffi.NULL)
|
||||||
|
|
||||||
|
|
||||||
def test_empty_blobdir(tmpdir):
|
|
||||||
# Apparently some client code expects this to be the same as passing NULL.
|
|
||||||
ctx = ffi.gc(
|
|
||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
|
||||||
lib.dc_context_unref,
|
|
||||||
)
|
|
||||||
db_fname = tmpdir.join("hello.db")
|
|
||||||
assert lib.dc_open(ctx, db_fname.strpath.encode("ascii"), b"")
|
|
||||||
|
|
||||||
|
|
||||||
def test_event_defines():
|
def test_event_defines():
|
||||||
assert const.DC_EVENT_INFO == 100
|
assert const.DC_EVENT_INFO == 100
|
||||||
assert const.DC_CONTACT_ID_SELF
|
assert const.DC_CONTACT_ID_SELF
|
||||||
@@ -93,65 +75,3 @@ def test_markseen_invalid_message_ids(acfactory):
|
|||||||
msg_ids = [9]
|
msg_ids = [9]
|
||||||
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
|
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
|
||||||
ac1._evlogger.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
ac1._evlogger.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
||||||
|
|
||||||
|
|
||||||
def test_get_special_message_id_returns_empty_message(acfactory):
|
|
||||||
ac1 = acfactory.get_configured_offline_account()
|
|
||||||
for i in range(1, 10):
|
|
||||||
msg = ac1.get_message_by_id(i)
|
|
||||||
assert msg.id == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info():
|
|
||||||
provider = lib.dc_provider_new_from_email(cutil.as_dc_charpointer("ex@example.com"))
|
|
||||||
assert cutil.from_dc_charpointer(
|
|
||||||
lib.dc_provider_get_overview_page(provider)
|
|
||||||
) == "https://providers.delta.chat/example.com"
|
|
||||||
assert cutil.from_dc_charpointer(lib.dc_provider_get_name(provider)) == "Example"
|
|
||||||
assert cutil.from_dc_charpointer(lib.dc_provider_get_markdown(provider)) == "\n..."
|
|
||||||
assert cutil.from_dc_charpointer(lib.dc_provider_get_status_date(provider)) == "2018-09"
|
|
||||||
assert lib.dc_provider_get_status(provider) == const.DC_PROVIDER_STATUS_PREPARATION
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_none():
|
|
||||||
assert lib.dc_provider_new_from_email(cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_info_closed():
|
|
||||||
ctx = ffi.gc(
|
|
||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
|
||||||
lib.dc_context_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):
|
|
||||||
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)
|
|
||||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
|
||||||
assert 'deltachat_core_version' 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
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from deltachat import const
|
|
||||||
from deltachat import provider
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_from_email():
|
|
||||||
example = provider.Provider.from_email("email@example.com")
|
|
||||||
assert example.overview_page == "https://providers.delta.chat/example.com"
|
|
||||||
assert example.name == "Example"
|
|
||||||
assert example.markdown == "\n..."
|
|
||||||
assert example.status_date == "2018-09"
|
|
||||||
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_from_domain():
|
|
||||||
example = provider.Provider("example.com")
|
|
||||||
assert example.overview_page == "https://providers.delta.chat/example.com"
|
|
||||||
assert example.name == "Example"
|
|
||||||
assert example.markdown == "\n..."
|
|
||||||
assert example.status_date == "2018-09"
|
|
||||||
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_none():
|
|
||||||
with pytest.raises(provider.ProviderNotFoundError):
|
|
||||||
provider.Provider.from_email("email@unexistent.no")
|
|
||||||
@@ -1,38 +1,34 @@
|
|||||||
[tox]
|
[tox]
|
||||||
# make sure to update environment list in travis.yml and appveyor.yml
|
# make sure to update environment list in travis.yml and appveyor.yml
|
||||||
envlist =
|
envlist =
|
||||||
py37
|
py27
|
||||||
|
py35
|
||||||
lint
|
lint
|
||||||
auditwheels
|
auditwheels
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands =
|
commands =
|
||||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
|
pytest -v -rsXx {posargs:tests}
|
||||||
# python tests/package_wheels.py {toxworkdir}/wheelhouse
|
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||||
passenv =
|
passenv =
|
||||||
TRAVIS
|
TRAVIS
|
||||||
DCC_RS_DEV
|
DCC_RS_DEV
|
||||||
DCC_RS_TARGET
|
DCC_RS_TARGET
|
||||||
DCC_PY_LIVECONFIG
|
DCC_PY_LIVECONFIG
|
||||||
CARGO_TARGET_DIR
|
|
||||||
RUSTC_WRAPPER
|
|
||||||
deps =
|
deps =
|
||||||
pytest
|
pytest
|
||||||
pytest-rerunfailures
|
pytest-faulthandler
|
||||||
pytest-timeout
|
|
||||||
pytest-xdist
|
|
||||||
pdbpp
|
pdbpp
|
||||||
requests
|
|
||||||
|
|
||||||
[testenv:auditwheels]
|
[testenv:auditwheels]
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
deps = auditwheel
|
|
||||||
commands =
|
commands =
|
||||||
python tests/auditwheels.py {toxworkdir}/wheelhouse
|
python tests/auditwheels.py {toxworkdir}/wheelhouse
|
||||||
|
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
skip_install = True
|
usedevelop = True
|
||||||
deps =
|
deps =
|
||||||
flake8
|
flake8
|
||||||
# pygments required by rst-lint
|
# pygments required by rst-lint
|
||||||
@@ -44,33 +40,21 @@ commands =
|
|||||||
rst-lint --encoding 'utf-8' README.rst
|
rst-lint --encoding 'utf-8' README.rst
|
||||||
|
|
||||||
[testenv:doc]
|
[testenv:doc]
|
||||||
changedir=doc
|
basepython = python3.5
|
||||||
deps =
|
deps =
|
||||||
sphinx==2.2.0
|
sphinx==2.0.1
|
||||||
breathe
|
breathe
|
||||||
|
|
||||||
|
changedir = doc
|
||||||
commands =
|
commands =
|
||||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
sphinx-build -w docker-toxdoc-warnings.log -b html . _build/html
|
||||||
|
|
||||||
|
|
||||||
[testenv:lintdoc]
|
|
||||||
skipsdist = True
|
|
||||||
usedevelop = True
|
|
||||||
deps =
|
|
||||||
{[testenv:lint]deps}
|
|
||||||
{[testenv:doc]deps}
|
|
||||||
commands =
|
|
||||||
{[testenv:lint]commands}
|
|
||||||
{[testenv:doc]commands}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
addopts = -v -rs
|
|
||||||
python_files = tests/test_*.py
|
python_files = tests/test_*.py
|
||||||
norecursedirs = .tox
|
norecursedirs = .tox
|
||||||
xfail_strict=true
|
xfail_strict=true
|
||||||
timeout = 60
|
timeout = 60
|
||||||
timeout_method = thread
|
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ if [ $? != 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
pushd python
|
pushd python
|
||||||
if [ -e "./liveconfig" -a -z "$DCC_PY_LIVECONFIG" ]; then
|
if [ -e "./liveconfig" ]; then
|
||||||
export DCC_PY_LIVECONFIG=liveconfig
|
export DCC_PY_LIVECONFIG=liveconfig
|
||||||
fi
|
fi
|
||||||
tox "$@"
|
tox "$@"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
nightly-2019-11-06
|
nightly-2019-07-10
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
import pathlib
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
rex = re.compile(r'version = "(\S+)"')
|
|
||||||
|
|
||||||
def read_toml_version(relpath):
|
|
||||||
p = pathlib.Path(relpath)
|
|
||||||
assert p.exists()
|
|
||||||
for line in open(str(p)):
|
|
||||||
m = rex.match(line)
|
|
||||||
if m is not None:
|
|
||||||
return m.group(1)
|
|
||||||
raise ValueError("no version found in {}".format(relpath))
|
|
||||||
|
|
||||||
def replace_toml_version(relpath, newversion):
|
|
||||||
p = pathlib.Path(relpath)
|
|
||||||
assert p.exists()
|
|
||||||
tmp_path = str(p) + "_tmp"
|
|
||||||
with open(tmp_path, "w") as f:
|
|
||||||
for line in open(str(p)):
|
|
||||||
m = rex.match(line)
|
|
||||||
if m is not None:
|
|
||||||
f.write('version = "{}"\n'.format(newversion))
|
|
||||||
else:
|
|
||||||
f.write(line)
|
|
||||||
os.rename(tmp_path, str(p))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
|
|
||||||
print("{}: {}".format(x, read_toml_version(x)))
|
|
||||||
raise SystemExit("need argument: new version, example 1.0.0-beta.27")
|
|
||||||
newversion = sys.argv[1]
|
|
||||||
if newversion.count(".") < 2:
|
|
||||||
raise SystemExit("need at least two dots in version")
|
|
||||||
|
|
||||||
core_toml = read_toml_version("Cargo.toml")
|
|
||||||
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
|
|
||||||
assert core_toml == ffi_toml, (core_toml, ffi_toml)
|
|
||||||
|
|
||||||
for line in open("CHANGELOG.md"):
|
|
||||||
## 1.0.0-beta5
|
|
||||||
if line.startswith("## "):
|
|
||||||
if line[2:].strip().startswith(newversion):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
|
|
||||||
|
|
||||||
replace_toml_version("Cargo.toml", newversion)
|
|
||||||
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
|
||||||
|
|
||||||
subprocess.call(["cargo", "update", "-p", "deltachat"])
|
|
||||||
|
|
||||||
print("after commit make sure to: ")
|
|
||||||
print("")
|
|
||||||
print(" git tag {}".format(newversion))
|
|
||||||
print("")
|
|
||||||
371
spec.md
371
spec.md
@@ -1,371 +0,0 @@
|
|||||||
# Chat-over-Email specification
|
|
||||||
|
|
||||||
Version 0.19.0
|
|
||||||
|
|
||||||
This document describes how emails can be used
|
|
||||||
to implement typical messenger functions
|
|
||||||
while staying compatible to existing MUAs.
|
|
||||||
|
|
||||||
- [Encryption](#encryption)
|
|
||||||
- [Outgoing messages](#outgoing-messages)
|
|
||||||
- [Incoming messages](#incoming-messages)
|
|
||||||
- [Forwarded messages](#forwarded-messages)
|
|
||||||
- [Groups](#groups)
|
|
||||||
- [Outgoing group messages](#outgoing-group-messages)
|
|
||||||
- [Incoming group messages](#incoming-group-messages)
|
|
||||||
- [Add and remove members](#add-and-remove-members)
|
|
||||||
- [Change group name](#change-group-name)
|
|
||||||
- [Set group image](#set-group-image)
|
|
||||||
- [Set profile image](#set-profile-image)
|
|
||||||
- [Miscellaneous](#miscellaneous)
|
|
||||||
|
|
||||||
|
|
||||||
# Encryption
|
|
||||||
|
|
||||||
Messages SHOULD be encrypted by the
|
|
||||||
[Autocrypt](https://autocrypt.org/level1.html) standard;
|
|
||||||
`prefer-encrypt=mutual` MAY be set by default.
|
|
||||||
|
|
||||||
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
|
|
||||||
by the [Memoryhole](https://github.com/autocrypt/memoryhole) standard.
|
|
||||||
If Memoryhole is not used,
|
|
||||||
the subject of encrypted messages SHOULD be replaced by the string
|
|
||||||
`Chat: Encrypted message` where the part after the colon MAY be localized.
|
|
||||||
|
|
||||||
|
|
||||||
# Outgoing messages
|
|
||||||
|
|
||||||
Messengers MUST add a `Chat-Version: 1.0` header to outgoing messages.
|
|
||||||
For filtering and smart appearance of the messages in normal MUAs,
|
|
||||||
the `Subject` header SHOULD start with the characters `Chat:`
|
|
||||||
and SHOULD be an excerpt of the message.
|
|
||||||
Replies to messages MAY follow the typical `Re:`-format.
|
|
||||||
|
|
||||||
The body MAY contain text which MUST have the content type `text/plain`
|
|
||||||
or `mulipart/alternative` containing `text/plain`.
|
|
||||||
|
|
||||||
The text MAY be divided into a user-text-part and a footer-part using the
|
|
||||||
line `-- ` (minus, minus, space, lineend).
|
|
||||||
|
|
||||||
The user-text-part MUST contain only user generated content.
|
|
||||||
User generated content are eg. texts a user has actually typed
|
|
||||||
or pasted or forwarded from another user.
|
|
||||||
Full quotes, footers or sth. like that MUST NOT go to the user-text-part.
|
|
||||||
|
|
||||||
From: sender@domain
|
|
||||||
To: rcpt@domain
|
|
||||||
Chat-Version: 1.0
|
|
||||||
Content-Type: text/plain
|
|
||||||
Subject: Chat: Hello ...
|
|
||||||
|
|
||||||
Hello world!
|
|
||||||
|
|
||||||
|
|
||||||
# Incoming messages
|
|
||||||
|
|
||||||
The `Chat-Version` header MAY be used
|
|
||||||
to detect if a messages comes from a compatible messenger.
|
|
||||||
|
|
||||||
The `Subject` header MUST NOT be used
|
|
||||||
to detect compatible messengers, groups or whatever.
|
|
||||||
|
|
||||||
Messenger SHOULD show the `Subject`
|
|
||||||
if the message comes from a normal MUA together with the email-body.
|
|
||||||
The email-body SHOULD be converted
|
|
||||||
to plain text, full-quotes and similar regions SHOULD be cut.
|
|
||||||
|
|
||||||
Attachments SHOULD be shown where possible.
|
|
||||||
If an attachment cannot be shown, a non-distracting warning SHOULD be printed.
|
|
||||||
|
|
||||||
|
|
||||||
# Forwarded messages
|
|
||||||
|
|
||||||
Forwarded messages are outgoing messages that contain a forwarded-header
|
|
||||||
before the user generated content.
|
|
||||||
|
|
||||||
The forwarded header MUST contain two lines:
|
|
||||||
The first line contains the text
|
|
||||||
`---------- Forwarded message ----------`
|
|
||||||
(10 minus, space, text `Forwarded message`, space, 10 minus).
|
|
||||||
The second line starts with `From: ` followed by the original sender
|
|
||||||
which SHOULD be anonymized or just a placeholder.
|
|
||||||
|
|
||||||
From: sender@domain
|
|
||||||
To: rcpt@domain
|
|
||||||
Chat-Version: 1.0
|
|
||||||
Content-Type: text/plain
|
|
||||||
Subject: Chat: Forwarded message
|
|
||||||
|
|
||||||
---------- Forwarded message ----------
|
|
||||||
From: Messenger
|
|
||||||
|
|
||||||
Hello world!
|
|
||||||
|
|
||||||
Incoming forwarded messages are detected by the header.
|
|
||||||
The messenger SHOULD mark these messages in a way that
|
|
||||||
it becomes obvious that the message is not created by the sender.
|
|
||||||
Note that most messengers do not show the original sender of forwarded messages
|
|
||||||
but MUAs typically expose the sender in the UI.
|
|
||||||
|
|
||||||
|
|
||||||
# Groups
|
|
||||||
|
|
||||||
Groups are chats with usually more than one recipient,
|
|
||||||
each defined by an email-address.
|
|
||||||
The sender plus the recipients are the group members.
|
|
||||||
|
|
||||||
To allow different groups with the same members,
|
|
||||||
groups are identified by a group-id.
|
|
||||||
The group-id MUST be created only from the characters
|
|
||||||
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`
|
|
||||||
and MUST have a length of at least 11 characters.
|
|
||||||
|
|
||||||
Groups MUST have a group-name.
|
|
||||||
The group-name is any non-zero-length UTF-8 string.
|
|
||||||
|
|
||||||
Groups MAY have a group-image.
|
|
||||||
|
|
||||||
|
|
||||||
## Outgoing groups messages
|
|
||||||
|
|
||||||
All group members MUST be added to the `From`/`To` headers.
|
|
||||||
The group-id MUST be written to the `Chat-Group-ID` header.
|
|
||||||
The group-name MUST be written to `Chat-Group-Name` header
|
|
||||||
(the forced presence of this header makes it easier
|
|
||||||
to join a group chat on a second device any time).
|
|
||||||
|
|
||||||
The `Subject` header of outgoing group messages
|
|
||||||
SHOULD start with the characters `Chat:`
|
|
||||||
followed by the group-name and a colon followed by an excerpt of the message.
|
|
||||||
|
|
||||||
To identify the group-id on replies from normal MUAs,
|
|
||||||
the group-id MUST also be added to the message-id of outgoing messages.
|
|
||||||
The message-id MUST have the format `Gr.<group-id>.<unique data>`.
|
|
||||||
|
|
||||||
From: member1@domain
|
|
||||||
To: member2@domain, member3@domain
|
|
||||||
Chat-Version: 1.0
|
|
||||||
Chat-Group-ID: 12345uvwxyZ
|
|
||||||
Chat-Group-Name: My Group
|
|
||||||
Message-ID: Gr.12345uvwxyZ.0001@domain
|
|
||||||
Subject: Chat: My Group: Hello group ...
|
|
||||||
|
|
||||||
Hello group - this group contains three members
|
|
||||||
|
|
||||||
Messengers adding the member list in the form `Name <email-address>`
|
|
||||||
MUST take care only to spread the names authorized by the contacts themselves.
|
|
||||||
Otherwise, names as _Daddy_ or _Honey_ may be spread
|
|
||||||
(this issue is also true for normal MUAs, however,
|
|
||||||
for more contact- and chat-centralized apps
|
|
||||||
such situations happen more frequently).
|
|
||||||
|
|
||||||
|
|
||||||
## Incoming group messages
|
|
||||||
|
|
||||||
The messenger MUST search incoming messages for the group-id
|
|
||||||
in the following headers: `Chat-Group-ID`,
|
|
||||||
`Message-ID`, `In-Reply-To` and `References` (in this order).
|
|
||||||
|
|
||||||
If the messenger finds a valid and existent group-id,
|
|
||||||
the message SHOULD be assigned to the given group.
|
|
||||||
If the messenger finds a valid but not existent group-id,
|
|
||||||
the messenger MAY create a new group.
|
|
||||||
If no group-id is found,
|
|
||||||
the message MAY be assigned
|
|
||||||
to a normal single-user chat with the email-address given in `From`.
|
|
||||||
|
|
||||||
|
|
||||||
## Add and remove members
|
|
||||||
|
|
||||||
Messenger clients MUST construct the member list
|
|
||||||
from the `From`/`To` headers only on the first group message
|
|
||||||
or if they see a `Chat-Group-Member-Added`
|
|
||||||
or `Chat-Group-Member-Removed` action header.
|
|
||||||
Both headers MUST have the email-address
|
|
||||||
of the added or removed member as the value.
|
|
||||||
Messenger clients MUST NOT construct the member list
|
|
||||||
on other group messages
|
|
||||||
(this is to avoid accidentally altered To-lists in normal MUAs;
|
|
||||||
the user does not expect adding a user to a _message_
|
|
||||||
will also add him to the _group_ "forever").
|
|
||||||
|
|
||||||
The messenger SHOULD send an explicit mail for each added or removed member.
|
|
||||||
The body of the message SHOULD contain
|
|
||||||
a localized description about what happened
|
|
||||||
and the message SHOULD appear as a message or action from the sender.
|
|
||||||
|
|
||||||
From: member1@domain
|
|
||||||
To: member2@domain, member3@domain, member4@domain
|
|
||||||
Chat-Version: 1.0
|
|
||||||
Chat-Group-ID: 12345uvwxyZ
|
|
||||||
Chat-Group-Name: My Group
|
|
||||||
Chat-Group-Member-Added: member4@domain
|
|
||||||
Message-ID: Gr.12345uvwxyZ.0002@domain
|
|
||||||
Subject: Chat: My Group: Hello, ...
|
|
||||||
|
|
||||||
Hello, I've added member4@domain to our group. Now we have 4 members.
|
|
||||||
|
|
||||||
To remove a member:
|
|
||||||
|
|
||||||
From: member1@domain
|
|
||||||
To: member2@domain, member3@domain
|
|
||||||
Chat-Version: 1.0
|
|
||||||
Chat-Group-ID: 12345uvwxyZ
|
|
||||||
Chat-Group-Name: My Group
|
|
||||||
Chat-Group-Member-Removed: member4@domain
|
|
||||||
Message-ID: Gr.12345uvwxyZ.0003@domain
|
|
||||||
Subject: Chat: My Group: Hello, ...
|
|
||||||
|
|
||||||
Hello, I've removed member4@domain from our group. Now we have 3 members.
|
|
||||||
|
|
||||||
|
|
||||||
## Change group name
|
|
||||||
|
|
||||||
To change the group-name,
|
|
||||||
the messenger MUST send the action header `Chat-Group-Name-Changed`
|
|
||||||
with the value set to the old group name to all group members.
|
|
||||||
The new group name goes to the header `Chat-Group-Name`.
|
|
||||||
|
|
||||||
The messenger SHOULD send an explicit mail for each name change.
|
|
||||||
The body of the message SHOULD contain
|
|
||||||
a localized description about what happened
|
|
||||||
and the message SHOULD appear as a message or action from the sender.
|
|
||||||
|
|
||||||
From: member1@domain
|
|
||||||
To: member2@domain, member3@domain
|
|
||||||
Chat-Version: 1.0
|
|
||||||
Chat-Group-ID: 12345uvwxyZ
|
|
||||||
Chat-Group-Name: Our Group
|
|
||||||
Chat-Group-Name-Changed: My Group
|
|
||||||
Message-ID: Gr.12345uvwxyZ.0004@domain
|
|
||||||
Subject: Chat: Our Group: Hello, ...
|
|
||||||
|
|
||||||
Hello, I've changed the group name from "My Group" to "Our Group".
|
|
||||||
|
|
||||||
|
|
||||||
## Set group image
|
|
||||||
|
|
||||||
A group MAY have a group-image.
|
|
||||||
To change or set the group-image,
|
|
||||||
the messenger MUST attach an image file to a message
|
|
||||||
and MUST add the header `Chat-Group-Image`
|
|
||||||
with the value set to the image name.
|
|
||||||
|
|
||||||
To remove the group-image,
|
|
||||||
the messenger MUST add the header `Chat-Group-Image: 0`.
|
|
||||||
|
|
||||||
The messenger SHOULD send an explicit mail for each group image change.
|
|
||||||
The body of the message SHOULD contain
|
|
||||||
a localized description about what happened
|
|
||||||
and the message SHOULD appear as a message or action from the sender.
|
|
||||||
|
|
||||||
|
|
||||||
From: member1@domain
|
|
||||||
To: member2@domain, member3@domain
|
|
||||||
Chat-Version: 1.0
|
|
||||||
Chat-Group-ID: 12345uvwxyZ
|
|
||||||
Chat-Group-Name: Our Group
|
|
||||||
Chat-Group-Image: image.jpg
|
|
||||||
Message-ID: Gr.12345uvwxyZ.0005@domain
|
|
||||||
Subject: Chat: Our Group: Hello, ...
|
|
||||||
Content-Type: multipart/mixed; boundary="==break=="
|
|
||||||
|
|
||||||
--==break==
|
|
||||||
Content-Type: text/plain
|
|
||||||
|
|
||||||
Hello, I've changed the group image.
|
|
||||||
--==break==
|
|
||||||
Content-Type: image/jpeg
|
|
||||||
Content-Disposition: attachment; filename="image.jpg"
|
|
||||||
|
|
||||||
/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBw ...
|
|
||||||
--==break==--
|
|
||||||
|
|
||||||
The image format SHOULD be image/jpeg or image/png.
|
|
||||||
To save data, it is RECOMMENDED
|
|
||||||
to add a `Chat-Group-Image` only on image changes.
|
|
||||||
|
|
||||||
|
|
||||||
# Set profile image
|
|
||||||
|
|
||||||
A user MAY have a profile-image that MAY be spread to his contacts.
|
|
||||||
To change or set the profile-image,
|
|
||||||
the messenger MUST attach an image file to a message
|
|
||||||
and MUST add the header `Chat-Profile-Image`
|
|
||||||
with the value set to the image name.
|
|
||||||
|
|
||||||
To remove the profile-image,
|
|
||||||
the messenger MUST add the header `Chat-Profile-Image: 0`.
|
|
||||||
|
|
||||||
To spread the image,
|
|
||||||
the messenger MAY send the profile image
|
|
||||||
together with the next mail to a given contact
|
|
||||||
(to do this only once,
|
|
||||||
the messenger has to keep a `profile_image_update_state` somewhere).
|
|
||||||
Alternatively, the messenger MAY send an explicit mail
|
|
||||||
for each profile-image change to all contacts using a compatible messenger.
|
|
||||||
The messenger SHOULD NOT send an explicit mail to normal MUAs.
|
|
||||||
|
|
||||||
From: sender@domain
|
|
||||||
To: rcpt@domain
|
|
||||||
Chat-Version: 1.0
|
|
||||||
Chat-Profile-Image: photo.jpg
|
|
||||||
Subject: Chat: Hello, ...
|
|
||||||
Content-Type: multipart/mixed; boundary="==break=="
|
|
||||||
|
|
||||||
--==break==
|
|
||||||
Content-Type: text/plain
|
|
||||||
|
|
||||||
Hello, I've changed my profile image.
|
|
||||||
--==break==
|
|
||||||
Content-Type: image/jpeg
|
|
||||||
Content-Disposition: attachment; filename="photo.jpg"
|
|
||||||
|
|
||||||
AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQYFBA ...
|
|
||||||
--==break==--
|
|
||||||
|
|
||||||
The image format SHOULD be image/jpeg or image/png.
|
|
||||||
Note that `Chat-Profile-Image` may appear together with all other headers,
|
|
||||||
eg. there may be a `Chat-Profile-Image` and a `Chat-Group-Image` header
|
|
||||||
in the same message.
|
|
||||||
To save data, it is RECOMMENDED to add a `Chat-Profile-Image` header
|
|
||||||
only on image changes.
|
|
||||||
|
|
||||||
|
|
||||||
# Miscellaneous
|
|
||||||
|
|
||||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
|
||||||
|
|
||||||
Messengers SHOULD add a `Chat-Voice-message: 1` header
|
|
||||||
if an attached audio file is a voice message.
|
|
||||||
|
|
||||||
Messengers MAY add a `Chat-Duration` header
|
|
||||||
to specify the duration of attached audio or video files.
|
|
||||||
The value MUST be the duration in milliseconds.
|
|
||||||
This allows the receiver to show the time without knowing the file format.
|
|
||||||
|
|
||||||
In-Reply-To: Gr.12345uvwxyZ.0005@domain
|
|
||||||
Chat-Voice-Message: 1
|
|
||||||
Chat-Duration: 10000
|
|
||||||
|
|
||||||
Messengers MAY send and receive Message Disposition Notifications
|
|
||||||
(MDNs, [RFC 8098](https://tools.ietf.org/html/rfc8098),
|
|
||||||
[RFC 3503](https://tools.ietf.org/html/rfc3503))
|
|
||||||
using the `Chat-Disposition-Notification-To` header
|
|
||||||
instead of the `Disposition-Notification-To`
|
|
||||||
(which unfortunately forces many other MUAs
|
|
||||||
to send weird mails not following any standard).
|
|
||||||
|
|
||||||
|
|
||||||
## Sync messages
|
|
||||||
|
|
||||||
If some action is required by a message header,
|
|
||||||
the action should only be performed if the _effective date_ is newer
|
|
||||||
than the date the last action was performed.
|
|
||||||
|
|
||||||
We define the effective date of a message
|
|
||||||
as the sending time of the message as indicated by its Date header,
|
|
||||||
or the time of first receipt if that date is in the future or unavailable.
|
|
||||||
|
|
||||||
|
|
||||||
Copyright © 2017-2019 Delta Chat contributors.
|
|
||||||
203
src/aheader.rs
203
src/aheader.rs
@@ -1,14 +1,13 @@
|
|||||||
//! # Autocrypt header module
|
|
||||||
//!
|
|
||||||
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::ffi::CStr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{fmt, str};
|
use std::{fmt, str};
|
||||||
|
|
||||||
|
use mmime::mailimf_types::*;
|
||||||
|
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::Context;
|
use crate::dc_tools::as_str;
|
||||||
use crate::key::*;
|
use crate::key::*;
|
||||||
|
|
||||||
/// Possible values for encryption preference
|
/// Possible values for encryption preference
|
||||||
@@ -42,13 +41,13 @@ impl str::FromStr for EncryptPreference {
|
|||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s {
|
match s {
|
||||||
"mutual" => Ok(EncryptPreference::Mutual),
|
"mutual" => Ok(EncryptPreference::Mutual),
|
||||||
"nopreference" => Ok(EncryptPreference::NoPreference),
|
"reset" => Ok(EncryptPreference::Reset),
|
||||||
_ => Err(()),
|
_ => Ok(EncryptPreference::NoPreference),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Autocrypt header
|
/// Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Aheader {
|
pub struct Aheader {
|
||||||
pub addr: String,
|
pub addr: String,
|
||||||
@@ -57,7 +56,6 @@ pub struct Aheader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Aheader {
|
impl Aheader {
|
||||||
/// Creates new autocrypt header
|
|
||||||
pub fn new(addr: String, public_key: Key, prefer_encrypt: EncryptPreference) -> Self {
|
pub fn new(addr: String, public_key: Key, prefer_encrypt: EncryptPreference) -> Self {
|
||||||
Aheader {
|
Aheader {
|
||||||
addr,
|
addr,
|
||||||
@@ -66,54 +64,69 @@ impl Aheader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_headers(
|
pub fn from_imffields(
|
||||||
context: &Context,
|
wanted_from: *const libc::c_char,
|
||||||
wanted_from: &str,
|
header: *const mailimf_fields,
|
||||||
headers: &[mailparse::MailHeader<'_>],
|
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
use mailparse::MailHeaderMap;
|
if wanted_from.is_null() || header.is_null() {
|
||||||
|
return None;
|
||||||
if let Ok(Some(value)) = headers.get_first_value("Autocrypt") {
|
|
||||||
match Self::from_str(&value) {
|
|
||||||
Ok(header) => {
|
|
||||||
if addr_cmp(&header.addr, wanted_from) {
|
|
||||||
return Some(header);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!(
|
|
||||||
context,
|
|
||||||
"found invalid autocrypt header {}: {:?}", value, err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
let mut fine_header = None;
|
||||||
|
let mut cur = unsafe { (*(*header).fld_list).first };
|
||||||
|
|
||||||
|
while !cur.is_null() {
|
||||||
|
let field = unsafe { (*cur).data as *mut mailimf_field };
|
||||||
|
if !field.is_null()
|
||||||
|
&& unsafe { (*field).fld_type } == MAILIMF_FIELD_OPTIONAL_FIELD as libc::c_int
|
||||||
|
{
|
||||||
|
let optional_field = unsafe { (*field).fld_data.fld_optional_field };
|
||||||
|
if !optional_field.is_null()
|
||||||
|
&& unsafe { !(*optional_field).fld_name.is_null() }
|
||||||
|
&& unsafe { CStr::from_ptr((*optional_field).fld_name).to_str().unwrap() }
|
||||||
|
== "Autocrypt"
|
||||||
|
{
|
||||||
|
let value = unsafe {
|
||||||
|
CStr::from_ptr((*optional_field).fld_value)
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
match Self::from_str(value) {
|
||||||
|
Ok(test) => {
|
||||||
|
if addr_cmp(&test.addr, as_str(wanted_from)) {
|
||||||
|
if fine_header.is_none() {
|
||||||
|
fine_header = Some(test);
|
||||||
|
} else {
|
||||||
|
// TODO: figure out what kind of error case this is
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cur = unsafe { (*cur).next };
|
||||||
|
}
|
||||||
|
|
||||||
|
fine_header
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Aheader {
|
impl fmt::Display for Aheader {
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(fmt, "addr={};", self.addr)?;
|
// TODO replace 78 with enum /rtn
|
||||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
// adds a whitespace every 78 characters, this allows libEtPan to
|
||||||
write!(fmt, " prefer-encrypt=mutual;")?;
|
// wrap the lines according to RFC 5322
|
||||||
}
|
|
||||||
|
|
||||||
// adds a whitespace every 78 characters, this allows
|
|
||||||
// email crate to wrap the lines according to RFC 5322
|
|
||||||
// (which may insert a linebreak before every whitespace)
|
// (which may insert a linebreak before every whitespace)
|
||||||
let keydata = self.public_key.to_base64().chars().enumerate().fold(
|
let keydata = self.public_key.to_base64(78);
|
||||||
String::new(),
|
write!(
|
||||||
|mut res, (i, c)| {
|
fmt,
|
||||||
if i % 78 == 78 - "keydata=".len() {
|
"addr={}; prefer-encrypt={}; keydata={}",
|
||||||
res.push(' ')
|
self.addr, self.prefer_encrypt, keydata
|
||||||
}
|
)
|
||||||
res.push(c);
|
|
||||||
res
|
|
||||||
},
|
|
||||||
);
|
|
||||||
write!(fmt, " keydata={}", keydata)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,9 +135,9 @@ impl str::FromStr for Aheader {
|
|||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
let mut attributes: BTreeMap<String, String> = s
|
let mut attributes: BTreeMap<String, String> = s
|
||||||
.split(';')
|
.split(";")
|
||||||
.filter_map(|a| {
|
.filter_map(|a| {
|
||||||
let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
|
let attribute: Vec<&str> = a.trim().splitn(2, "=").collect();
|
||||||
if attribute.len() < 2 {
|
if attribute.len() < 2 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -159,14 +172,17 @@ impl str::FromStr for Aheader {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let prefer_encrypt = attributes
|
let prefer_encrypt = match attributes
|
||||||
.remove("prefer-encrypt")
|
.remove("prefer-encrypt")
|
||||||
.and_then(|raw| raw.parse().ok())
|
.and_then(|raw| raw.parse().ok())
|
||||||
.unwrap_or_default();
|
{
|
||||||
|
Some(pref) => pref,
|
||||||
|
None => EncryptPreference::NoPreference,
|
||||||
|
};
|
||||||
|
|
||||||
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
||||||
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
||||||
if attributes.keys().any(|k| !k.starts_with('_')) {
|
if attributes.keys().find(|k| !k.starts_with("_")).is_some() {
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,13 +198,15 @@ impl str::FromStr for Aheader {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const RAWKEY: &str = "xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=";
|
fn rawkey() -> String {
|
||||||
|
"xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=".into()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_str() {
|
fn test_from_str() {
|
||||||
let h: Aheader = format!(
|
let h: Aheader = format!(
|
||||||
"addr=me@mail.com; prefer-encrypt=mutual; keydata={}",
|
"addr=me@mail.com; prefer-encrypt=mutual; keydata={}",
|
||||||
RAWKEY
|
rawkey()
|
||||||
)
|
)
|
||||||
.parse()
|
.parse()
|
||||||
.expect("failed to parse");
|
.expect("failed to parse");
|
||||||
@@ -197,22 +215,9 @@ mod tests {
|
|||||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptPreference::Reset is an internal value, parser should never return it
|
|
||||||
#[test]
|
|
||||||
fn test_from_str_reset() {
|
|
||||||
let raw = format!(
|
|
||||||
"addr=reset@example.com; prefer-encrypt=reset; keydata={}",
|
|
||||||
RAWKEY
|
|
||||||
);
|
|
||||||
let h: Aheader = raw.parse().expect("failed to parse");
|
|
||||||
|
|
||||||
assert_eq!(h.addr, "reset@example.com");
|
|
||||||
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_str_non_critical() {
|
fn test_from_str_non_critical() {
|
||||||
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={}", RAWKEY);
|
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={}", rawkey());
|
||||||
let h: Aheader = raw.parse().expect("failed to parse");
|
let h: Aheader = raw.parse().expect("failed to parse");
|
||||||
|
|
||||||
assert_eq!(h.addr, "me@mail.com");
|
assert_eq!(h.addr, "me@mail.com");
|
||||||
@@ -223,57 +228,33 @@ mod tests {
|
|||||||
fn test_from_str_superflous_critical() {
|
fn test_from_str_superflous_critical() {
|
||||||
let raw = format!(
|
let raw = format!(
|
||||||
"addr=me@mail.com; _foo=one; _bar=two; other=me; keydata={}",
|
"addr=me@mail.com; _foo=one; _bar=two; other=me; keydata={}",
|
||||||
RAWKEY
|
rawkey()
|
||||||
);
|
);
|
||||||
assert!(raw.parse::<Aheader>().is_err());
|
assert!(raw.parse::<Aheader>().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_good_headers() {
|
fn test_good_headers() {
|
||||||
let fixed_header = concat!(
|
let fixed_header = "addr=a@b.example.org; prefer-encrypt=mutual; keydata=xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g 4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8 ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu88 80iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTll HOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+ws CJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiML AAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiF Nyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAg dLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72 rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v 81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgC u3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOt kb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKC LhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4s WVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWv BuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiML AAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGr wdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktE k6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0 j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7x egRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=";
|
||||||
"addr=a@b.example.org; prefer-encrypt=mutual; ",
|
|
||||||
"keydata=xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJg",
|
|
||||||
" WL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6",
|
|
||||||
" CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKK",
|
|
||||||
" bhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1Kv",
|
|
||||||
" VL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbG",
|
|
||||||
" UuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaK",
|
|
||||||
" rc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087",
|
|
||||||
" LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VN",
|
|
||||||
" HtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Ddd",
|
|
||||||
" fxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCv",
|
|
||||||
" SJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vau",
|
|
||||||
" f1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+",
|
|
||||||
" G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjm",
|
|
||||||
" kRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09",
|
|
||||||
" /JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHR",
|
|
||||||
" TR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaK",
|
|
||||||
" rc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivX",
|
|
||||||
" urm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9Mtrm",
|
|
||||||
" ZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb",
|
|
||||||
" +F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNg",
|
|
||||||
" wm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc="
|
|
||||||
);
|
|
||||||
|
|
||||||
let ah = Aheader::from_str(fixed_header).expect("failed to parse");
|
let ah = Aheader::from_str(fixed_header).expect("failed to parse");
|
||||||
assert_eq!(ah.addr, "a@b.example.org");
|
assert_eq!(ah.addr, "a@b.example.org");
|
||||||
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
||||||
assert_eq!(format!("{}", ah), fixed_header);
|
|
||||||
|
|
||||||
let rendered = ah.to_string();
|
let rendered = ah.to_string();
|
||||||
assert_eq!(rendered, fixed_header);
|
assert_eq!(rendered, fixed_header);
|
||||||
|
|
||||||
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", RAWKEY)).expect("failed to parse");
|
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", rawkey())).expect("failed to parse");
|
||||||
assert_eq!(ah.addr, "a@b.example.org");
|
assert_eq!(ah.addr, "a@b.example.org");
|
||||||
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
|
||||||
|
|
||||||
Aheader::from_str(&format!(
|
Aheader::from_str(&format!(
|
||||||
"addr=a@b.example.org; prefer-encrypt=ignoreUnknownValues; keydata={}",
|
"addr=a@b.example.org; prefer-encrypt=ignoreUnknownValues; keydata={}",
|
||||||
RAWKEY
|
rawkey()
|
||||||
))
|
))
|
||||||
.expect("failed to parse");
|
.expect("failed to parse");
|
||||||
|
|
||||||
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", RAWKEY))
|
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", rawkey()))
|
||||||
.expect("failed to parse");
|
.expect("failed to parse");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,30 +266,4 @@ mod tests {
|
|||||||
assert!(Aheader::from_str(" ;;").is_err());
|
assert!(Aheader::from_str(" ;;").is_err());
|
||||||
assert!(Aheader::from_str("addr=a@t.de; unknwon=1; keydata=jau").is_err());
|
assert!(Aheader::from_str("addr=a@t.de; unknwon=1; keydata=jau").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_display_aheader() {
|
|
||||||
assert!(format!(
|
|
||||||
"{}",
|
|
||||||
Aheader::new(
|
|
||||||
"test@example.com".to_string(),
|
|
||||||
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
|
|
||||||
EncryptPreference::Mutual
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.contains("prefer-encrypt=mutual;"));
|
|
||||||
|
|
||||||
// According to Autocrypt Level 1 specification,
|
|
||||||
// only "prefer-encrypt=mutual;" can be used.
|
|
||||||
// If the setting is nopreference, the whole attribute is omitted.
|
|
||||||
assert!(!format!(
|
|
||||||
"{}",
|
|
||||||
Aheader::new(
|
|
||||||
"test@example.com".to_string(),
|
|
||||||
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
|
|
||||||
EncryptPreference::NoPreference
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.contains("prefer-encrypt"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
595
src/blob.rs
595
src/blob.rs
@@ -1,595 +0,0 @@
|
|||||||
//! # Blob directory management
|
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::fmt;
|
|
||||||
use std::fs;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use crate::context::Context;
|
|
||||||
use crate::events::Event;
|
|
||||||
|
|
||||||
/// Represents a file in the blob directory.
|
|
||||||
///
|
|
||||||
/// The object has a name, which will always be valid UTF-8. Having a
|
|
||||||
/// blob object does not imply the respective file exists, however
|
|
||||||
/// when using one of the `create*()` methods a unique file is
|
|
||||||
/// created.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct BlobObject<'a> {
|
|
||||||
blobdir: &'a Path,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> BlobObject<'a> {
|
|
||||||
/// Creates a new blob object with a unique name.
|
|
||||||
///
|
|
||||||
/// Creates a new file in the blob directory. The name will be
|
|
||||||
/// derived from the platform-agnostic basename of the suggested
|
|
||||||
/// name, followed by a random number and followed by a possible
|
|
||||||
/// extension. The `data` will be written into the file without
|
|
||||||
/// race-conditions.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// [BlobError::CreateFailure] is used when the file could not
|
|
||||||
/// be created. You can expect [BlobError.cause] to contain an
|
|
||||||
/// underlying error.
|
|
||||||
///
|
|
||||||
/// [BlobError::WriteFailure] is used when the file could not
|
|
||||||
/// be written to. You can expect [BlobError.cause] to contain an
|
|
||||||
/// underlying error.
|
|
||||||
pub fn create(
|
|
||||||
context: &'a Context,
|
|
||||||
suggested_name: impl AsRef<str>,
|
|
||||||
data: &[u8],
|
|
||||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
|
||||||
let blobdir = context.get_blobdir();
|
|
||||||
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
|
|
||||||
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext)?;
|
|
||||||
file.write_all(data)
|
|
||||||
.map_err(|err| BlobError::WriteFailure {
|
|
||||||
blobdir: blobdir.to_path_buf(),
|
|
||||||
blobname: name.clone(),
|
|
||||||
cause: err,
|
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
|
||||||
let blob = BlobObject {
|
|
||||||
blobdir,
|
|
||||||
name: format!("$BLOBDIR/{}", name),
|
|
||||||
};
|
|
||||||
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
|
|
||||||
Ok(blob)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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> {
|
|
||||||
let max_attempt = 15;
|
|
||||||
let mut name = format!("{}{}", stem, ext);
|
|
||||||
for attempt in 0..max_attempt {
|
|
||||||
let path = dir.join(&name);
|
|
||||||
match fs::OpenOptions::new()
|
|
||||||
.create_new(true)
|
|
||||||
.write(true)
|
|
||||||
.open(&path)
|
|
||||||
{
|
|
||||||
Ok(file) => return Ok((name, file)),
|
|
||||||
Err(err) => {
|
|
||||||
if attempt == max_attempt {
|
|
||||||
return Err(BlobError::CreateFailure {
|
|
||||||
blobdir: dir.to_path_buf(),
|
|
||||||
blobname: name,
|
|
||||||
cause: err,
|
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This is supposed to be unreachable, but the compiler doesn't know.
|
|
||||||
Err(BlobError::CreateFailure {
|
|
||||||
blobdir: dir.to_path_buf(),
|
|
||||||
blobname: name,
|
|
||||||
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
|
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new blob object with unique name by copying an existing file.
|
|
||||||
///
|
|
||||||
/// This creates a new blob as described in [BlobObject::create]
|
|
||||||
/// but also copies an existing file into it. This is done in a
|
|
||||||
/// in way which avoids race-conditions when multiple files are
|
|
||||||
/// concurrently created.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// In addition to the errors in [BlobObject::create] the
|
|
||||||
/// [BlobError::CopyFailure] is used when the data can not be
|
|
||||||
/// copied.
|
|
||||||
pub fn create_and_copy(
|
|
||||||
context: &'a Context,
|
|
||||||
src: impl AsRef<Path>,
|
|
||||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
|
||||||
let mut src_file = fs::File::open(src.as_ref()).map_err(|err| BlobError::CopyFailure {
|
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
|
||||||
blobname: String::from(""),
|
|
||||||
src: src.as_ref().to_path_buf(),
|
|
||||||
cause: err,
|
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
|
||||||
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_for_err = name.clone();
|
|
||||||
std::io::copy(&mut src_file, &mut dst_file).map_err(|err| {
|
|
||||||
{
|
|
||||||
// Attempt to remove the failed file, swallow errors resulting from that.
|
|
||||||
let path = context.get_blobdir().join(&name_for_err);
|
|
||||||
fs::remove_file(path).ok();
|
|
||||||
}
|
|
||||||
BlobError::CopyFailure {
|
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
|
||||||
blobname: name_for_err,
|
|
||||||
src: src.as_ref().to_path_buf(),
|
|
||||||
cause: err,
|
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
let blob = BlobObject {
|
|
||||||
blobdir: context.get_blobdir(),
|
|
||||||
name: format!("$BLOBDIR/{}", name),
|
|
||||||
};
|
|
||||||
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
|
|
||||||
Ok(blob)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a blob from a file, possibly copying it to the blobdir.
|
|
||||||
///
|
|
||||||
/// If the source file is not a path to into the blob directory
|
|
||||||
/// the file will be copied into the blob directory first. If the
|
|
||||||
/// source file is already in the blobdir it will not be copied
|
|
||||||
/// and only be created if it is a valid blobname, that is no
|
|
||||||
/// subdirectory is used and [BlobObject::sanitise_name] does not
|
|
||||||
/// modify the filename.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// This merely delegates to the [BlobObject::create_and_copy] and
|
|
||||||
/// the [BlobObject::from_path] methods. See those for possible
|
|
||||||
/// errors.
|
|
||||||
pub fn create_from_path(
|
|
||||||
context: &Context,
|
|
||||||
src: impl AsRef<Path>,
|
|
||||||
) -> std::result::Result<BlobObject, BlobError> {
|
|
||||||
if src.as_ref().starts_with(context.get_blobdir()) {
|
|
||||||
BlobObject::from_path(context, src)
|
|
||||||
} else {
|
|
||||||
BlobObject::create_and_copy(context, src)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [BlobObject] for an existing blob from a path.
|
|
||||||
///
|
|
||||||
/// The path must designate a file directly in the blobdir and
|
|
||||||
/// must use a valid blob name. That is after sanitisation the
|
|
||||||
/// name must still be the same, that means it must be valid UTF-8
|
|
||||||
/// and not have any special characters in it.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// [BlobError::WrongBlobdir] is used if the path is not in
|
|
||||||
/// the blob directory.
|
|
||||||
///
|
|
||||||
/// [BlobError::WrongName] is used if the file name does not
|
|
||||||
/// remain identical after sanitisation.
|
|
||||||
pub fn from_path(
|
|
||||||
context: &Context,
|
|
||||||
path: impl AsRef<Path>,
|
|
||||||
) -> std::result::Result<BlobObject, BlobError> {
|
|
||||||
let rel_path = path
|
|
||||||
.as_ref()
|
|
||||||
.strip_prefix(context.get_blobdir())
|
|
||||||
.map_err(|_| BlobError::WrongBlobdir {
|
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
|
||||||
src: path.as_ref().to_path_buf(),
|
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
|
||||||
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
|
||||||
return Err(BlobError::WrongName {
|
|
||||||
blobname: path.as_ref().to_path_buf(),
|
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
|
||||||
blobname: path.as_ref().to_path_buf(),
|
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
|
||||||
BlobObject::from_name(context, name.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [BlobObject] for an existing blob.
|
|
||||||
///
|
|
||||||
/// The `name` may optionally be prefixed with the `$BLOBDIR/`
|
|
||||||
/// prefixed, as returned by [BlobObject::as_name]. This is how
|
|
||||||
/// you want to create a [BlobObject] for a filename read from the
|
|
||||||
/// database.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// [BlobError::WrongName] is used if the name is not a valid
|
|
||||||
/// blobname, i.e. if [BlobObject::sanitise_name] does modify the
|
|
||||||
/// provided name.
|
|
||||||
pub fn from_name(
|
|
||||||
context: &'a Context,
|
|
||||||
name: String,
|
|
||||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
|
||||||
let name: String = match name.starts_with("$BLOBDIR/") {
|
|
||||||
true => name.splitn(2, '/').last().unwrap().to_string(),
|
|
||||||
false => name,
|
|
||||||
};
|
|
||||||
if !BlobObject::is_acceptible_blob_name(&name) {
|
|
||||||
return Err(BlobError::WrongName {
|
|
||||||
blobname: PathBuf::from(name),
|
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(BlobObject {
|
|
||||||
blobdir: context.get_blobdir(),
|
|
||||||
name: format!("$BLOBDIR/{}", name),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the absolute path to the blob in the filesystem.
|
|
||||||
pub fn to_abs_path(&self) -> PathBuf {
|
|
||||||
let fname = Path::new(&self.name).strip_prefix("$BLOBDIR/").unwrap();
|
|
||||||
self.blobdir.join(fname)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the blob name, as stored in the database.
|
|
||||||
///
|
|
||||||
/// This returns the blob in the `$BLOBDIR/<name>` format used in
|
|
||||||
/// the database. Do not use this unless you're about to store
|
|
||||||
/// this string in the database or [Params]. Eventually even
|
|
||||||
/// those conversions should be handled by the type system.
|
|
||||||
///
|
|
||||||
/// [Params]: crate::param::Params
|
|
||||||
pub fn as_name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the filename of the blob.
|
|
||||||
pub fn as_file_name(&self) -> &str {
|
|
||||||
self.name.rsplitn(2, '/').next().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The path relative in the blob directory.
|
|
||||||
pub fn as_rel_path(&self) -> &Path {
|
|
||||||
Path::new(self.as_file_name())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the extension of the blob.
|
|
||||||
///
|
|
||||||
/// If a blob's filename has an extension, it is always guaranteed
|
|
||||||
/// to be lowercase.
|
|
||||||
pub fn suffix(&self) -> Option<&str> {
|
|
||||||
let ext = self.name.rsplitn(2, '.').next();
|
|
||||||
if ext == Some(&self.name) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
ext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a safe name based on a messy input string.
|
|
||||||
///
|
|
||||||
/// The safe name will be a valid filename on Unix and Windows and
|
|
||||||
/// not contain any path separators. The input can contain path
|
|
||||||
/// segments separated by either Unix or Windows path separators,
|
|
||||||
/// the rightmost non-empty segment will be used as name,
|
|
||||||
/// sanitised for special characters.
|
|
||||||
///
|
|
||||||
/// The resulting name is returned as a tuple, the first part
|
|
||||||
/// being the stem or basename and the second being an extension,
|
|
||||||
/// including the dot. E.g. "foo.txt" is returned as `("foo",
|
|
||||||
/// ".txt")` while "bar" is returned as `("bar", "")`.
|
|
||||||
///
|
|
||||||
/// The extension part will always be lowercased.
|
|
||||||
fn sanitise_name(name: &str) -> (String, String) {
|
|
||||||
let mut name = name.to_string();
|
|
||||||
for part in name.rsplit('/') {
|
|
||||||
if !part.is_empty() {
|
|
||||||
name = part.to_string();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for part in name.rsplit('\\') {
|
|
||||||
if !part.is_empty() {
|
|
||||||
name = part.to_string();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let opts = sanitize_filename::Options {
|
|
||||||
truncate: true,
|
|
||||||
windows: true,
|
|
||||||
replacement: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
let clean = sanitize_filename::sanitize_with_options(name, opts);
|
|
||||||
let mut iter = clean.splitn(2, '.');
|
|
||||||
let mut stem = iter.next().unwrap_or_default().to_string();
|
|
||||||
let mut ext = iter.next().unwrap_or_default().to_string();
|
|
||||||
stem.truncate(64);
|
|
||||||
ext.truncate(32);
|
|
||||||
match ext.len() {
|
|
||||||
0 => (stem, "".to_string()),
|
|
||||||
_ => (stem, format!(".{}", ext).to_lowercase()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks whether a name is a valid blob name.
|
|
||||||
///
|
|
||||||
/// This is slightly less strict than stanitise_name, presumably
|
|
||||||
/// someone already created a file with such a name so we just
|
|
||||||
/// ensure it's not actually a path in disguise is actually utf-8.
|
|
||||||
fn is_acceptible_blob_name(name: impl AsRef<OsStr>) -> bool {
|
|
||||||
let uname = match name.as_ref().to_str() {
|
|
||||||
Some(name) => name,
|
|
||||||
None => return false,
|
|
||||||
};
|
|
||||||
if uname.find('/').is_some() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if uname.find('\\').is_some() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if uname.find('\0').is_some() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> fmt::Display for BlobObject<'a> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "$BLOBDIR/{}", self.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Errors for the [BlobObject].
|
|
||||||
#[derive(Fail, Debug)]
|
|
||||||
pub enum BlobError {
|
|
||||||
CreateFailure {
|
|
||||||
blobdir: PathBuf,
|
|
||||||
blobname: String,
|
|
||||||
#[cause]
|
|
||||||
cause: std::io::Error,
|
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
|
||||||
WriteFailure {
|
|
||||||
blobdir: PathBuf,
|
|
||||||
blobname: String,
|
|
||||||
#[cause]
|
|
||||||
cause: std::io::Error,
|
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
|
||||||
CopyFailure {
|
|
||||||
blobdir: PathBuf,
|
|
||||||
blobname: String,
|
|
||||||
src: PathBuf,
|
|
||||||
#[cause]
|
|
||||||
cause: std::io::Error,
|
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
|
||||||
WrongBlobdir {
|
|
||||||
blobdir: PathBuf,
|
|
||||||
src: PathBuf,
|
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
|
||||||
WrongName {
|
|
||||||
blobname: PathBuf,
|
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementing Display is done by hand because the failure
|
|
||||||
// #[fail(display = "...")] syntax does not allow using
|
|
||||||
// `blobdir.display()`.
|
|
||||||
impl fmt::Display for BlobError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
// Match on the data rather than kind, they are equivalent for
|
|
||||||
// identifying purposes but contain the actual data we need.
|
|
||||||
match &self {
|
|
||||||
BlobError::CreateFailure {
|
|
||||||
blobdir, blobname, ..
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Failed to create blob {} in {}",
|
|
||||||
blobname,
|
|
||||||
blobdir.display()
|
|
||||||
),
|
|
||||||
BlobError::WriteFailure {
|
|
||||||
blobdir, blobname, ..
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Failed to write data to blob {} in {}",
|
|
||||||
blobname,
|
|
||||||
blobdir.display()
|
|
||||||
),
|
|
||||||
BlobError::CopyFailure {
|
|
||||||
blobdir,
|
|
||||||
blobname,
|
|
||||||
src,
|
|
||||||
..
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Failed to copy data from {} to blob {} in {}",
|
|
||||||
src.display(),
|
|
||||||
blobname,
|
|
||||||
blobdir.display(),
|
|
||||||
),
|
|
||||||
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
|
|
||||||
f,
|
|
||||||
"File path {} is not in blobdir {}",
|
|
||||||
src.display(),
|
|
||||||
blobdir.display(),
|
|
||||||
),
|
|
||||||
BlobError::WrongName { blobname, .. } => {
|
|
||||||
write!(f, "Blob has a bad name: {}", blobname.display(),)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use crate::test_utils::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_create() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let blob = BlobObject::create(&t.ctx, "foo", b"hello").unwrap();
|
|
||||||
let fname = t.ctx.get_blobdir().join("foo");
|
|
||||||
let data = fs::read(fname).unwrap();
|
|
||||||
assert_eq!(data, b"hello");
|
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/foo");
|
|
||||||
assert_eq!(blob.to_abs_path(), t.ctx.get_blobdir().join("foo"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lowercase_ext() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello").unwrap();
|
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_as_file_name() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
|
||||||
assert_eq!(blob.as_file_name(), "foo.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_as_rel_path() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
|
||||||
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_suffix() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
|
||||||
assert_eq!(blob.suffix(), Some("txt"));
|
|
||||||
let blob = BlobObject::create(&t.ctx, "bar", b"world").unwrap();
|
|
||||||
assert_eq!(blob.suffix(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_create_dup() {
|
|
||||||
let t = dummy_context();
|
|
||||||
BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
|
|
||||||
let foo_path = t.ctx.get_blobdir().join("foo.txt");
|
|
||||||
assert!(foo_path.exists());
|
|
||||||
BlobObject::create(&t.ctx, "foo.txt", b"world").unwrap();
|
|
||||||
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() {
|
|
||||||
let fname = dirent.unwrap().file_name();
|
|
||||||
if fname == foo_path.file_name().unwrap() {
|
|
||||||
assert_eq!(fs::read(&foo_path).unwrap(), b"hello");
|
|
||||||
} else {
|
|
||||||
let name = fname.to_str().unwrap();
|
|
||||||
assert!(name.starts_with("foo"));
|
|
||||||
assert!(name.ends_with(".txt"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_double_ext_preserved() {
|
|
||||||
let t = dummy_context();
|
|
||||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello").unwrap();
|
|
||||||
let foo_path = t.ctx.get_blobdir().join("foo.tar.gz");
|
|
||||||
assert!(foo_path.exists());
|
|
||||||
BlobObject::create(&t.ctx, "foo.tar.gz", b"world").unwrap();
|
|
||||||
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() {
|
|
||||||
let fname = dirent.unwrap().file_name();
|
|
||||||
if fname == foo_path.file_name().unwrap() {
|
|
||||||
assert_eq!(fs::read(&foo_path).unwrap(), b"hello");
|
|
||||||
} else {
|
|
||||||
let name = fname.to_str().unwrap();
|
|
||||||
println!("{}", name);
|
|
||||||
assert!(name.starts_with("foo"));
|
|
||||||
assert!(name.ends_with(".tar.gz"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_create_long_names() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let s = "1".repeat(150);
|
|
||||||
let blob = BlobObject::create(&t.ctx, &s, b"data").unwrap();
|
|
||||||
let blobname = blob.as_name().split('/').last().unwrap();
|
|
||||||
assert!(blobname.len() < 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_create_and_copy() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let src = t.dir.path().join("src");
|
|
||||||
fs::write(&src, b"boo").unwrap();
|
|
||||||
let blob = BlobObject::create_and_copy(&t.ctx, &src).unwrap();
|
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
|
||||||
let data = fs::read(blob.to_abs_path()).unwrap();
|
|
||||||
assert_eq!(data, b"boo");
|
|
||||||
|
|
||||||
let whoops = t.dir.path().join("whoops");
|
|
||||||
assert!(BlobObject::create_and_copy(&t.ctx, &whoops).is_err());
|
|
||||||
let whoops = t.ctx.get_blobdir().join("whoops");
|
|
||||||
assert!(!whoops.exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_create_from_path() {
|
|
||||||
let t = dummy_context();
|
|
||||||
|
|
||||||
let src_ext = t.dir.path().join("external");
|
|
||||||
fs::write(&src_ext, b"boo").unwrap();
|
|
||||||
let blob = BlobObject::create_from_path(&t.ctx, &src_ext).unwrap();
|
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
|
||||||
let data = fs::read(blob.to_abs_path()).unwrap();
|
|
||||||
assert_eq!(data, b"boo");
|
|
||||||
|
|
||||||
let src_int = t.ctx.get_blobdir().join("internal");
|
|
||||||
fs::write(&src_int, b"boo").unwrap();
|
|
||||||
let blob = BlobObject::create_from_path(&t.ctx, &src_int).unwrap();
|
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
|
|
||||||
let data = fs::read(blob.to_abs_path()).unwrap();
|
|
||||||
assert_eq!(data, b"boo");
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn test_create_from_name_long() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
|
||||||
fs::write(&src_ext, b"boo").unwrap();
|
|
||||||
let blob = BlobObject::create_from_path(&t.ctx, &src_ext).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
blob.as_name(),
|
|
||||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_blob_name() {
|
|
||||||
assert!(BlobObject::is_acceptible_blob_name("foo"));
|
|
||||||
assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
|
|
||||||
assert!(BlobObject::is_acceptible_blob_name("f".repeat(128)));
|
|
||||||
assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
|
|
||||||
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
|
||||||
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2415
src/chat.rs
2415
src/chat.rs
File diff suppressed because it is too large
Load Diff
329
src/chatlist.rs
329
src/chatlist.rs
@@ -1,12 +1,11 @@
|
|||||||
//! # Chat list module
|
|
||||||
|
|
||||||
use crate::chat::*;
|
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::*;
|
use crate::context::*;
|
||||||
|
use crate::dc_chat::*;
|
||||||
|
use crate::dc_lot::*;
|
||||||
|
use crate::dc_msg::*;
|
||||||
|
use crate::dc_tools::*;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::lot::Lot;
|
|
||||||
use crate::message::{Message, MessageState, MsgId};
|
|
||||||
use crate::stock::StockMessage;
|
use crate::stock::StockMessage;
|
||||||
|
|
||||||
/// An object representing a single chatlist in memory.
|
/// An object representing a single chatlist in memory.
|
||||||
@@ -33,13 +32,17 @@ use crate::stock::StockMessage;
|
|||||||
/// first entry and only present on new messages, there is the rough idea that it can be optionally always
|
/// first entry and only present on new messages, there is the rough idea that it can be optionally always
|
||||||
/// present and sorted into the list by date. Rendering the deaddrop in the described way
|
/// present and sorted into the list by date. Rendering the deaddrop in the described way
|
||||||
/// would not add extra work in the UI then.
|
/// would not add extra work in the UI then.
|
||||||
#[derive(Debug)]
|
pub struct Chatlist<'a> {
|
||||||
pub struct Chatlist {
|
context: &'a Context,
|
||||||
/// Stores pairs of `chat_id, message_id`
|
/// Stores pairs of `chat_id, message_id`
|
||||||
ids: Vec<(u32, MsgId)>,
|
ids: Vec<(u32, u32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chatlist {
|
impl<'a> Chatlist<'a> {
|
||||||
|
pub fn get_context(&self) -> &Context {
|
||||||
|
self.context
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a list of chats.
|
/// Get a list of chats.
|
||||||
/// The list can be filtered by query parameters.
|
/// The list can be filtered by query parameters.
|
||||||
///
|
///
|
||||||
@@ -60,7 +63,7 @@ impl Chatlist {
|
|||||||
/// or "Not now".
|
/// or "Not now".
|
||||||
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
|
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
|
||||||
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
|
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
|
||||||
/// archived *any* chat using dc_archive_chat(). The UI should show a link as
|
/// archived _any_ chat using dc_archive_chat(). The UI should show a link as
|
||||||
/// "Show archived chats", if the user clicks this item, the UI should show a
|
/// "Show archived chats", if the user clicks this item, the UI should show a
|
||||||
/// list of all archived chats that can be created by this function hen using
|
/// list of all archived chats that can be created by this function hen using
|
||||||
/// the DC_GCL_ARCHIVED_ONLY flag.
|
/// the DC_GCL_ARCHIVED_ONLY flag.
|
||||||
@@ -71,7 +74,7 @@ impl Chatlist {
|
|||||||
/// The `listflags` is a combination of flags:
|
/// The `listflags` is a combination of flags:
|
||||||
/// - if the flag DC_GCL_ARCHIVED_ONLY is set, only archived chats are returned.
|
/// - if the flag DC_GCL_ARCHIVED_ONLY is set, only archived chats are returned.
|
||||||
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
||||||
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
|
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
|
||||||
/// chats
|
/// chats
|
||||||
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
||||||
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||||
@@ -83,17 +86,30 @@ impl Chatlist {
|
|||||||
/// `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 fn try_load(
|
||||||
context: &Context,
|
context: &'a Context,
|
||||||
listflags: usize,
|
listflags: usize,
|
||||||
query: Option<&str>,
|
query: Option<&str>,
|
||||||
query_contact_id: Option<u32>,
|
query_contact_id: Option<u32>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let mut add_archived_link_item = false;
|
let mut add_archived_link_item = 0;
|
||||||
|
|
||||||
|
// select with left join and minimum:
|
||||||
|
// - the inner select must use `hidden` and _not_ `m.hidden`
|
||||||
|
// which would refer the outer select and take a lot of time
|
||||||
|
// - `GROUP BY` is needed several messages may have the same timestamp
|
||||||
|
// - the list starts with the newest chats
|
||||||
|
// nb: the query currently shows messages from blocked contacts in groups.
|
||||||
|
// however, for normal-groups, this is okay as the message is also returned by dc_get_chat_msgs()
|
||||||
|
// (otherwise it would be hard to follow conversations, wa and tg do the same)
|
||||||
|
// for the deaddrop, however, they should really be hidden, however, _currently_ the deaddrop is not
|
||||||
|
// shown at all permanent in the chatlist.
|
||||||
|
|
||||||
let process_row = |row: &rusqlite::Row| {
|
let process_row = |row: &rusqlite::Row| {
|
||||||
let chat_id: u32 = row.get(0)?;
|
let chat_id: i32 = row.get(0)?;
|
||||||
let msg_id: MsgId = row.get(1).unwrap_or_default();
|
// TODO: verify that it is okay for this to be Null
|
||||||
Ok((chat_id, msg_id))
|
let msg_id: i32 = row.get(1).unwrap_or_default();
|
||||||
|
|
||||||
|
Ok((chat_id as u32, msg_id as u32))
|
||||||
};
|
};
|
||||||
|
|
||||||
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
||||||
@@ -101,60 +117,37 @@ impl Chatlist {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
};
|
};
|
||||||
|
|
||||||
// select with left join and minimum:
|
// nb: the query currently shows messages from blocked contacts in groups.
|
||||||
//
|
// however, for normal-groups, this is okay as the message is also returned by dc_get_chat_msgs()
|
||||||
// - the inner select must use `hidden` and _not_ `m.hidden`
|
// (otherwise it would be hard to follow conversations, wa and tg do the same)
|
||||||
// which would refer the outer select and take a lot of time
|
// for the deaddrop, however, they should really be hidden, however, _currently_ the deaddrop is not
|
||||||
// - `GROUP BY` is needed several messages may have the same
|
|
||||||
// timestamp
|
|
||||||
// - the list starts with the newest chats
|
|
||||||
//
|
|
||||||
// nb: the query currently shows messages from blocked
|
|
||||||
// contacts in groups. however, for normal-groups, this is
|
|
||||||
// okay as the message is also returned by dc_get_chat_msgs()
|
|
||||||
// (otherwise it would be hard to follow conversations, wa and
|
|
||||||
// tg do the same) for the deaddrop, however, they should
|
|
||||||
// really be hidden, however, _currently_ the deaddrop is not
|
|
||||||
// shown at all permanent in the chatlist.
|
// shown at all permanent in the chatlist.
|
||||||
|
|
||||||
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
||||||
// show chats shared with a given contact
|
// show chats shared with a given contact
|
||||||
context.sql.query_map(
|
context.sql.query_map(
|
||||||
"SELECT c.id, m.id
|
"SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \
|
||||||
FROM chats c
|
ON c.id=m.chat_id \
|
||||||
LEFT JOIN msgs m
|
AND m.timestamp=( SELECT MAX(timestamp) \
|
||||||
ON c.id=m.chat_id
|
FROM msgs WHERE chat_id=c.id \
|
||||||
AND m.timestamp=(
|
AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \
|
||||||
SELECT MAX(timestamp)
|
AND c.blocked=0 AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?) \
|
||||||
FROM msgs
|
GROUP BY c.id ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;",
|
||||||
WHERE chat_id=c.id
|
params![query_contact_id as i32],
|
||||||
AND (hidden=0 OR state=?))
|
process_row,
|
||||||
WHERE c.id>9
|
process_rows,
|
||||||
AND c.blocked=0
|
)?
|
||||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
|
|
||||||
GROUP BY c.id
|
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
|
||||||
params![MessageState::OutDraft, query_contact_id as i32],
|
|
||||||
process_row,
|
|
||||||
process_rows,
|
|
||||||
)?
|
|
||||||
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
|
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
|
||||||
// show archived chats
|
// show archived chats
|
||||||
context.sql.query_map(
|
context.sql.query_map(
|
||||||
"SELECT c.id, m.id
|
"SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \
|
||||||
FROM chats c
|
ON c.id=m.chat_id \
|
||||||
LEFT JOIN msgs m
|
AND m.timestamp=( SELECT MAX(timestamp) \
|
||||||
ON c.id=m.chat_id
|
FROM msgs WHERE chat_id=c.id \
|
||||||
AND m.timestamp=(
|
AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \
|
||||||
SELECT MAX(timestamp)
|
AND c.blocked=0 AND c.archived=1 GROUP BY c.id \
|
||||||
FROM msgs
|
ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;",
|
||||||
WHERE chat_id=c.id
|
params![],
|
||||||
AND (hidden=0 OR state=?))
|
|
||||||
WHERE c.id>9
|
|
||||||
AND c.blocked=0
|
|
||||||
AND c.archived=1
|
|
||||||
GROUP BY c.id
|
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
|
||||||
params![MessageState::OutDraft],
|
|
||||||
process_row,
|
process_row,
|
||||||
process_rows,
|
process_rows,
|
||||||
)?
|
)?
|
||||||
@@ -164,62 +157,51 @@ impl Chatlist {
|
|||||||
|
|
||||||
let str_like_cmd = format!("%{}%", query);
|
let str_like_cmd = format!("%{}%", query);
|
||||||
context.sql.query_map(
|
context.sql.query_map(
|
||||||
"SELECT c.id, m.id
|
"SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \
|
||||||
FROM chats c
|
ON c.id=m.chat_id \
|
||||||
LEFT JOIN msgs m
|
AND m.timestamp=( SELECT MAX(timestamp) \
|
||||||
ON c.id=m.chat_id
|
FROM msgs WHERE chat_id=c.id \
|
||||||
AND m.timestamp=(
|
AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \
|
||||||
SELECT MAX(timestamp)
|
AND c.blocked=0 AND c.name LIKE ? \
|
||||||
FROM msgs
|
GROUP BY c.id ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;",
|
||||||
WHERE chat_id=c.id
|
params![str_like_cmd],
|
||||||
AND (hidden=0 OR state=?))
|
|
||||||
WHERE c.id>9
|
|
||||||
AND c.blocked=0
|
|
||||||
AND c.name LIKE ?
|
|
||||||
GROUP BY c.id
|
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
|
||||||
params![MessageState::OutDraft, str_like_cmd],
|
|
||||||
process_row,
|
process_row,
|
||||||
process_rows,
|
process_rows,
|
||||||
)?
|
)?
|
||||||
} else {
|
} else {
|
||||||
// show normal chatlist
|
// show normal chatlist
|
||||||
let mut ids = context.sql.query_map(
|
let mut ids = context.sql.query_map(
|
||||||
"SELECT c.id, m.id
|
"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
|
AND m.timestamp=( SELECT MAX(timestamp) \
|
||||||
AND m.timestamp=(
|
FROM msgs WHERE chat_id=c.id \
|
||||||
SELECT MAX(timestamp)
|
AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \
|
||||||
FROM msgs
|
AND c.blocked=0 AND c.archived=0 \
|
||||||
WHERE chat_id=c.id
|
GROUP BY c.id \
|
||||||
AND (hidden=0 OR state=?))
|
ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;",
|
||||||
WHERE c.id>9
|
params![],
|
||||||
AND c.blocked=0
|
|
||||||
AND c.archived=0
|
|
||||||
GROUP BY c.id
|
|
||||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
|
||||||
params![MessageState::OutDraft],
|
|
||||||
process_row,
|
process_row,
|
||||||
process_rows,
|
process_rows,
|
||||||
)?;
|
)?;
|
||||||
if 0 == listflags & DC_GCL_NO_SPECIALS {
|
if 0 == listflags & DC_GCL_NO_SPECIALS {
|
||||||
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) {
|
let last_deaddrop_fresh_msg_id = get_last_deaddrop_fresh_msg(context);
|
||||||
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
|
if last_deaddrop_fresh_msg_id > 0 {
|
||||||
|
ids.push((1, last_deaddrop_fresh_msg_id));
|
||||||
}
|
}
|
||||||
add_archived_link_item = true;
|
add_archived_link_item = 1;
|
||||||
}
|
}
|
||||||
ids
|
ids
|
||||||
};
|
};
|
||||||
|
|
||||||
if add_archived_link_item && dc_get_archived_cnt(context) > 0 {
|
if 0 != add_archived_link_item && dc_get_archived_cnt(context) > 0 {
|
||||||
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
|
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
|
||||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
|
ids.push((DC_CHAT_ID_ALLDONE_HINT as u32, 0));
|
||||||
}
|
}
|
||||||
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
|
ids.push((DC_CHAT_ID_ARCHIVED_LINK as u32, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Chatlist { ids })
|
Ok(Chatlist { context, ids })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find out the number of chats.
|
/// Find out the number of chats.
|
||||||
@@ -227,7 +209,6 @@ impl Chatlist {
|
|||||||
self.ids.len()
|
self.ids.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if chatlist is empty.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.ids.is_empty()
|
self.ids.is_empty()
|
||||||
}
|
}
|
||||||
@@ -245,9 +226,12 @@ impl Chatlist {
|
|||||||
/// Get a single message ID of a chatlist.
|
/// Get a single message ID of a chatlist.
|
||||||
///
|
///
|
||||||
/// To get the message object from the message ID, use dc_get_msg().
|
/// To get the message object from the message ID, use dc_get_msg().
|
||||||
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
|
pub fn get_msg_id(&self, index: usize) -> u32 {
|
||||||
ensure!(index < self.ids.len(), "Chatlist index out of range");
|
if index >= self.ids.len() {
|
||||||
Ok(self.ids[index].1)
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ids[index].1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a summary for a chatlist index.
|
/// Get a summary for a chatlist index.
|
||||||
@@ -264,124 +248,87 @@ 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 unsafe fn get_summary(&self, index: usize, mut chat: *mut Chat<'a>) -> *mut dc_lot_t {
|
||||||
// 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".
|
||||||
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
||||||
|
|
||||||
let mut ret = Lot::new();
|
let mut ret = dc_lot_new();
|
||||||
if index >= self.ids.len() {
|
if index >= self.ids.len() {
|
||||||
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
(*ret).text2 = "ErrBadChatlistIndex".strdup();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
let chat_loaded: Chat;
|
|
||||||
let chat = if let Some(chat) = chat {
|
|
||||||
chat
|
|
||||||
} else if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0) {
|
|
||||||
chat_loaded = chat;
|
|
||||||
&chat_loaded
|
|
||||||
} else {
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastmsg_id = self.ids[index].1;
|
let lastmsg_id = self.ids[index].1;
|
||||||
let mut lastcontact = None;
|
let mut lastcontact = None;
|
||||||
|
|
||||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
|
if chat.is_null() {
|
||||||
if lastmsg.from_id != DC_CONTACT_ID_SELF
|
chat = dc_chat_new(self.context);
|
||||||
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
let chat_to_delete = chat;
|
||||||
{
|
if !dc_chat_load_from_db(chat, self.ids[index].0) {
|
||||||
lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok();
|
(*ret).text2 = "ErrCannotReadChat".strdup();
|
||||||
}
|
dc_chat_unref(chat_to_delete);
|
||||||
|
|
||||||
Some(lastmsg)
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastmsg = if 0 != lastmsg_id {
|
||||||
|
let lastmsg = dc_msg_new_untyped(self.context);
|
||||||
|
dc_msg_load_from_db(lastmsg, self.context, lastmsg_id);
|
||||||
|
|
||||||
|
if (*lastmsg).from_id != 1 as libc::c_uint
|
||||||
|
&& ((*chat).type_0 == DC_CHAT_TYPE_GROUP
|
||||||
|
|| (*chat).type_0 == DC_CHAT_TYPE_VERIFIED_GROUP)
|
||||||
|
{
|
||||||
|
lastcontact = Contact::load_from_db(self.context, (*lastmsg).from_id).ok();
|
||||||
|
}
|
||||||
|
lastmsg
|
||||||
} else {
|
} else {
|
||||||
None
|
std::ptr::null_mut()
|
||||||
};
|
};
|
||||||
|
|
||||||
if chat.id == DC_CHAT_ID_ARCHIVED_LINK {
|
if (*chat).id == DC_CHAT_ID_ARCHIVED_LINK as u32 {
|
||||||
ret.text2 = None;
|
(*ret).text2 = dc_strdup(0 as *const libc::c_char)
|
||||||
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
|
} else if lastmsg.is_null() || (*lastmsg).from_id == DC_CONTACT_ID_UNDEFINED as u32 {
|
||||||
{
|
(*ret).text2 = self.context.stock_str(StockMessage::NoMessages).strdup();
|
||||||
ret.text2 = Some(context.stock_str(StockMessage::NoMessages).to_string());
|
|
||||||
} else {
|
} else {
|
||||||
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context);
|
dc_lot_fill(ret, lastmsg, chat, lastcontact.as_ref(), self.context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dc_msg_unref(lastmsg);
|
||||||
|
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the number of archived chats
|
|
||||||
pub fn dc_get_archived_cnt(context: &Context) -> u32 {
|
pub fn dc_get_archived_cnt(context: &Context) -> u32 {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_row_col(
|
||||||
context,
|
context,
|
||||||
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
||||||
params![],
|
params![],
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
fn get_last_deaddrop_fresh_msg(context: &Context) -> u32 {
|
||||||
// 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
|
||||||
// sufficient as there are typically only few fresh messages.
|
// only few fresh messages.
|
||||||
context.sql.query_get_value(
|
context
|
||||||
context,
|
.sql
|
||||||
concat!(
|
.query_row_col(
|
||||||
"SELECT m.id",
|
context,
|
||||||
" FROM msgs m",
|
"SELECT m.id FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
|
||||||
" LEFT JOIN chats c",
|
WHERE m.state=10 \
|
||||||
" ON c.id=m.chat_id",
|
AND m.hidden=0 \
|
||||||
" WHERE m.state=10",
|
AND c.blocked=2 \
|
||||||
" AND m.hidden=0",
|
ORDER BY m.timestamp DESC, m.id DESC;",
|
||||||
" AND c.blocked=2",
|
params![],
|
||||||
" ORDER BY m.timestamp DESC, m.id DESC;"
|
0,
|
||||||
),
|
)
|
||||||
params![],
|
.unwrap_or_default()
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use crate::chat;
|
|
||||||
use crate::test_utils::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_try_load() {
|
|
||||||
let t = dummy_context();
|
|
||||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
|
|
||||||
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat").unwrap();
|
|
||||||
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat").unwrap();
|
|
||||||
|
|
||||||
// check that the chatlist starts with the most recent message
|
|
||||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
|
||||||
assert_eq!(chats.len(), 3);
|
|
||||||
assert_eq!(chats.get_chat_id(0), chat_id3);
|
|
||||||
assert_eq!(chats.get_chat_id(1), chat_id2);
|
|
||||||
assert_eq!(chats.get_chat_id(2), chat_id1);
|
|
||||||
|
|
||||||
// drafts are sorted to the top
|
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
|
||||||
msg.set_text(Some("hello".to_string()));
|
|
||||||
set_draft(&t.ctx, chat_id2, Some(&mut msg));
|
|
||||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
|
||||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
|
||||||
|
|
||||||
// check chatlist query and archive functionality
|
|
||||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None).unwrap();
|
|
||||||
assert_eq!(chats.len(), 1);
|
|
||||||
|
|
||||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
|
|
||||||
assert_eq!(chats.len(), 0);
|
|
||||||
|
|
||||||
chat::archive(&t.ctx, chat_id1, true).ok();
|
|
||||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
|
|
||||||
assert_eq!(chats.len(), 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
346
src/config.rs
346
src/config.rs
@@ -1,15 +1,11 @@
|
|||||||
//! # Key-value configuration management
|
|
||||||
|
|
||||||
use derive_deref::{Deref, DerefMut};
|
|
||||||
use strum::{EnumProperty, IntoEnumIterator};
|
use strum::{EnumProperty, IntoEnumIterator};
|
||||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||||
|
|
||||||
use crate::blob::BlobObject;
|
|
||||||
use crate::constants::DC_VERSION_STR;
|
use crate::constants::DC_VERSION_STR;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
|
use crate::dc_job::*;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::job::*;
|
use crate::error::Error;
|
||||||
use crate::sql;
|
|
||||||
use crate::stock::StockMessage;
|
use crate::stock::StockMessage;
|
||||||
|
|
||||||
/// The available configuration keys.
|
/// The available configuration keys.
|
||||||
@@ -23,243 +19,62 @@ pub enum Config {
|
|||||||
MailUser,
|
MailUser,
|
||||||
MailPw,
|
MailPw,
|
||||||
MailPort,
|
MailPort,
|
||||||
ImapCertificateChecks,
|
|
||||||
SendServer,
|
SendServer,
|
||||||
SendUser,
|
SendUser,
|
||||||
SendPw,
|
SendPw,
|
||||||
SendPort,
|
SendPort,
|
||||||
SmtpCertificateChecks,
|
|
||||||
ServerFlags,
|
ServerFlags,
|
||||||
|
|
||||||
#[strum(props(default = "INBOX"))]
|
#[strum(props(default = "INBOX"))]
|
||||||
ImapFolder,
|
ImapFolder,
|
||||||
|
|
||||||
Displayname,
|
Displayname,
|
||||||
Selfstatus,
|
Selfstatus,
|
||||||
Selfavatar,
|
Selfavatar,
|
||||||
|
|
||||||
#[strum(props(default = "0"))]
|
|
||||||
BccSelf,
|
|
||||||
|
|
||||||
#[strum(props(default = "1"))]
|
#[strum(props(default = "1"))]
|
||||||
E2eeEnabled,
|
E2eeEnabled,
|
||||||
|
|
||||||
#[strum(props(default = "1"))]
|
#[strum(props(default = "1"))]
|
||||||
MdnsEnabled,
|
MdnsEnabled,
|
||||||
|
|
||||||
#[strum(props(default = "1"))]
|
|
||||||
InboxWatch,
|
InboxWatch,
|
||||||
|
|
||||||
#[strum(props(default = "1"))]
|
#[strum(props(default = "1"))]
|
||||||
SentboxWatch,
|
SentboxWatch,
|
||||||
|
|
||||||
#[strum(props(default = "1"))]
|
#[strum(props(default = "1"))]
|
||||||
MvboxWatch,
|
MvboxWatch,
|
||||||
|
|
||||||
#[strum(props(default = "1"))]
|
#[strum(props(default = "1"))]
|
||||||
MvboxMove,
|
MvboxMove,
|
||||||
|
#[strum(props(default = "0"))]
|
||||||
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
|
||||||
ShowEmails,
|
ShowEmails,
|
||||||
|
|
||||||
SaveMimeHeaders,
|
SaveMimeHeaders,
|
||||||
ConfiguredAddr,
|
ConfiguredAddr,
|
||||||
ConfiguredMailServer,
|
ConfiguredMailServer,
|
||||||
ConfiguredMailUser,
|
ConfiguredMailUser,
|
||||||
ConfiguredMailPw,
|
ConfiguredMailPw,
|
||||||
ConfiguredMailPort,
|
ConfiguredMailPort,
|
||||||
ConfiguredMailSecurity,
|
|
||||||
ConfiguredImapCertificateChecks,
|
|
||||||
ConfiguredSendServer,
|
ConfiguredSendServer,
|
||||||
ConfiguredSendUser,
|
ConfiguredSendUser,
|
||||||
ConfiguredSendPw,
|
ConfiguredSendPw,
|
||||||
ConfiguredSendPort,
|
ConfiguredSendPort,
|
||||||
ConfiguredSmtpCertificateChecks,
|
|
||||||
ConfiguredServerFlags,
|
ConfiguredServerFlags,
|
||||||
ConfiguredSendSecurity,
|
|
||||||
ConfiguredE2EEEnabled,
|
|
||||||
Configured,
|
Configured,
|
||||||
|
// Deprecated
|
||||||
#[strum(serialize = "sys.version")]
|
#[strum(serialize = "sys.version")]
|
||||||
SysVersion,
|
SysVersion,
|
||||||
|
|
||||||
#[strum(serialize = "sys.msgsize_max_recommended")]
|
#[strum(serialize = "sys.msgsize_max_recommended")]
|
||||||
SysMsgsizeMaxRecommended,
|
SysMsgsizeMaxRecommended,
|
||||||
|
|
||||||
#[strum(serialize = "sys.config_keys")]
|
#[strum(serialize = "sys.config_keys")]
|
||||||
SysConfigKeys,
|
SysConfigKeys,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait defining a [Context]-wide configuration item.
|
|
||||||
///
|
|
||||||
/// Configuration items are stored in database of a [Context]. Most
|
|
||||||
/// configuration items are newtypes which implement [std::ops::Deref]
|
|
||||||
/// and [std::ops::DerefMut] though this is not required. However
|
|
||||||
/// what **is required** for the struct to implement
|
|
||||||
/// [rusqlite::ToSql] and [rusqlite::types::FromSql].
|
|
||||||
pub trait ConfigItem {
|
|
||||||
/// Returns the name of the key used in the SQLite database.
|
|
||||||
fn keyname() -> &'static str;
|
|
||||||
|
|
||||||
/// Loads the configuration item from the [Context]'s database.
|
|
||||||
///
|
|
||||||
/// If the configuration item is not available in the database,
|
|
||||||
/// `None` will be returned.
|
|
||||||
fn load(context: &Context) -> Result<Option<Self>, sql::Error>
|
|
||||||
where
|
|
||||||
Self: std::marker::Sized + rusqlite::types::FromSql,
|
|
||||||
{
|
|
||||||
context
|
|
||||||
.sql
|
|
||||||
.query_row(
|
|
||||||
"SELECT value FROM config WHERE keyname=?;",
|
|
||||||
params!(Self::keyname()),
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.or_else(|err| match err {
|
|
||||||
sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
|
||||||
e => Err(e),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores the configuration item in the [Context]'s database.
|
|
||||||
fn store(&self, context: &Context) -> Result<(), sql::Error>
|
|
||||||
where
|
|
||||||
Self: rusqlite::ToSql,
|
|
||||||
{
|
|
||||||
if context.sql.exists(
|
|
||||||
"select value FROM config WHERE keyname=?;",
|
|
||||||
params!(Self::keyname()),
|
|
||||||
)? {
|
|
||||||
context.sql.execute(
|
|
||||||
"UPDATE config SET value=? WHERE keyname=?",
|
|
||||||
params![&self, Self::keyname()],
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
context.sql.execute(
|
|
||||||
"INSERT INTO config (keyname, value) VALUES (?, ?)",
|
|
||||||
params![Self::keyname(), &self],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes the configuration item from the [Context]'s database.
|
|
||||||
fn delete(context: &Context) -> Result<(), sql::Error> {
|
|
||||||
context
|
|
||||||
.sql
|
|
||||||
.execute(
|
|
||||||
"DELETE FROM config WHERE keyname=?",
|
|
||||||
params![Self::keyname()],
|
|
||||||
)
|
|
||||||
.and(Ok(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration item: display address for this account.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
|
|
||||||
pub struct Addr(pub String);
|
|
||||||
|
|
||||||
impl rusqlite::ToSql for Addr {
|
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
|
||||||
self.0.to_sql()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl rusqlite::types::FromSql for Addr {
|
|
||||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
|
||||||
value.as_str().map(|v| Addr(v.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigItem for Addr {
|
|
||||||
fn keyname() -> &'static str {
|
|
||||||
"addr"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration item:
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
|
|
||||||
pub struct MailServer(pub String);
|
|
||||||
|
|
||||||
impl rusqlite::ToSql for MailServer {
|
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
|
||||||
self.0.to_sql()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl rusqlite::types::FromSql for MailServer {
|
|
||||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
|
||||||
value.as_str().map(|v| MailServer(v.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigItem for MailServer {
|
|
||||||
fn keyname() -> &'static str {
|
|
||||||
"mail_server"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration item:
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
|
|
||||||
pub struct MailUser(pub String);
|
|
||||||
// XXX TODO
|
|
||||||
|
|
||||||
/// Configuration item: whether to watch the INBOX folder for changes.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
|
|
||||||
pub struct InboxWatch(pub bool);
|
|
||||||
|
|
||||||
impl Default for InboxWatch {
|
|
||||||
fn default() -> Self {
|
|
||||||
InboxWatch(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl rusqlite::ToSql for InboxWatch {
|
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
|
||||||
// Column affinity is "text" so gets stored as string by SQLite.
|
|
||||||
let obj = rusqlite::types::Value::Integer(self.0 as i64);
|
|
||||||
Ok(rusqlite::types::ToSqlOutput::Owned(obj))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl rusqlite::types::FromSql for InboxWatch {
|
|
||||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
|
||||||
let str_to_int = |s: &str| {
|
|
||||||
s.parse::<i64>()
|
|
||||||
.map_err(|e| rusqlite::types::FromSqlError::Other(Box::new(e)))
|
|
||||||
};
|
|
||||||
let int_to_bool = |i| match i {
|
|
||||||
0 => Ok(false),
|
|
||||||
1 => Ok(true),
|
|
||||||
v => Err(rusqlite::types::FromSqlError::OutOfRange(v)),
|
|
||||||
};
|
|
||||||
value
|
|
||||||
.as_str()
|
|
||||||
.and_then(str_to_int)
|
|
||||||
.and_then(int_to_bool)
|
|
||||||
.map(InboxWatch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigItem for InboxWatch {
|
|
||||||
fn keyname() -> &'static str {
|
|
||||||
"inbox_watch"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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_config(self, key);
|
||||||
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
|
rel_path.map(|p| dc_get_abs_path_safe(self, &p).to_str().unwrap().to_string())
|
||||||
}
|
}
|
||||||
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
|
Config::SysVersion => Some(std::str::from_utf8(DC_VERSION_STR).unwrap().into()),
|
||||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", 24 * 1024 * 1024 / 4 * 3)),
|
Config::SysMsgsizeMaxRecommended => Some(format!("{}", 24 * 1024 * 1024 / 4 * 3)),
|
||||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||||
_ => self.sql.get_raw_config(self, key),
|
_ => self.sql.get_config(self, key),
|
||||||
};
|
};
|
||||||
|
|
||||||
if value.is_some() {
|
if value.is_some() {
|
||||||
@@ -273,37 +88,28 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_config_int(&self, key: Config) -> i32 {
|
|
||||||
self.get_config(key)
|
|
||||||
.and_then(|s| s.parse().ok())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_config_bool(&self, key: Config) -> bool {
|
|
||||||
self.get_config_int(key) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 fn set_config(&self, key: Config, value: Option<&str>) -> Result<(), Error> {
|
||||||
match key {
|
match key {
|
||||||
Config::Selfavatar if value.is_some() => {
|
Config::Selfavatar if value.is_some() => {
|
||||||
let blob = BlobObject::create_from_path(&self, value.unwrap())?;
|
let rel_path = std::fs::canonicalize(value.unwrap())?;
|
||||||
self.sql.set_raw_config(self, key, Some(blob.as_name()))
|
self.sql
|
||||||
|
.set_config(self, key, Some(&rel_path.to_string_lossy()))
|
||||||
}
|
}
|
||||||
Config::InboxWatch => {
|
Config::InboxWatch => {
|
||||||
let ret = self.sql.set_raw_config(self, key, value);
|
let ret = self.sql.set_config(self, key, value);
|
||||||
interrupt_inbox_idle(self, true);
|
unsafe { dc_interrupt_imap_idle(self) };
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
Config::SentboxWatch => {
|
Config::SentboxWatch => {
|
||||||
let ret = self.sql.set_raw_config(self, key, value);
|
let ret = self.sql.set_config(self, key, value);
|
||||||
interrupt_sentbox_idle(self);
|
unsafe { dc_interrupt_sentbox_idle(self) };
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
Config::MvboxWatch => {
|
Config::MvboxWatch => {
|
||||||
let ret = self.sql.set_raw_config(self, key, value);
|
let ret = self.sql.set_config(self, key, value);
|
||||||
interrupt_mvbox_idle(self);
|
unsafe { dc_interrupt_mvbox_idle(self) };
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
Config::Selfstatus => {
|
Config::Selfstatus => {
|
||||||
@@ -314,9 +120,10 @@ impl Context {
|
|||||||
value
|
value
|
||||||
};
|
};
|
||||||
|
|
||||||
self.sql.set_raw_config(self, key, val)
|
let ret = self.sql.set_config(self, key, val);
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
_ => self.sql.set_raw_config(self, key, value),
|
_ => self.sql.set_config(self, key, value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,17 +142,10 @@ fn get_config_keys_string() -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test_utils::*;
|
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref TC: TestContext = dummy_context();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_string() {
|
fn test_to_string() {
|
||||||
assert_eq!(Config::MailServer.to_string(), "mail_server");
|
assert_eq!(Config::MailServer.to_string(), "mail_server");
|
||||||
@@ -362,108 +162,4 @@ mod tests {
|
|||||||
fn test_default_prop() {
|
fn test_default_prop() {
|
||||||
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
|
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_selfavatar() -> failure::Fallible<()> {
|
|
||||||
let t = dummy_context();
|
|
||||||
let avatar_src = t.dir.path().join("avatar.jpg");
|
|
||||||
std::fs::write(&avatar_src, b"avatar")?;
|
|
||||||
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
|
|
||||||
assert!(!avatar_blob.exists());
|
|
||||||
t.ctx
|
|
||||||
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))?;
|
|
||||||
assert!(avatar_blob.exists());
|
|
||||||
assert_eq!(std::fs::read(&avatar_blob)?, b"avatar");
|
|
||||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
|
|
||||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_selfavatar_in_blobdir() -> failure::Fallible<()> {
|
|
||||||
let t = dummy_context();
|
|
||||||
let avatar_src = t.ctx.get_blobdir().join("avatar.jpg");
|
|
||||||
std::fs::write(&avatar_src, b"avatar")?;
|
|
||||||
t.ctx
|
|
||||||
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))?;
|
|
||||||
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
|
|
||||||
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_inbox_watch() {
|
|
||||||
// Loading from context when it is not in the DB.
|
|
||||||
let val = InboxWatch::load(&TC.ctx).unwrap();
|
|
||||||
assert_eq!(val, None);
|
|
||||||
|
|
||||||
// Create in-memory from default.
|
|
||||||
let mut val = InboxWatch::default();
|
|
||||||
assert_eq!(*val, true);
|
|
||||||
|
|
||||||
// Assign using deref.
|
|
||||||
*val = false;
|
|
||||||
assert_eq!(*val, false);
|
|
||||||
|
|
||||||
// Construct newtype directly.
|
|
||||||
let val = InboxWatch(false);
|
|
||||||
assert_eq!(*val, false);
|
|
||||||
|
|
||||||
// Helper to query raw DB value.
|
|
||||||
let query_db_raw = || {
|
|
||||||
TC.ctx
|
|
||||||
.sql
|
|
||||||
.query_row(
|
|
||||||
"SELECT value FROM config WHERE KEYNAME=?",
|
|
||||||
params![InboxWatch::keyname()],
|
|
||||||
|row| row.get::<_, String>(0),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save (non-default) value to the DB.
|
|
||||||
InboxWatch(false).store(&TC.ctx).unwrap();
|
|
||||||
assert_eq!(query_db_raw(), "0");
|
|
||||||
let val = InboxWatch::load(&TC.ctx).unwrap().unwrap();
|
|
||||||
assert_eq!(val, InboxWatch(false));
|
|
||||||
|
|
||||||
// Save true (aka default) value to the DB.
|
|
||||||
InboxWatch(true).store(&TC.ctx).unwrap();
|
|
||||||
assert_eq!(query_db_raw(), "1");
|
|
||||||
let val = InboxWatch::load(&TC.ctx).unwrap().unwrap();
|
|
||||||
assert_eq!(val, InboxWatch(true));
|
|
||||||
|
|
||||||
// Delete the value from the DB.
|
|
||||||
InboxWatch::delete(&TC.ctx).unwrap();
|
|
||||||
assert!(!TC
|
|
||||||
.ctx
|
|
||||||
.sql
|
|
||||||
.exists(
|
|
||||||
"SELECT value FROM config WHERE KEYNAME=?",
|
|
||||||
params![InboxWatch::keyname()],
|
|
||||||
)
|
|
||||||
.unwrap());
|
|
||||||
let val = InboxWatch::load(&TC.ctx).unwrap();
|
|
||||||
assert_eq!(val, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_addr() {
|
|
||||||
// In-memory creation
|
|
||||||
let val = Addr("me@example.com".into());
|
|
||||||
assert_eq!(*val, "me@example.com");
|
|
||||||
|
|
||||||
// Load when DB is empty.
|
|
||||||
let val = Addr::load(&TC.ctx).unwrap();
|
|
||||||
assert_eq!(val, None);
|
|
||||||
|
|
||||||
// Store and load.
|
|
||||||
Addr("me@example.com".into()).store(&TC.ctx).unwrap();
|
|
||||||
let val = Addr::load(&TC.ctx).unwrap();
|
|
||||||
assert_eq!(val, Some(Addr("me@example.com".into())));
|
|
||||||
|
|
||||||
// Delete
|
|
||||||
Addr::delete(&TC.ctx).unwrap();
|
|
||||||
assert_eq!(Addr::load(&TC.ctx).unwrap(), None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,349 +0,0 @@
|
|||||||
//! # Thunderbird's Autoconfiguration implementation
|
|
||||||
//!
|
|
||||||
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
|
|
||||||
use quick_xml;
|
|
||||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
|
||||||
|
|
||||||
use crate::constants::*;
|
|
||||||
use crate::context::Context;
|
|
||||||
use crate::login_param::LoginParam;
|
|
||||||
|
|
||||||
use super::read_url::read_url;
|
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum Error {
|
|
||||||
#[fail(display = "Invalid email address: {:?}", _0)]
|
|
||||||
InvalidEmailAddress(String),
|
|
||||||
|
|
||||||
#[fail(display = "XML error at position {}", position)]
|
|
||||||
InvalidXml {
|
|
||||||
position: usize,
|
|
||||||
#[cause]
|
|
||||||
error: quick_xml::Error,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[fail(display = "Bad or incomplete autoconfig")]
|
|
||||||
IncompleteAutoconfig(LoginParam),
|
|
||||||
|
|
||||||
#[fail(display = "Failed to get URL {}", _0)]
|
|
||||||
ReadUrlError(#[cause] super::read_url::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
impl From<super::read_url::Error> for Error {
|
|
||||||
fn from(err: super::read_url::Error) -> Error {
|
|
||||||
Error::ReadUrlError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct MozAutoconfigure<'a> {
|
|
||||||
pub in_emailaddr: &'a str,
|
|
||||||
pub in_emaildomain: &'a str,
|
|
||||||
pub in_emaillocalpart: &'a str,
|
|
||||||
pub out: LoginParam,
|
|
||||||
pub out_imap_set: bool,
|
|
||||||
pub out_smtp_set: bool,
|
|
||||||
pub tag_server: MozServer,
|
|
||||||
pub tag_config: MozConfigTag,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
enum MozServer {
|
|
||||||
Undefined,
|
|
||||||
Imap,
|
|
||||||
Smtp,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum MozConfigTag {
|
|
||||||
Undefined,
|
|
||||||
Hostname,
|
|
||||||
Port,
|
|
||||||
Sockettype,
|
|
||||||
Username,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
|
|
||||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
|
||||||
reader.trim_text(true);
|
|
||||||
|
|
||||||
// Split address into local part and domain part.
|
|
||||||
let p = in_emailaddr
|
|
||||||
.find('@')
|
|
||||||
.ok_or_else(|| Error::InvalidEmailAddress(in_emailaddr.to_string()))?;
|
|
||||||
let (in_emaillocalpart, in_emaildomain) = in_emailaddr.split_at(p);
|
|
||||||
let in_emaildomain = &in_emaildomain[1..];
|
|
||||||
|
|
||||||
let mut moz_ac = MozAutoconfigure {
|
|
||||||
in_emailaddr,
|
|
||||||
in_emaildomain,
|
|
||||||
in_emaillocalpart,
|
|
||||||
out: LoginParam::new(),
|
|
||||||
out_imap_set: false,
|
|
||||||
out_smtp_set: false,
|
|
||||||
tag_server: MozServer::Undefined,
|
|
||||||
tag_config: MozConfigTag::Undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
loop {
|
|
||||||
let event = reader
|
|
||||||
.read_event(&mut buf)
|
|
||||||
.map_err(|error| Error::InvalidXml {
|
|
||||||
position: reader.buffer_position(),
|
|
||||||
error,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
quick_xml::events::Event::Start(ref e) => {
|
|
||||||
moz_autoconfigure_starttag_cb(e, &mut moz_ac, &reader)
|
|
||||||
}
|
|
||||||
quick_xml::events::Event::End(ref e) => moz_autoconfigure_endtag_cb(e, &mut moz_ac),
|
|
||||||
quick_xml::events::Event::Text(ref e) => {
|
|
||||||
moz_autoconfigure_text_cb(e, &mut moz_ac, &reader)
|
|
||||||
}
|
|
||||||
quick_xml::events::Event::Eof => break,
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
buf.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if moz_ac.out.mail_server.is_empty()
|
|
||||||
|| moz_ac.out.mail_port == 0
|
|
||||||
|| moz_ac.out.send_server.is_empty()
|
|
||||||
|| moz_ac.out.send_port == 0
|
|
||||||
{
|
|
||||||
Err(Error::IncompleteAutoconfig(moz_ac.out))
|
|
||||||
} else {
|
|
||||||
Ok(moz_ac.out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn moz_autoconfigure(
|
|
||||||
context: &Context,
|
|
||||||
url: &str,
|
|
||||||
param_in: &LoginParam,
|
|
||||||
) -> Result<LoginParam> {
|
|
||||||
let xml_raw = read_url(context, url)?;
|
|
||||||
|
|
||||||
let res = parse_xml(¶m_in.addr, &xml_raw);
|
|
||||||
if let Err(err) = &res {
|
|
||||||
warn!(
|
|
||||||
context,
|
|
||||||
"Failed to parse Thunderbird autoconfiguration XML: {}", err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
|
|
||||||
event: &BytesText,
|
|
||||||
moz_ac: &mut MozAutoconfigure,
|
|
||||||
reader: &quick_xml::Reader<B>,
|
|
||||||
) {
|
|
||||||
let val = event.unescape_and_decode(reader).unwrap_or_default();
|
|
||||||
|
|
||||||
let addr = moz_ac.in_emailaddr;
|
|
||||||
let email_local = moz_ac.in_emaillocalpart;
|
|
||||||
let email_domain = moz_ac.in_emaildomain;
|
|
||||||
|
|
||||||
let val = val
|
|
||||||
.trim()
|
|
||||||
.replace("%EMAILADDRESS%", addr)
|
|
||||||
.replace("%EMAILLOCALPART%", email_local)
|
|
||||||
.replace("%EMAILDOMAIN%", email_domain);
|
|
||||||
|
|
||||||
match moz_ac.tag_server {
|
|
||||||
MozServer::Imap => match moz_ac.tag_config {
|
|
||||||
MozConfigTag::Hostname => moz_ac.out.mail_server = val,
|
|
||||||
MozConfigTag::Port => moz_ac.out.mail_port = val.parse().unwrap_or_default(),
|
|
||||||
MozConfigTag::Username => moz_ac.out.mail_user = val,
|
|
||||||
MozConfigTag::Sockettype => {
|
|
||||||
let val_lower = val.to_lowercase();
|
|
||||||
if val_lower == "ssl" {
|
|
||||||
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
|
|
||||||
}
|
|
||||||
if val_lower == "starttls" {
|
|
||||||
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32
|
|
||||||
}
|
|
||||||
if val_lower == "plain" {
|
|
||||||
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
MozServer::Smtp => match moz_ac.tag_config {
|
|
||||||
MozConfigTag::Hostname => moz_ac.out.send_server = val,
|
|
||||||
MozConfigTag::Port => moz_ac.out.send_port = val.parse().unwrap_or_default(),
|
|
||||||
MozConfigTag::Username => moz_ac.out.send_user = val,
|
|
||||||
MozConfigTag::Sockettype => {
|
|
||||||
let val_lower = val.to_lowercase();
|
|
||||||
if val_lower == "ssl" {
|
|
||||||
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
|
|
||||||
}
|
|
||||||
if val_lower == "starttls" {
|
|
||||||
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32
|
|
||||||
}
|
|
||||||
if val_lower == "plain" {
|
|
||||||
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
MozServer::Undefined => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn moz_autoconfigure_endtag_cb(event: &BytesEnd, moz_ac: &mut MozAutoconfigure) {
|
|
||||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
|
||||||
|
|
||||||
if tag == "incomingserver" {
|
|
||||||
if moz_ac.tag_server == MozServer::Imap {
|
|
||||||
moz_ac.out_imap_set = true;
|
|
||||||
}
|
|
||||||
moz_ac.tag_server = MozServer::Undefined;
|
|
||||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
|
||||||
} else if tag == "outgoingserver" {
|
|
||||||
if moz_ac.tag_server == MozServer::Smtp {
|
|
||||||
moz_ac.out_smtp_set = true;
|
|
||||||
}
|
|
||||||
moz_ac.tag_server = MozServer::Undefined;
|
|
||||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
|
||||||
} else {
|
|
||||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn moz_autoconfigure_starttag_cb<B: std::io::BufRead>(
|
|
||||||
event: &BytesStart,
|
|
||||||
moz_ac: &mut MozAutoconfigure,
|
|
||||||
reader: &quick_xml::Reader<B>,
|
|
||||||
) {
|
|
||||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
|
||||||
|
|
||||||
if tag == "incomingserver" {
|
|
||||||
moz_ac.tag_server = if let Some(typ) = event.attributes().find(|attr| {
|
|
||||||
attr.as_ref()
|
|
||||||
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type")
|
|
||||||
.unwrap_or_default()
|
|
||||||
}) {
|
|
||||||
let typ = typ
|
|
||||||
.unwrap()
|
|
||||||
.unescape_and_decode_value(reader)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_lowercase();
|
|
||||||
|
|
||||||
if typ == "imap" && !moz_ac.out_imap_set {
|
|
||||||
MozServer::Imap
|
|
||||||
} else {
|
|
||||||
MozServer::Undefined
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MozServer::Undefined
|
|
||||||
};
|
|
||||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
|
||||||
} else if tag == "outgoingserver" {
|
|
||||||
moz_ac.tag_server = if !moz_ac.out_smtp_set {
|
|
||||||
MozServer::Smtp
|
|
||||||
} else {
|
|
||||||
MozServer::Undefined
|
|
||||||
};
|
|
||||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
|
||||||
} else if tag == "hostname" {
|
|
||||||
moz_ac.tag_config = MozConfigTag::Hostname;
|
|
||||||
} else if tag == "port" {
|
|
||||||
moz_ac.tag_config = MozConfigTag::Port;
|
|
||||||
} else if tag == "sockettype" {
|
|
||||||
moz_ac.tag_config = MozConfigTag::Sockettype;
|
|
||||||
} else if tag == "username" {
|
|
||||||
moz_ac.tag_config = MozConfigTag::Username;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_outlook_autoconfig() {
|
|
||||||
// Copied from https://autoconfig.thunderbird.net/v1.1/outlook.com on 2019-10-11
|
|
||||||
let xml_raw =
|
|
||||||
"<clientConfig version=\"1.1\">
|
|
||||||
<emailProvider id=\"outlook.com\">
|
|
||||||
<domain>hotmail.com</domain>
|
|
||||||
<domain>hotmail.co.uk</domain>
|
|
||||||
<domain>hotmail.co.jp</domain>
|
|
||||||
<domain>hotmail.com.br</domain>
|
|
||||||
<domain>hotmail.de</domain>
|
|
||||||
<domain>hotmail.fr</domain>
|
|
||||||
<domain>hotmail.it</domain>
|
|
||||||
<domain>hotmail.es</domain>
|
|
||||||
<domain>live.com</domain>
|
|
||||||
<domain>live.co.uk</domain>
|
|
||||||
<domain>live.co.jp</domain>
|
|
||||||
<domain>live.de</domain>
|
|
||||||
<domain>live.fr</domain>
|
|
||||||
<domain>live.it</domain>
|
|
||||||
<domain>live.jp</domain>
|
|
||||||
<domain>msn.com</domain>
|
|
||||||
<domain>outlook.com</domain>
|
|
||||||
<displayName>Outlook.com (Microsoft)</displayName>
|
|
||||||
<displayShortName>Outlook</displayShortName>
|
|
||||||
<incomingServer type=\"exchange\">
|
|
||||||
<hostname>outlook.office365.com</hostname>
|
|
||||||
<port>443</port>
|
|
||||||
<username>%EMAILADDRESS%</username>
|
|
||||||
<socketType>SSL</socketType>
|
|
||||||
<authentication>OAuth2</authentication>
|
|
||||||
<owaURL>https://outlook.office365.com/owa/</owaURL>
|
|
||||||
<ewsURL>https://outlook.office365.com/ews/exchange.asmx</ewsURL>
|
|
||||||
<useGlobalPreferredServer>true</useGlobalPreferredServer>
|
|
||||||
</incomingServer>
|
|
||||||
<incomingServer type=\"imap\">
|
|
||||||
<hostname>outlook.office365.com</hostname>
|
|
||||||
<port>993</port>
|
|
||||||
<socketType>SSL</socketType>
|
|
||||||
<authentication>password-cleartext</authentication>
|
|
||||||
<username>%EMAILADDRESS%</username>
|
|
||||||
</incomingServer>
|
|
||||||
<incomingServer type=\"pop3\">
|
|
||||||
<hostname>outlook.office365.com</hostname>
|
|
||||||
<port>995</port>
|
|
||||||
<socketType>SSL</socketType>
|
|
||||||
<authentication>password-cleartext</authentication>
|
|
||||||
<username>%EMAILADDRESS%</username>
|
|
||||||
<pop3>
|
|
||||||
<leaveMessagesOnServer>true</leaveMessagesOnServer>
|
|
||||||
<!-- Outlook.com docs specifically mention that POP3 deletes have effect on the main inbox on webmail and IMAP -->
|
|
||||||
</pop3>
|
|
||||||
</incomingServer>
|
|
||||||
<outgoingServer type=\"smtp\">
|
|
||||||
<hostname>smtp.office365.com</hostname>
|
|
||||||
<port>587</port>
|
|
||||||
<socketType>STARTTLS</socketType>
|
|
||||||
<authentication>password-cleartext</authentication>
|
|
||||||
<username>%EMAILADDRESS%</username>
|
|
||||||
</outgoingServer>
|
|
||||||
<documentation url=\"http://windows.microsoft.com/en-US/windows/outlook/send-receive-from-app\">
|
|
||||||
<descr lang=\"en\">Set up an email app with Outlook.com</descr>
|
|
||||||
</documentation>
|
|
||||||
</emailProvider>
|
|
||||||
<webMail>
|
|
||||||
<loginPage url=\"https://www.outlook.com/\"/>
|
|
||||||
<loginPageInfo url=\"https://www.outlook.com/\">
|
|
||||||
<username>%EMAILADDRESS%</username>
|
|
||||||
<usernameField id=\"i0116\" name=\"login\"/>
|
|
||||||
<passwordField id=\"i0118\" name=\"passwd\"/>
|
|
||||||
<loginButton id=\"idSIButton9\" name=\"SI\"/>
|
|
||||||
</loginPageInfo>
|
|
||||||
</webMail>
|
|
||||||
</clientConfig>";
|
|
||||||
let res = parse_xml("example@outlook.com", xml_raw).expect("XML parsing failed");
|
|
||||||
assert_eq!(res.mail_server, "outlook.office365.com");
|
|
||||||
assert_eq!(res.mail_port, 993);
|
|
||||||
assert_eq!(res.send_server, "smtp.office365.com");
|
|
||||||
assert_eq!(res.send_port, 587);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
//! Outlook's Autodiscover
|
|
||||||
|
|
||||||
use quick_xml;
|
|
||||||
use quick_xml::events::BytesEnd;
|
|
||||||
|
|
||||||
use crate::constants::*;
|
|
||||||
use crate::context::Context;
|
|
||||||
use crate::login_param::LoginParam;
|
|
||||||
|
|
||||||
use super::read_url::read_url;
|
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum Error {
|
|
||||||
#[fail(display = "XML error at position {}", position)]
|
|
||||||
InvalidXml {
|
|
||||||
position: usize,
|
|
||||||
#[cause]
|
|
||||||
error: quick_xml::Error,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[fail(display = "Bad or incomplete autoconfig")]
|
|
||||||
IncompleteAutoconfig(LoginParam),
|
|
||||||
|
|
||||||
#[fail(display = "Failed to get URL {}", _0)]
|
|
||||||
ReadUrlError(#[cause] super::read_url::Error),
|
|
||||||
|
|
||||||
#[fail(display = "Number of redirection is exceeded")]
|
|
||||||
RedirectionError,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
impl From<super::read_url::Error> for Error {
|
|
||||||
fn from(err: super::read_url::Error) -> Error {
|
|
||||||
Error::ReadUrlError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OutlookAutodiscover {
|
|
||||||
pub out: LoginParam,
|
|
||||||
pub out_imap_set: bool,
|
|
||||||
pub out_smtp_set: bool,
|
|
||||||
pub config_type: Option<String>,
|
|
||||||
pub config_server: String,
|
|
||||||
pub config_port: i32,
|
|
||||||
pub config_ssl: String,
|
|
||||||
pub config_redirecturl: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ParsingResult {
|
|
||||||
LoginParam(LoginParam),
|
|
||||||
RedirectUrl(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult> {
|
|
||||||
let mut outlk_ad = OutlookAutodiscover {
|
|
||||||
out: LoginParam::new(),
|
|
||||||
out_imap_set: false,
|
|
||||||
out_smtp_set: false,
|
|
||||||
config_type: None,
|
|
||||||
config_server: String::new(),
|
|
||||||
config_port: 0,
|
|
||||||
config_ssl: String::new(),
|
|
||||||
config_redirecturl: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut reader = quick_xml::Reader::from_str(&xml_raw);
|
|
||||||
reader.trim_text(true);
|
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
|
|
||||||
let mut current_tag: Option<String> = None;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let event = reader
|
|
||||||
.read_event(&mut buf)
|
|
||||||
.map_err(|error| Error::InvalidXml {
|
|
||||||
position: reader.buffer_position(),
|
|
||||||
error,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
quick_xml::events::Event::Start(ref e) => {
|
|
||||||
let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase();
|
|
||||||
|
|
||||||
if tag == "protocol" {
|
|
||||||
outlk_ad.config_type = None;
|
|
||||||
outlk_ad.config_server = String::new();
|
|
||||||
outlk_ad.config_port = 0;
|
|
||||||
outlk_ad.config_ssl = String::new();
|
|
||||||
outlk_ad.config_redirecturl = None;
|
|
||||||
|
|
||||||
current_tag = None;
|
|
||||||
} else {
|
|
||||||
current_tag = Some(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
quick_xml::events::Event::End(ref e) => {
|
|
||||||
outlk_autodiscover_endtag_cb(e, &mut outlk_ad);
|
|
||||||
current_tag = None;
|
|
||||||
}
|
|
||||||
quick_xml::events::Event::Text(ref e) => {
|
|
||||||
let val = e.unescape_and_decode(&reader).unwrap_or_default();
|
|
||||||
|
|
||||||
if let Some(ref tag) = current_tag {
|
|
||||||
match tag.as_str() {
|
|
||||||
"type" => {
|
|
||||||
outlk_ad.config_type = Some(val.trim().to_lowercase().to_string())
|
|
||||||
}
|
|
||||||
"server" => outlk_ad.config_server = val.trim().to_string(),
|
|
||||||
"port" => outlk_ad.config_port = val.trim().parse().unwrap_or_default(),
|
|
||||||
"ssl" => outlk_ad.config_ssl = val.trim().to_string(),
|
|
||||||
"redirecturl" => outlk_ad.config_redirecturl = Some(val.trim().to_string()),
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
quick_xml::events::Event::Eof => break,
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
buf.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// XML redirect via redirecturl
|
|
||||||
let res = if outlk_ad.config_redirecturl.is_none()
|
|
||||||
|| outlk_ad.config_redirecturl.as_ref().unwrap().is_empty()
|
|
||||||
{
|
|
||||||
if outlk_ad.out.mail_server.is_empty()
|
|
||||||
|| outlk_ad.out.mail_port == 0
|
|
||||||
|| outlk_ad.out.send_server.is_empty()
|
|
||||||
|| outlk_ad.out.send_port == 0
|
|
||||||
{
|
|
||||||
return Err(Error::IncompleteAutoconfig(outlk_ad.out));
|
|
||||||
}
|
|
||||||
ParsingResult::LoginParam(outlk_ad.out)
|
|
||||||
} else {
|
|
||||||
ParsingResult::RedirectUrl(outlk_ad.config_redirecturl.unwrap())
|
|
||||||
};
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn outlk_autodiscover(
|
|
||||||
context: &Context,
|
|
||||||
url: &str,
|
|
||||||
_param_in: &LoginParam,
|
|
||||||
) -> Result<LoginParam> {
|
|
||||||
let mut url = url.to_string();
|
|
||||||
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
|
|
||||||
for _i in 0..10 {
|
|
||||||
let xml_raw = read_url(context, &url)?;
|
|
||||||
let res = parse_xml(&xml_raw);
|
|
||||||
if let Err(err) = &res {
|
|
||||||
warn!(context, "{}", err);
|
|
||||||
}
|
|
||||||
match res? {
|
|
||||||
ParsingResult::RedirectUrl(redirect_url) => url = redirect_url,
|
|
||||||
ParsingResult::LoginParam(login_param) => return Ok(login_param),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(Error::RedirectionError)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn outlk_autodiscover_endtag_cb(event: &BytesEnd, outlk_ad: &mut OutlookAutodiscover) {
|
|
||||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
|
||||||
|
|
||||||
if tag == "protocol" {
|
|
||||||
if let Some(type_) = &outlk_ad.config_type {
|
|
||||||
let port = outlk_ad.config_port;
|
|
||||||
let ssl_on = outlk_ad.config_ssl == "on";
|
|
||||||
let ssl_off = outlk_ad.config_ssl == "off";
|
|
||||||
if type_ == "imap" && !outlk_ad.out_imap_set {
|
|
||||||
outlk_ad.out.mail_server =
|
|
||||||
std::mem::replace(&mut outlk_ad.config_server, String::new());
|
|
||||||
outlk_ad.out.mail_port = port;
|
|
||||||
if ssl_on {
|
|
||||||
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
|
|
||||||
} else if ssl_off {
|
|
||||||
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
|
|
||||||
}
|
|
||||||
outlk_ad.out_imap_set = true
|
|
||||||
} else if type_ == "smtp" && !outlk_ad.out_smtp_set {
|
|
||||||
outlk_ad.out.send_server =
|
|
||||||
std::mem::replace(&mut outlk_ad.config_server, String::new());
|
|
||||||
outlk_ad.out.send_port = outlk_ad.config_port;
|
|
||||||
if ssl_on {
|
|
||||||
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
|
|
||||||
} else if ssl_off {
|
|
||||||
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
|
|
||||||
}
|
|
||||||
outlk_ad.out_smtp_set = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_redirect() {
|
|
||||||
let res = parse_xml("
|
|
||||||
<?xml version=\"1.0\" encoding=\"utf-8\"?>
|
|
||||||
<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006\">
|
|
||||||
<Response xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a\">
|
|
||||||
<Account>
|
|
||||||
<AccountType>email</AccountType>
|
|
||||||
<Action>redirectUrl</Action>
|
|
||||||
<RedirectUrl>https://mail.example.com/autodiscover/autodiscover.xml</RedirectUrl>
|
|
||||||
</Account>
|
|
||||||
</Response>
|
|
||||||
</Autodiscover>
|
|
||||||
").expect("XML is not parsed successfully");
|
|
||||||
match res {
|
|
||||||
ParsingResult::LoginParam(_lp) => {
|
|
||||||
panic!("redirecturl is not found");
|
|
||||||
}
|
|
||||||
ParsingResult::RedirectUrl(url) => {
|
|
||||||
assert_eq!(
|
|
||||||
url,
|
|
||||||
"https://mail.example.com/autodiscover/autodiscover.xml"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_loginparam() {
|
|
||||||
let res = parse_xml(
|
|
||||||
"\
|
|
||||||
<?xml version=\"1.0\" encoding=\"utf-8\"?>
|
|
||||||
<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006\">
|
|
||||||
<Response xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a\">
|
|
||||||
<Account>
|
|
||||||
<AccountType>email</AccountType>
|
|
||||||
<Action>settings</Action>
|
|
||||||
<Protocol>
|
|
||||||
<Type>IMAP</Type>
|
|
||||||
<Server>example.com</Server>
|
|
||||||
<Port>993</Port>
|
|
||||||
<SSL>on</SSL>
|
|
||||||
<AuthRequired>on</AuthRequired>
|
|
||||||
</Protocol>
|
|
||||||
<Protocol>
|
|
||||||
<Type>SMTP</Type>
|
|
||||||
<Server>smtp.example.com</Server>
|
|
||||||
<Port>25</Port>
|
|
||||||
<SSL>off</SSL>
|
|
||||||
<AuthRequired>on</AuthRequired>
|
|
||||||
</Protocol>
|
|
||||||
</Account>
|
|
||||||
</Response>
|
|
||||||
</Autodiscover>",
|
|
||||||
)
|
|
||||||
.expect("XML is not parsed successfully");
|
|
||||||
|
|
||||||
match res {
|
|
||||||
ParsingResult::LoginParam(lp) => {
|
|
||||||
assert_eq!(lp.mail_server, "example.com");
|
|
||||||
assert_eq!(lp.mail_port, 993);
|
|
||||||
assert_eq!(lp.send_server, "smtp.example.com");
|
|
||||||
assert_eq!(lp.send_port, 25);
|
|
||||||
}
|
|
||||||
ParsingResult::RedirectUrl(_) => {
|
|
||||||
panic!("RedirectUrl is not expected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,583 +0,0 @@
|
|||||||
//! Email accounts autoconfiguration process module
|
|
||||||
|
|
||||||
mod auto_mozilla;
|
|
||||||
mod auto_outlook;
|
|
||||||
mod read_url;
|
|
||||||
|
|
||||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
|
||||||
|
|
||||||
use crate::config::Config;
|
|
||||||
use crate::constants::*;
|
|
||||||
use crate::context::Context;
|
|
||||||
use crate::dc_tools::*;
|
|
||||||
use crate::e2ee;
|
|
||||||
use crate::job::*;
|
|
||||||
use crate::login_param::LoginParam;
|
|
||||||
use crate::oauth2::*;
|
|
||||||
use crate::param::Params;
|
|
||||||
|
|
||||||
use auto_mozilla::moz_autoconfigure;
|
|
||||||
use auto_outlook::outlk_autodiscover;
|
|
||||||
|
|
||||||
macro_rules! progress {
|
|
||||||
($context:tt, $progress:expr) => {
|
|
||||||
assert!(
|
|
||||||
$progress <= 1000,
|
|
||||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
|
||||||
);
|
|
||||||
$context.call_cb($crate::events::Event::ConfigureProgress($progress));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect
|
|
||||||
pub fn configure(context: &Context) {
|
|
||||||
if context.has_ongoing() {
|
|
||||||
warn!(context, "There is already another ongoing process running.",);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
job_kill_action(context, Action::ConfigureImap);
|
|
||||||
job_add(context, Action::ConfigureImap, 0, Params::new(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the context is already configured.
|
|
||||||
pub fn dc_is_configured(context: &Context) -> bool {
|
|
||||||
context.sql.get_raw_config_bool(context, "configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
* Configure JOB
|
|
||||||
******************************************************************************/
|
|
||||||
#[allow(non_snake_case, unused_must_use)]
|
|
||||||
pub fn JobConfigureImap(context: &Context) {
|
|
||||||
if !context.sql.is_open() {
|
|
||||||
error!(context, "Cannot configure, database not opened.",);
|
|
||||||
progress!(context, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !context.alloc_ongoing() {
|
|
||||||
progress!(context, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut success = false;
|
|
||||||
let mut imap_connected_here = false;
|
|
||||||
let mut smtp_connected_here = false;
|
|
||||||
|
|
||||||
let mut param_autoconfig: Option<LoginParam> = None;
|
|
||||||
|
|
||||||
context
|
|
||||||
.inbox_thread
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.imap
|
|
||||||
.disconnect(context);
|
|
||||||
context
|
|
||||||
.sentbox_thread
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.imap
|
|
||||||
.disconnect(context);
|
|
||||||
context
|
|
||||||
.mvbox_thread
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.imap
|
|
||||||
.disconnect(context);
|
|
||||||
context.smtp.clone().lock().unwrap().disconnect();
|
|
||||||
info!(context, "Configure ...",);
|
|
||||||
|
|
||||||
// Variables that are shared between steps:
|
|
||||||
let mut param = LoginParam::from_database(context, "");
|
|
||||||
// need all vars here to be mutable because rust thinks the same step could be called multiple times
|
|
||||||
// and also initialize, because otherwise rust thinks it's used while unitilized, even if thats not the case as the loop goes only forward
|
|
||||||
let mut param_domain = "undefined.undefined".to_owned();
|
|
||||||
let mut param_addr_urlencoded: String =
|
|
||||||
"Internal Error: this value should never be used".to_owned();
|
|
||||||
let mut keep_flags = std::i32::MAX;
|
|
||||||
|
|
||||||
const STEP_3_INDEX: u8 = 13;
|
|
||||||
let mut step_counter: u8 = 0;
|
|
||||||
while !context.shall_stop_ongoing() {
|
|
||||||
step_counter += 1;
|
|
||||||
|
|
||||||
let success = match step_counter {
|
|
||||||
// Read login parameters from the database
|
|
||||||
1 => {
|
|
||||||
progress!(context, 1);
|
|
||||||
if param.addr.is_empty() {
|
|
||||||
error!(context, "Please enter an email address.",);
|
|
||||||
}
|
|
||||||
!param.addr.is_empty()
|
|
||||||
}
|
|
||||||
// Step 1: Load the parameters and check email-address and password
|
|
||||||
2 => {
|
|
||||||
if 0 != param.server_flags & 0x2 {
|
|
||||||
// the used oauth2 addr may differ, check this.
|
|
||||||
// if dc_get_oauth2_addr() is not available in the oauth2 implementation,
|
|
||||||
// just use the given one.
|
|
||||||
progress!(context, 10);
|
|
||||||
if let Some(oauth2_addr) =
|
|
||||||
dc_get_oauth2_addr(context, ¶m.addr, ¶m.mail_pw)
|
|
||||||
.and_then(|e| e.parse().ok())
|
|
||||||
{
|
|
||||||
info!(context, "Authorized address is {}", oauth2_addr);
|
|
||||||
param.addr = oauth2_addr;
|
|
||||||
context
|
|
||||||
.sql
|
|
||||||
.set_raw_config(context, "addr", Some(param.addr.as_str()))
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
progress!(context, 20);
|
|
||||||
}
|
|
||||||
true // no oauth? - just continue it's no error
|
|
||||||
}
|
|
||||||
3 => {
|
|
||||||
if let Ok(parsed) = param.addr.parse() {
|
|
||||||
let parsed: EmailAddress = parsed;
|
|
||||||
param_domain = parsed.domain;
|
|
||||||
param_addr_urlencoded =
|
|
||||||
utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string();
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
error!(context, "Bad email-address.");
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Step 2: Autoconfig
|
|
||||||
4 => {
|
|
||||||
progress!(context, 200);
|
|
||||||
if param.mail_server.is_empty()
|
|
||||||
&& param.mail_port == 0
|
|
||||||
/*&¶m.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then */
|
|
||||||
&& param.send_server.is_empty()
|
|
||||||
&& param.send_port == 0
|
|
||||||
&& param.send_user.is_empty()
|
|
||||||
/*&¶m.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for autoconfig or not */
|
|
||||||
&& param.server_flags & !0x2 == 0
|
|
||||||
{
|
|
||||||
keep_flags = param.server_flags & 0x2;
|
|
||||||
} else {
|
|
||||||
// Autoconfig is not needed so skip it.
|
|
||||||
step_counter = STEP_3_INDEX - 1;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
/* A. Search configurations from the domain used in the email-address, prefer encrypted */
|
|
||||||
5 => {
|
|
||||||
if param_autoconfig.is_none() {
|
|
||||||
let url = format!(
|
|
||||||
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
|
|
||||||
param_domain, param_addr_urlencoded
|
|
||||||
);
|
|
||||||
param_autoconfig = moz_autoconfigure(context, &url, ¶m).ok();
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
6 => {
|
|
||||||
progress!(context, 300);
|
|
||||||
if param_autoconfig.is_none() {
|
|
||||||
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
|
|
||||||
let url = format!(
|
|
||||||
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
|
|
||||||
param_domain, param_addr_urlencoded
|
|
||||||
);
|
|
||||||
param_autoconfig = moz_autoconfigure(context, &url, ¶m).ok();
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
/* Outlook section start ------------- */
|
|
||||||
/* Outlook uses always SSL but different domains (this comment describes the next two steps) */
|
|
||||||
7 => {
|
|
||||||
progress!(context, 310);
|
|
||||||
if param_autoconfig.is_none() {
|
|
||||||
let url = format!(
|
|
||||||
"https://{}{}/autodiscover/autodiscover.xml",
|
|
||||||
"", param_domain
|
|
||||||
);
|
|
||||||
param_autoconfig = outlk_autodiscover(context, &url, ¶m).ok();
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
8 => {
|
|
||||||
progress!(context, 320);
|
|
||||||
if param_autoconfig.is_none() {
|
|
||||||
let url = format!(
|
|
||||||
"https://{}{}/autodiscover/autodiscover.xml",
|
|
||||||
"autodiscover.", param_domain
|
|
||||||
);
|
|
||||||
param_autoconfig = outlk_autodiscover(context, &url, ¶m).ok();
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
/* ----------- Outlook section end */
|
|
||||||
9 => {
|
|
||||||
progress!(context, 330);
|
|
||||||
if param_autoconfig.is_none() {
|
|
||||||
let url = format!(
|
|
||||||
"http://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
|
|
||||||
param_domain, param_addr_urlencoded
|
|
||||||
);
|
|
||||||
param_autoconfig = moz_autoconfigure(context, &url, ¶m).ok();
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
10 => {
|
|
||||||
progress!(context, 340);
|
|
||||||
if param_autoconfig.is_none() {
|
|
||||||
// do not transfer the email-address unencrypted
|
|
||||||
let url = format!(
|
|
||||||
"http://{}/.well-known/autoconfig/mail/config-v1.1.xml",
|
|
||||||
param_domain
|
|
||||||
);
|
|
||||||
param_autoconfig = moz_autoconfigure(context, &url, ¶m).ok();
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
/* B. If we have no configuration yet, search configuration in Thunderbird's centeral database */
|
|
||||||
11 => {
|
|
||||||
progress!(context, 350);
|
|
||||||
if param_autoconfig.is_none() {
|
|
||||||
/* always SSL for Thunderbird's database */
|
|
||||||
let url = format!("https://autoconfig.thunderbird.net/v1.1/{}", param_domain);
|
|
||||||
param_autoconfig = moz_autoconfigure(context, &url, ¶m).ok();
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
/* C. Do we have any result? */
|
|
||||||
12 => {
|
|
||||||
progress!(context, 500);
|
|
||||||
if let Some(ref cfg) = param_autoconfig {
|
|
||||||
info!(context, "Got autoconfig: {}", &cfg);
|
|
||||||
if !cfg.mail_user.is_empty() {
|
|
||||||
param.mail_user = cfg.mail_user.clone();
|
|
||||||
}
|
|
||||||
param.mail_server = cfg.mail_server.clone(); /* all other values are always NULL when entering autoconfig */
|
|
||||||
param.mail_port = cfg.mail_port;
|
|
||||||
param.send_server = cfg.send_server.clone();
|
|
||||||
param.send_port = cfg.send_port;
|
|
||||||
param.send_user = cfg.send_user.clone();
|
|
||||||
param.server_flags = cfg.server_flags;
|
|
||||||
/* although param_autoconfig's data are no longer needed from, it is important to keep the object as
|
|
||||||
we may enter "deep guessing" if we could not read a configuration */
|
|
||||||
}
|
|
||||||
param.server_flags |= keep_flags;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
// Step 3: Fill missing fields with defaults
|
|
||||||
13 => {
|
|
||||||
// if you move this, don't forget to update STEP_3_INDEX, too
|
|
||||||
if param.mail_server.is_empty() {
|
|
||||||
param.mail_server = format!("imap.{}", param_domain,)
|
|
||||||
}
|
|
||||||
if param.mail_port == 0 {
|
|
||||||
param.mail_port = if 0 != param.server_flags & (0x100 | 0x400) {
|
|
||||||
143
|
|
||||||
} else {
|
|
||||||
993
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if param.mail_user.is_empty() {
|
|
||||||
param.mail_user = param.addr.clone();
|
|
||||||
}
|
|
||||||
if param.send_server.is_empty() && !param.mail_server.is_empty() {
|
|
||||||
param.send_server = param.mail_server.clone();
|
|
||||||
if param.send_server.starts_with("imap.") {
|
|
||||||
param.send_server = param.send_server.replacen("imap", "smtp", 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if param.send_port == 0 {
|
|
||||||
param.send_port = if 0 != param.server_flags & DC_LP_SMTP_SOCKET_STARTTLS as i32
|
|
||||||
{
|
|
||||||
587
|
|
||||||
} else if 0 != param.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 {
|
|
||||||
25
|
|
||||||
} else {
|
|
||||||
465
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if param.send_user.is_empty() && !param.mail_user.is_empty() {
|
|
||||||
param.send_user = param.mail_user.clone();
|
|
||||||
}
|
|
||||||
if param.send_pw.is_empty() && !param.mail_pw.is_empty() {
|
|
||||||
param.send_pw = param.mail_pw.clone()
|
|
||||||
}
|
|
||||||
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_AUTH_FLAGS as i32) {
|
|
||||||
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
|
|
||||||
param.server_flags |= DC_LP_AUTH_NORMAL as i32
|
|
||||||
}
|
|
||||||
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_IMAP_SOCKET_FLAGS as i32) {
|
|
||||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
|
|
||||||
param.server_flags |= if param.send_port == 143 {
|
|
||||||
DC_LP_IMAP_SOCKET_STARTTLS as i32
|
|
||||||
} else {
|
|
||||||
DC_LP_IMAP_SOCKET_SSL as i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !dc_exactly_one_bit_set(param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32)) {
|
|
||||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
|
||||||
param.server_flags |= if param.send_port == 587 {
|
|
||||||
DC_LP_SMTP_SOCKET_STARTTLS as i32
|
|
||||||
} else if param.send_port == 25 {
|
|
||||||
DC_LP_SMTP_SOCKET_PLAIN as i32
|
|
||||||
} else {
|
|
||||||
DC_LP_SMTP_SOCKET_SSL as i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* do we have a complete configuration? */
|
|
||||||
if param.mail_server.is_empty()
|
|
||||||
|| param.mail_port == 0
|
|
||||||
|| param.mail_user.is_empty()
|
|
||||||
|| param.mail_pw.is_empty()
|
|
||||||
|| param.send_server.is_empty()
|
|
||||||
|| param.send_port == 0
|
|
||||||
|| param.send_user.is_empty()
|
|
||||||
|| param.send_pw.is_empty()
|
|
||||||
|| param.server_flags == 0
|
|
||||||
{
|
|
||||||
error!(context, "Account settings incomplete.");
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14 => {
|
|
||||||
progress!(context, 600);
|
|
||||||
/* try to connect to IMAP - if we did not got an autoconfig,
|
|
||||||
do some further tries with different settings and username variations */
|
|
||||||
imap_connected_here =
|
|
||||||
try_imap_connections(context, &mut param, param_autoconfig.is_some());
|
|
||||||
imap_connected_here
|
|
||||||
}
|
|
||||||
15 => {
|
|
||||||
progress!(context, 800);
|
|
||||||
smtp_connected_here =
|
|
||||||
try_smtp_connections(context, &mut param, param_autoconfig.is_some());
|
|
||||||
smtp_connected_here
|
|
||||||
}
|
|
||||||
16 => {
|
|
||||||
progress!(context, 900);
|
|
||||||
let create_mvbox = context.get_config_bool(Config::MvboxWatch)
|
|
||||||
|| context.get_config_bool(Config::MvboxMove);
|
|
||||||
let imap = &context.inbox_thread.read().unwrap().imap;
|
|
||||||
if let Err(err) = imap.ensure_configured_folders(context, create_mvbox) {
|
|
||||||
warn!(context, "configuring folders failed: {:?}", err);
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
let res = imap.select_with_uidvalidity(context, "INBOX");
|
|
||||||
if let Err(err) = res {
|
|
||||||
error!(context, "could not read INBOX status: {:?}", err);
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17 => {
|
|
||||||
progress!(context, 910);
|
|
||||||
/* configuration success - write back the configured parameters with the "configured_" prefix; also write the "configured"-flag */
|
|
||||||
param
|
|
||||||
.save_to_database(
|
|
||||||
context,
|
|
||||||
"configured_", /*the trailing underscore is correct*/
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
context.sql.set_raw_config_bool(context, "configured", true);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
18 => {
|
|
||||||
progress!(context, 920);
|
|
||||||
// we generate the keypair just now - we could also postpone this until the first message is sent, however,
|
|
||||||
// this may result in a unexpected and annoying delay when the user sends his very first message
|
|
||||||
// (~30 seconds on a Moto G4 play) and might looks as if message sending is always that slow.
|
|
||||||
e2ee::ensure_secret_key_exists(context);
|
|
||||||
success = true;
|
|
||||||
info!(context, "key generation completed");
|
|
||||||
progress!(context, 940);
|
|
||||||
break; // We are done here
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
error!(context, "Internal error: step counter out of bound",);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !success {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if imap_connected_here {
|
|
||||||
context
|
|
||||||
.inbox_thread
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.imap
|
|
||||||
.disconnect(context);
|
|
||||||
}
|
|
||||||
if smtp_connected_here {
|
|
||||||
context.smtp.clone().lock().unwrap().disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// remember the entered parameters on success
|
|
||||||
// and restore to last-entered on failure.
|
|
||||||
// this way, the parameters visible to the ui are always in-sync with the current configuration.
|
|
||||||
if success {
|
|
||||||
LoginParam::from_database(context, "").save_to_database(context, "configured_raw_");
|
|
||||||
} else {
|
|
||||||
LoginParam::from_database(context, "configured_raw_").save_to_database(context, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
context.free_ongoing();
|
|
||||||
progress!(context, if success { 1000 } else { 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_imap_connections(
|
|
||||||
context: &Context,
|
|
||||||
mut param: &mut LoginParam,
|
|
||||||
was_autoconfig: bool,
|
|
||||||
) -> bool {
|
|
||||||
// progress 650 and 660
|
|
||||||
if let Some(res) = try_imap_connection(context, &mut param, was_autoconfig, 0) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
progress!(context, 670);
|
|
||||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
|
|
||||||
param.server_flags |= DC_LP_IMAP_SOCKET_SSL;
|
|
||||||
param.mail_port = 993;
|
|
||||||
|
|
||||||
if let Some(at) = param.mail_user.find('@') {
|
|
||||||
param.mail_user = param.mail_user.split_at(at).0.to_string();
|
|
||||||
}
|
|
||||||
if let Some(at) = param.send_user.find('@') {
|
|
||||||
param.send_user = param.send_user.split_at(at).0.to_string();
|
|
||||||
}
|
|
||||||
// progress 680 and 690
|
|
||||||
if let Some(res) = try_imap_connection(context, &mut param, was_autoconfig, 1) {
|
|
||||||
res
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_imap_connection(
|
|
||||||
context: &Context,
|
|
||||||
param: &mut LoginParam,
|
|
||||||
was_autoconfig: bool,
|
|
||||||
variation: usize,
|
|
||||||
) -> Option<bool> {
|
|
||||||
if let Some(res) = try_imap_one_param(context, ¶m) {
|
|
||||||
return Some(res);
|
|
||||||
}
|
|
||||||
if was_autoconfig {
|
|
||||||
return Some(false);
|
|
||||||
}
|
|
||||||
progress!(context, 650 + variation * 30);
|
|
||||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
|
|
||||||
param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS;
|
|
||||||
if let Some(res) = try_imap_one_param(context, ¶m) {
|
|
||||||
return Some(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
progress!(context, 660 + variation * 30);
|
|
||||||
param.mail_port = 143;
|
|
||||||
|
|
||||||
try_imap_one_param(context, ¶m)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_imap_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
|
|
||||||
let inf = format!(
|
|
||||||
"imap: {}@{}:{} flags=0x{:x}",
|
|
||||||
param.mail_user, param.mail_server, param.mail_port, param.server_flags
|
|
||||||
);
|
|
||||||
info!(context, "Trying: {}", inf);
|
|
||||||
if context
|
|
||||||
.inbox_thread
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.imap
|
|
||||||
.connect(context, ¶m)
|
|
||||||
{
|
|
||||||
info!(context, "success: {}", inf);
|
|
||||||
return Some(true);
|
|
||||||
}
|
|
||||||
if context.shall_stop_ongoing() {
|
|
||||||
return Some(false);
|
|
||||||
}
|
|
||||||
info!(context, "Could not connect: {}", inf);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_smtp_connections(
|
|
||||||
context: &Context,
|
|
||||||
mut param: &mut LoginParam,
|
|
||||||
was_autoconfig: bool,
|
|
||||||
) -> bool {
|
|
||||||
/* try to connect to SMTP - if we did not got an autoconfig, the first try was SSL-465 and we do a second try with STARTTLS-587 */
|
|
||||||
if let Some(res) = try_smtp_one_param(context, ¶m) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
if was_autoconfig {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
progress!(context, 850);
|
|
||||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
|
||||||
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
|
|
||||||
param.send_port = 587;
|
|
||||||
|
|
||||||
if let Some(res) = try_smtp_one_param(context, ¶m) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
progress!(context, 860);
|
|
||||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
|
||||||
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
|
|
||||||
param.send_port = 25;
|
|
||||||
if let Some(res) = try_smtp_one_param(context, ¶m) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_smtp_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
|
|
||||||
let inf = format!(
|
|
||||||
"smtp: {}@{}:{} flags: 0x{:x}",
|
|
||||||
param.send_user, param.send_server, param.send_port, param.server_flags
|
|
||||||
);
|
|
||||||
info!(context, "Trying: {}", inf);
|
|
||||||
match context
|
|
||||||
.smtp
|
|
||||||
.clone()
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.connect(context, ¶m)
|
|
||||||
{
|
|
||||||
Ok(()) => {
|
|
||||||
info!(context, "success: {}", inf);
|
|
||||||
Some(true)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
if context.shall_stop_ongoing() {
|
|
||||||
Some(false)
|
|
||||||
} else {
|
|
||||||
warn!(context, "could not connect: {}", err);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use crate::config::*;
|
|
||||||
use crate::configure::JobConfigureImap;
|
|
||||||
use crate::test_utils::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_no_panic_on_bad_credentials() {
|
|
||||||
let t = dummy_context();
|
|
||||||
t.ctx
|
|
||||||
.set_config(Config::Addr, Some("probably@unexistant.addr"))
|
|
||||||
.unwrap();
|
|
||||||
t.ctx.set_config(Config::MailPw, Some("123456")).unwrap();
|
|
||||||
JobConfigureImap(&t.ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
use crate::context::Context;
|
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum Error {
|
|
||||||
#[fail(display = "URL request error")]
|
|
||||||
GetError(#[cause] reqwest::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
pub fn read_url(context: &Context, url: &str) -> Result<String> {
|
|
||||||
info!(context, "Requesting URL {}", url);
|
|
||||||
|
|
||||||
match reqwest::Client::new()
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.and_then(|mut res| res.text())
|
|
||||||
{
|
|
||||||
Ok(res) => Ok(res),
|
|
||||||
Err(err) => {
|
|
||||||
info!(context, "Can\'t read URL {}", url);
|
|
||||||
|
|
||||||
Err(Error::GetError(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
575
src/constants.rs
575
src/constants.rs
@@ -1,128 +1,104 @@
|
|||||||
//! # Constants
|
//! Constants
|
||||||
#![allow(non_camel_case_types, dead_code)]
|
#![allow(non_camel_case_types)]
|
||||||
|
use num_traits::{FromPrimitive, ToPrimitive};
|
||||||
|
use rusqlite as sql;
|
||||||
|
use rusqlite::types::*;
|
||||||
|
|
||||||
use deltachat_derive::*;
|
pub const DC_VERSION_STR: &'static [u8; 14] = b"1.0.0-alpha.3\x00";
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
lazy_static! {
|
pub const DC_MOVE_STATE_MOVING: u32 = 3;
|
||||||
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
|
pub const DC_MOVE_STATE_STAY: u32 = 2;
|
||||||
}
|
pub const DC_MOVE_STATE_PENDING: u32 = 1;
|
||||||
|
pub const DC_MOVE_STATE_UNDEFINED: u32 = 0;
|
||||||
// some defaults
|
|
||||||
const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
|
|
||||||
const DC_INBOX_WATCH_DEFAULT: i32 = 1;
|
|
||||||
const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
|
|
||||||
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
|
|
||||||
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
|
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum Blocked {
|
|
||||||
Not = 0,
|
|
||||||
Manually = 1,
|
|
||||||
Deaddrop = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Blocked {
|
|
||||||
fn default() -> Self {
|
|
||||||
Blocked::Not
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum ShowEmails {
|
|
||||||
Off = 0,
|
|
||||||
AcceptedContacts = 1,
|
|
||||||
All = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ShowEmails {
|
|
||||||
fn default() -> Self {
|
|
||||||
ShowEmails::Off // also change Config.ShowEmails props(default) on changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const DC_IMAP_SEEN: u32 = 0x1;
|
|
||||||
|
|
||||||
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
|
|
||||||
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
|
|
||||||
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
|
|
||||||
|
|
||||||
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
|
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
|
||||||
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
|
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
|
||||||
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
|
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
|
||||||
|
|
||||||
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
|
pub const DC_GCM_ADDDAYMARKER: usize = 0x01;
|
||||||
|
|
||||||
pub const DC_GCL_VERIFIED_ONLY: usize = 0x01;
|
pub const DC_GCL_VERIFIED_ONLY: usize = 0x01;
|
||||||
pub const DC_GCL_ADD_SELF: usize = 0x02;
|
pub const DC_GCL_ADD_SELF: usize = 0x02;
|
||||||
|
|
||||||
// values for DC_PARAM_FORCE_PLAINTEXT
|
/// param1 is a directory where the keys are written to
|
||||||
pub(crate) const DC_FP_NO_AUTOCRYPT_HEADER: i32 = 2;
|
pub const DC_IMEX_EXPORT_SELF_KEYS: usize = 1;
|
||||||
pub(crate) const DC_FP_ADD_AUTOCRYPT_HEADER: i32 = 1;
|
/// param1 is a directory where the keys are searched in and read from
|
||||||
|
pub const DC_IMEX_IMPORT_SELF_KEYS: usize = 2;
|
||||||
|
/// param1 is a directory where the backup is written to
|
||||||
|
pub const DC_IMEX_EXPORT_BACKUP: usize = 11;
|
||||||
|
/// param1 is the file with the backup to import
|
||||||
|
pub const DC_IMEX_IMPORT_BACKUP: usize = 12;
|
||||||
|
|
||||||
|
/// id=contact
|
||||||
|
pub const DC_QR_ASK_VERIFYCONTACT: usize = 200;
|
||||||
|
/// text1=groupname
|
||||||
|
pub const DC_QR_ASK_VERIFYGROUP: usize = 202;
|
||||||
|
/// id=contact
|
||||||
|
pub const DC_QR_FPR_OK: usize = 210;
|
||||||
|
/// id=contact
|
||||||
|
pub const DC_QR_FPR_MISMATCH: usize = 220;
|
||||||
|
/// test1=formatted fingerprint
|
||||||
|
pub const DC_QR_FPR_WITHOUT_ADDR: usize = 230;
|
||||||
|
/// id=contact
|
||||||
|
pub const DC_QR_ADDR: usize = 320;
|
||||||
|
/// text1=text
|
||||||
|
pub const DC_QR_TEXT: usize = 330;
|
||||||
|
/// text1=URL
|
||||||
|
pub const DC_QR_URL: usize = 332;
|
||||||
|
/// text1=error string
|
||||||
|
pub const DC_QR_ERROR: usize = 400;
|
||||||
|
|
||||||
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
|
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
|
||||||
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
|
pub const DC_CHAT_ID_DEADDROP: usize = 1;
|
||||||
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
|
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
|
||||||
pub const DC_CHAT_ID_TRASH: u32 = 3;
|
pub const DC_CHAT_ID_TRASH: usize = 3;
|
||||||
/// a message is just in creation but not yet assigned to a chat (eg. we may need the message ID to set up blobs; this avoids unready message to be sent and shown)
|
/// a message is just in creation but not yet assigned to a chat (eg. we may need the message ID to set up blobs; this avoids unready message to be sent and shown)
|
||||||
const DC_CHAT_ID_MSGS_IN_CREATION: u32 = 4;
|
pub const DC_CHAT_ID_MSGS_IN_CREATION: usize = 4;
|
||||||
/// virtual chat showing all messages flagged with msgs.starred=2
|
/// virtual chat showing all messages flagged with msgs.starred=2
|
||||||
pub const DC_CHAT_ID_STARRED: u32 = 5;
|
pub const DC_CHAT_ID_STARRED: usize = 5;
|
||||||
/// only an indicator in a chatlist
|
/// only an indicator in a chatlist
|
||||||
pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6;
|
pub const DC_CHAT_ID_ARCHIVED_LINK: usize = 6;
|
||||||
/// only an indicator in a chatlist
|
/// only an indicator in a chatlist
|
||||||
pub const DC_CHAT_ID_ALLDONE_HINT: u32 = 7;
|
pub const DC_CHAT_ID_ALLDONE_HINT: usize = 7;
|
||||||
/// larger chat IDs are "real" chats, their messages are "real" messages.
|
/// larger chat IDs are "real" chats, their messages are "real" messages.
|
||||||
pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
|
pub const DC_CHAT_ID_LAST_SPECIAL: usize = 9;
|
||||||
|
|
||||||
#[derive(
|
pub const DC_CHAT_TYPE_UNDEFINED: i32 = 0;
|
||||||
Debug,
|
pub const DC_CHAT_TYPE_SINGLE: i32 = 100;
|
||||||
Display,
|
pub const DC_CHAT_TYPE_GROUP: i32 = 120;
|
||||||
Clone,
|
pub const DC_CHAT_TYPE_VERIFIED_GROUP: i32 = 130;
|
||||||
Copy,
|
|
||||||
PartialEq,
|
|
||||||
Eq,
|
|
||||||
FromPrimitive,
|
|
||||||
ToPrimitive,
|
|
||||||
FromSql,
|
|
||||||
ToSql,
|
|
||||||
IntoStaticStr,
|
|
||||||
)]
|
|
||||||
#[repr(u32)]
|
|
||||||
pub enum Chattype {
|
|
||||||
Undefined = 0,
|
|
||||||
Single = 100,
|
|
||||||
Group = 120,
|
|
||||||
VerifiedGroup = 130,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Chattype {
|
pub const DC_MSG_ID_MARKER1: usize = 1;
|
||||||
fn default() -> Self {
|
pub const DC_MSG_ID_DAYMARKER: usize = 9;
|
||||||
Chattype::Undefined
|
pub const DC_MSG_ID_LAST_SPECIAL: usize = 9;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const DC_MSG_ID_MARKER1: u32 = 1;
|
pub const DC_STATE_UNDEFINED: i32 = 0;
|
||||||
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
|
pub const DC_STATE_IN_FRESH: i32 = 10;
|
||||||
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
|
pub const DC_STATE_IN_NOTICED: i32 = 13;
|
||||||
|
pub const DC_STATE_IN_SEEN: i32 = 16;
|
||||||
|
pub const DC_STATE_OUT_PREPARING: i32 = 18;
|
||||||
|
pub const DC_STATE_OUT_DRAFT: i32 = 19;
|
||||||
|
pub const DC_STATE_OUT_PENDING: i32 = 20;
|
||||||
|
pub const DC_STATE_OUT_FAILED: i32 = 24;
|
||||||
|
/// to check if a mail was sent, use dc_msg_is_sent()
|
||||||
|
pub const DC_STATE_OUT_DELIVERED: i32 = 26;
|
||||||
|
pub const DC_STATE_OUT_MDN_RCVD: i32 = 28;
|
||||||
|
|
||||||
/// approx. max. length returned by dc_msg_get_text()
|
/// approx. max. length returned by dc_msg_get_text()
|
||||||
const DC_MAX_GET_TEXT_LEN: usize = 30000;
|
pub const DC_MAX_GET_TEXT_LEN: usize = 30000;
|
||||||
/// approx. max. length returned by dc_get_msg_info()
|
/// approx. max. length returned by dc_get_msg_info()
|
||||||
const DC_MAX_GET_INFO_LEN: usize = 100000;
|
pub const DC_MAX_GET_INFO_LEN: usize = 100000;
|
||||||
|
|
||||||
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
|
pub const DC_CONTACT_ID_UNDEFINED: usize = 0;
|
||||||
pub const DC_CONTACT_ID_SELF: u32 = 1;
|
pub const DC_CONTACT_ID_SELF: usize = 1;
|
||||||
pub const DC_CONTACT_ID_INFO: u32 = 2;
|
pub const DC_CONTACT_ID_DEVICE: usize = 2;
|
||||||
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
|
pub const DC_CONTACT_ID_LAST_SPECIAL: usize = 9;
|
||||||
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
|
|
||||||
|
|
||||||
// Flags for empty server job
|
pub const DC_TEXT1_DRAFT: usize = 1;
|
||||||
|
pub const DC_TEXT1_USERNAME: usize = 2;
|
||||||
|
pub const DC_TEXT1_SELF: usize = 3;
|
||||||
|
|
||||||
pub const DC_EMPTY_MVBOX: u32 = 0x01;
|
pub const DC_CREATE_MVBOX: usize = 1;
|
||||||
pub const DC_EMPTY_INBOX: u32 = 0x02;
|
|
||||||
|
|
||||||
// Flags for configuring IMAP and SMTP servers.
|
// Flags for configuring IMAP and SMTP servers.
|
||||||
// These flags are optional
|
// These flags are optional
|
||||||
@@ -130,25 +106,25 @@ pub const DC_EMPTY_INBOX: u32 = 0x02;
|
|||||||
// via dc_set_config() using the key "server_flags".
|
// via dc_set_config() using the key "server_flags".
|
||||||
|
|
||||||
/// Force OAuth2 authorization. This flag does not skip automatic configuration.
|
/// Force OAuth2 authorization. This flag does not skip automatic configuration.
|
||||||
/// Before calling configure() with DC_LP_AUTH_OAUTH2 set,
|
/// Before calling dc_configure() with DC_LP_AUTH_OAUTH2 set,
|
||||||
/// the user has to confirm access at the URL returned by dc_get_oauth2_url().
|
/// the user has to confirm access at the URL returned by dc_get_oauth2_url().
|
||||||
pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
|
pub const DC_LP_AUTH_OAUTH2: usize = 0x2;
|
||||||
|
|
||||||
/// Force NORMAL authorization, this is the default.
|
/// Force NORMAL authorization, this is the default.
|
||||||
/// If this flag is set, automatic configuration is skipped.
|
/// If this flag is set, automatic configuration is skipped.
|
||||||
pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
|
pub const DC_LP_AUTH_NORMAL: usize = 0x4;
|
||||||
|
|
||||||
/// Connect to IMAP via STARTTLS.
|
/// Connect to IMAP via STARTTLS.
|
||||||
/// If this flag is set, automatic configuration is skipped.
|
/// If this flag is set, automatic configuration is skipped.
|
||||||
pub const DC_LP_IMAP_SOCKET_STARTTLS: i32 = 0x100;
|
pub const DC_LP_IMAP_SOCKET_STARTTLS: usize = 0x100;
|
||||||
|
|
||||||
/// Connect to IMAP via SSL.
|
/// Connect to IMAP via SSL.
|
||||||
/// If this flag is set, automatic configuration is skipped.
|
/// If this flag is set, automatic configuration is skipped.
|
||||||
pub const DC_LP_IMAP_SOCKET_SSL: i32 = 0x200;
|
pub const DC_LP_IMAP_SOCKET_SSL: usize = 0x200;
|
||||||
|
|
||||||
/// Connect to IMAP unencrypted, this should not be used.
|
/// Connect to IMAP unencrypted, this should not be used.
|
||||||
/// If this flag is set, automatic configuration is skipped.
|
/// If this flag is set, automatic configuration is skipped.
|
||||||
pub const DC_LP_IMAP_SOCKET_PLAIN: i32 = 0x400;
|
pub const DC_LP_IMAP_SOCKET_PLAIN: usize = 0x400;
|
||||||
|
|
||||||
/// Connect to SMTP via STARTTLS.
|
/// Connect to SMTP via STARTTLS.
|
||||||
/// If this flag is set, automatic configuration is skipped.
|
/// If this flag is set, automatic configuration is skipped.
|
||||||
@@ -163,21 +139,15 @@ pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
|
|||||||
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
|
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
|
||||||
|
|
||||||
/// if none of these flags are set, the default is chosen
|
/// if none of these flags are set, the default is chosen
|
||||||
pub const DC_LP_AUTH_FLAGS: i32 = (DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL);
|
pub const DC_LP_AUTH_FLAGS: usize = (DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL);
|
||||||
/// if none of these flags are set, the default is chosen
|
/// if none of these flags are set, the default is chosen
|
||||||
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
|
pub const DC_LP_IMAP_SOCKET_FLAGS: usize =
|
||||||
(DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN);
|
(DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN);
|
||||||
/// if none of these flags are set, the default is chosen
|
/// if none of these flags are set, the default is chosen
|
||||||
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
|
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
|
||||||
(DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN);
|
(DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN);
|
||||||
|
|
||||||
// QR code scanning (view from Bob, the joiner)
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||||
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
|
|
||||||
pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
|
|
||||||
pub const DC_BOB_ERROR: i32 = 0;
|
|
||||||
pub const DC_BOB_SUCCESS: i32 = 1;
|
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
|
||||||
#[repr(i32)]
|
#[repr(i32)]
|
||||||
pub enum Viewtype {
|
pub enum Viewtype {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
@@ -197,11 +167,6 @@ pub enum Viewtype {
|
|||||||
/// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
|
/// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
|
||||||
Gif = 21,
|
Gif = 21,
|
||||||
|
|
||||||
/// Message containing a sticker, similar to image.
|
|
||||||
/// If possible, the ui should display the image without borders in a transparent way.
|
|
||||||
/// A click on a sticker will offer to install the sticker set in some future.
|
|
||||||
Sticker = 23,
|
|
||||||
|
|
||||||
/// Message containing an Audio file.
|
/// Message containing an Audio file.
|
||||||
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
|
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
|
||||||
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
|
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
|
||||||
@@ -227,12 +192,6 @@ pub enum Viewtype {
|
|||||||
File = 60,
|
File = 60,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Viewtype {
|
|
||||||
fn default() -> Self {
|
|
||||||
Viewtype::Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -243,66 +202,323 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToSql for Viewtype {
|
||||||
|
fn to_sql(&self) -> sql::Result<ToSqlOutput> {
|
||||||
|
let num: i64 = self
|
||||||
|
.to_i64()
|
||||||
|
.expect("impossible: Viewtype -> i64 conversion failed");
|
||||||
|
|
||||||
|
Ok(ToSqlOutput::Owned(Value::Integer(num)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromSql for Viewtype {
|
||||||
|
fn column_result(col: ValueRef) -> FromSqlResult<Self> {
|
||||||
|
let inner = FromSql::column_result(col)?;
|
||||||
|
FromPrimitive::from_i64(inner).ok_or(FromSqlError::InvalidType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// These constants are used as events
|
// These constants are used as events
|
||||||
// reported to the callback given to dc_context_new().
|
// reported to the callback given to dc_context_new().
|
||||||
// If you do not want to handle an event, it is always safe to return 0,
|
// If you do not want to handle an event, it is always safe to return 0,
|
||||||
// so there is no need to add a "case" for every event.
|
// so there is no need to add a "case" for every event.
|
||||||
|
|
||||||
const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||||
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
|
#[repr(u32)]
|
||||||
const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
|
pub enum Event {
|
||||||
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
|
/// The library-user may write an informational string to the log.
|
||||||
const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
|
/// 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.
|
||||||
|
/// @param data1 0
|
||||||
|
/// @param data2 (const char*) Info string in english language.
|
||||||
|
/// Must not be free()'d or modified and is valid only until the callback returns.
|
||||||
|
/// @return 0
|
||||||
|
INFO = 100,
|
||||||
|
|
||||||
|
/// Emitted when SMTP connection is established and login was successful.
|
||||||
|
///
|
||||||
|
/// @param data1 0
|
||||||
|
/// @param data2 (const char*) Info string in english language.
|
||||||
|
/// Must not be free()'d or modified and is valid only until the callback returns.
|
||||||
|
/// @return 0
|
||||||
|
SMTP_CONNECTED = 101,
|
||||||
|
|
||||||
|
/// Emitted when IMAP connection is established and login was successful.
|
||||||
|
///
|
||||||
|
/// @param data1 0
|
||||||
|
/// @param data2 (const char*) Info string in english language.
|
||||||
|
/// Must not be free()'d or modified and is valid only until the callback returns.
|
||||||
|
/// @return 0
|
||||||
|
IMAP_CONNECTED = 102,
|
||||||
|
|
||||||
|
/// Emitted when a message was successfully sent to the SMTP server.
|
||||||
|
///
|
||||||
|
/// @param data1 0
|
||||||
|
/// @param data2 (const char*) Info string in english language.
|
||||||
|
/// Must not be free()'d or modified and is valid only until the callback returns.
|
||||||
|
/// @return 0
|
||||||
|
SMTP_MESSAGE_SENT = 103,
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// @param data1 0
|
||||||
|
/// @param data2 (const char*) Warning string in english language.
|
||||||
|
/// Must not be free()'d or modified and is valid only until the callback returns.
|
||||||
|
/// @return 0
|
||||||
|
WARNING = 300,
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||||
|
///
|
||||||
|
/// However, for ongoing processes (eg. dc_configure())
|
||||||
|
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
|
||||||
|
/// it might be better to delay showing these events until the function has really
|
||||||
|
/// failed (returned false). It should be sufficient to report only the _last_ error
|
||||||
|
/// in a messasge box then.
|
||||||
|
///
|
||||||
|
/// @param data1 0
|
||||||
|
/// @param data2 (const char*) Error string, always set, never NULL. Frequent error strings are
|
||||||
|
/// localized using #DC_EVENT_GET_STRING, however, most error strings will be in english language.
|
||||||
|
/// Must not be free()'d or modified and is valid only until the callback returns.
|
||||||
|
/// @return 0
|
||||||
|
ERROR = 400,
|
||||||
|
|
||||||
|
/// An action cannot be performed because there is no network available.
|
||||||
|
///
|
||||||
|
/// The library will typically try over after a some time
|
||||||
|
/// and when dc_maybe_network() is called.
|
||||||
|
///
|
||||||
|
/// Network errors should be reported to users in a non-disturbing way,
|
||||||
|
/// however, as network errors may come in a sequence,
|
||||||
|
/// it is not useful to raise each an every error to the user.
|
||||||
|
/// For this purpose, data1 is set to 1 if the error is probably worth reporting.
|
||||||
|
///
|
||||||
|
/// Moreover, if the UI detects that the device is offline,
|
||||||
|
/// it is probably more useful to report this to the user
|
||||||
|
/// instead of the string from data2.
|
||||||
|
///
|
||||||
|
/// @param data1 (int) 1=first/new network error, should be reported the user;
|
||||||
|
/// 0=subsequent network error, should be logged only
|
||||||
|
/// @param data2 (const char*) Error string, always set, never NULL.
|
||||||
|
/// Must not be free()'d or modified and is valid only until the callback returns.
|
||||||
|
/// @return 0
|
||||||
|
ERROR_NETWORK = 401,
|
||||||
|
|
||||||
|
/// An action cannot be performed because the user is not in the group.
|
||||||
|
/// Reported eg. after a call to
|
||||||
|
/// dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||||
|
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
|
||||||
|
/// dc_send_text_msg() or another sending function.
|
||||||
|
///
|
||||||
|
/// @param data1 0
|
||||||
|
/// @param data2 (const char*) Info string in english language.
|
||||||
|
/// Must not be free()'d or modified
|
||||||
|
/// and is valid only until the callback returns.
|
||||||
|
/// @return 0
|
||||||
|
ERROR_SELF_NOT_IN_GROUP = 410,
|
||||||
|
|
||||||
|
/// Messages or chats changed. One or more messages or chats changed for various
|
||||||
|
/// reasons in the database:
|
||||||
|
/// - Messages sent, received or removed
|
||||||
|
/// - Chats created, deleted or archived
|
||||||
|
/// - A draft has been set
|
||||||
|
///
|
||||||
|
/// @param data1 (int) chat_id for single added messages
|
||||||
|
/// @param data2 (int) msg_id for single added messages
|
||||||
|
/// @return 0
|
||||||
|
MSGS_CHANGED = 2000,
|
||||||
|
|
||||||
|
/// There is a fresh message. Typically, the user will show an notification
|
||||||
|
/// when receiving this message.
|
||||||
|
///
|
||||||
|
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
||||||
|
///
|
||||||
|
/// @param data1 (int) chat_id
|
||||||
|
/// @param data2 (int) msg_id
|
||||||
|
/// @return 0
|
||||||
|
INCOMING_MSG = 2005,
|
||||||
|
|
||||||
|
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||||
|
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
|
||||||
|
///
|
||||||
|
/// @param data1 (int) chat_id
|
||||||
|
/// @param data2 (int) msg_id
|
||||||
|
/// @return 0
|
||||||
|
MSG_DELIVERED = 2010,
|
||||||
|
|
||||||
|
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||||
|
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
|
||||||
|
///
|
||||||
|
/// @param data1 (int) chat_id
|
||||||
|
/// @param data2 (int) msg_id
|
||||||
|
/// @return 0
|
||||||
|
MSG_FAILED = 2012,
|
||||||
|
|
||||||
|
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||||
|
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
|
||||||
|
///
|
||||||
|
/// @param data1 (int) chat_id
|
||||||
|
/// @param data2 (int) msg_id
|
||||||
|
/// @return 0
|
||||||
|
MSG_READ = 2015,
|
||||||
|
|
||||||
|
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||||
|
/// Or the verify state of a chat has changed.
|
||||||
|
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||||
|
/// and dc_remove_contact_from_chat().
|
||||||
|
///
|
||||||
|
/// @param data1 (int) chat_id
|
||||||
|
/// @param data2 0
|
||||||
|
/// @return 0
|
||||||
|
CHAT_MODIFIED = 2020,
|
||||||
|
|
||||||
|
/// Contact(s) created, renamed, blocked or deleted.
|
||||||
|
///
|
||||||
|
/// @param data1 (int) If not 0, this is the contact_id of an added contact that should be selected.
|
||||||
|
/// @param data2 0
|
||||||
|
/// @return 0
|
||||||
|
CONTACTS_CHANGED = 2030,
|
||||||
|
|
||||||
|
/// Location of one or more contact has changed.
|
||||||
|
///
|
||||||
|
/// @param data1 (int) contact_id of the contact for which the location has changed.
|
||||||
|
/// If the locations of several contacts have been changed,
|
||||||
|
/// eg. after calling dc_delete_all_locations(), this parameter is set to 0.
|
||||||
|
/// @param data2 0
|
||||||
|
/// @return 0
|
||||||
|
LOCATION_CHANGED = 2035,
|
||||||
|
|
||||||
|
/// Inform about the configuration progress started by dc_configure().
|
||||||
|
///
|
||||||
|
/// @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
|
/// @param data2 0
|
||||||
|
/// @return 0
|
||||||
|
CONFIGURE_PROGRESS = 2041,
|
||||||
|
|
||||||
|
/// Inform about the import/export progress started by dc_imex().
|
||||||
|
///
|
||||||
|
/// @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
|
/// @param data2 0
|
||||||
|
/// @return 0
|
||||||
|
IMEX_PROGRESS = 2051,
|
||||||
|
|
||||||
|
/// A file has been exported. A file has been written by dc_imex().
|
||||||
|
/// This event may be sent multiple times by a single call to dc_imex().
|
||||||
|
///
|
||||||
|
/// A typical purpose for a handler of this event may be to make the file public to some system
|
||||||
|
/// services.
|
||||||
|
///
|
||||||
|
/// @param data1 (const char*) Path and file name.
|
||||||
|
/// Must not be free()'d or modified and is valid only until the callback returns.
|
||||||
|
/// @param data2 0
|
||||||
|
/// @return 0
|
||||||
|
IMEX_FILE_WRITTEN = 2052,
|
||||||
|
|
||||||
|
/// Progress information of a secure-join handshake from the view of the inviter
|
||||||
|
/// (Alice, the person who shows the QR code).
|
||||||
|
///
|
||||||
|
/// These events are typically sent after a joiner has scanned the QR code
|
||||||
|
/// generated by dc_get_securejoin_qr().
|
||||||
|
///
|
||||||
|
/// @param data1 (int) ID of the contact that wants to join.
|
||||||
|
/// @param data2 (int) Progress as:
|
||||||
|
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||||
|
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||||
|
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||||
|
/// 1000=Protocol finished for this contact.
|
||||||
|
/// @return 0
|
||||||
|
SECUREJOIN_INVITER_PROGRESS = 2060,
|
||||||
|
|
||||||
|
/// Progress information of a secure-join handshake from the view of the joiner
|
||||||
|
/// (Bob, the person who scans the QR code).
|
||||||
|
/// The events are typically sent while dc_join_securejoin(), which
|
||||||
|
/// may take some time, is executed.
|
||||||
|
/// @param data1 (int) ID of the inviting contact.
|
||||||
|
/// @param data2 (int) Progress as:
|
||||||
|
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||||
|
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||||
|
/// @return 0
|
||||||
|
SECUREJOIN_JOINER_PROGRESS = 2061,
|
||||||
|
|
||||||
|
// the following events are functions that should be provided by the frontends
|
||||||
|
/// Requeste a localized string from the frontend.
|
||||||
|
/// @param data1 (int) ID of the string to request, one of the DC_STR_/// constants.
|
||||||
|
/// @param data2 (int) The count. If the requested string contains a placeholder for a numeric value,
|
||||||
|
/// the ui may use this value to return different strings on different plural forms.
|
||||||
|
/// @return (const char*) Null-terminated UTF-8 string.
|
||||||
|
/// The string will be free()'d by the core,
|
||||||
|
/// so it must be allocated using malloc() or a compatible function.
|
||||||
|
/// Return 0 if the ui cannot provide the requested string
|
||||||
|
/// the core will use a default string in english language then.
|
||||||
|
GET_STRING = 2091,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
|
||||||
|
pub const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
|
||||||
|
pub const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
|
||||||
|
pub const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
|
||||||
|
pub const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
|
||||||
|
|
||||||
|
/// Values for dc_get|set_config("show_emails")
|
||||||
|
pub const DC_SHOW_EMAILS_OFF: usize = 0;
|
||||||
|
pub const DC_SHOW_EMAILS_ACCEPTED_CONTACTS: usize = 1;
|
||||||
|
pub const DC_SHOW_EMAILS_ALL: usize = 2;
|
||||||
|
|
||||||
// TODO: Strings need some doumentation about used placeholders.
|
// TODO: Strings need some doumentation about used placeholders.
|
||||||
// These constants are used to set stock translation strings
|
// These constants are used to request strings using #DC_EVENT_GET_STRING.
|
||||||
|
|
||||||
const DC_STR_NOMESSAGES: usize = 1;
|
pub const DC_STR_NOMESSAGES: usize = 1;
|
||||||
const DC_STR_SELF: usize = 2;
|
pub const DC_STR_SELF: usize = 2;
|
||||||
const DC_STR_DRAFT: usize = 3;
|
pub const DC_STR_DRAFT: usize = 3;
|
||||||
const DC_STR_MEMBER: usize = 4;
|
pub const DC_STR_MEMBER: usize = 4;
|
||||||
const DC_STR_CONTACT: usize = 6;
|
pub const DC_STR_CONTACT: usize = 6;
|
||||||
const DC_STR_VOICEMESSAGE: usize = 7;
|
pub const DC_STR_VOICEMESSAGE: usize = 7;
|
||||||
const DC_STR_DEADDROP: usize = 8;
|
pub const DC_STR_DEADDROP: usize = 8;
|
||||||
const DC_STR_IMAGE: usize = 9;
|
pub const DC_STR_IMAGE: usize = 9;
|
||||||
const DC_STR_VIDEO: usize = 10;
|
pub const DC_STR_VIDEO: usize = 10;
|
||||||
const DC_STR_AUDIO: usize = 11;
|
pub const DC_STR_AUDIO: usize = 11;
|
||||||
const DC_STR_FILE: usize = 12;
|
pub const DC_STR_FILE: usize = 12;
|
||||||
const DC_STR_STATUSLINE: usize = 13;
|
pub const DC_STR_STATUSLINE: usize = 13;
|
||||||
const DC_STR_NEWGROUPDRAFT: usize = 14;
|
pub const DC_STR_NEWGROUPDRAFT: usize = 14;
|
||||||
const DC_STR_MSGGRPNAME: usize = 15;
|
pub const DC_STR_MSGGRPNAME: usize = 15;
|
||||||
const DC_STR_MSGGRPIMGCHANGED: usize = 16;
|
pub const DC_STR_MSGGRPIMGCHANGED: usize = 16;
|
||||||
const DC_STR_MSGADDMEMBER: usize = 17;
|
pub const DC_STR_MSGADDMEMBER: usize = 17;
|
||||||
const DC_STR_MSGDELMEMBER: usize = 18;
|
pub const DC_STR_MSGDELMEMBER: usize = 18;
|
||||||
const DC_STR_MSGGROUPLEFT: usize = 19;
|
pub const DC_STR_MSGGROUPLEFT: usize = 19;
|
||||||
const DC_STR_GIF: usize = 23;
|
pub const DC_STR_GIF: usize = 23;
|
||||||
const DC_STR_ENCRYPTEDMSG: usize = 24;
|
pub const DC_STR_ENCRYPTEDMSG: usize = 24;
|
||||||
const DC_STR_E2E_AVAILABLE: usize = 25;
|
pub const DC_STR_E2E_AVAILABLE: usize = 25;
|
||||||
const DC_STR_ENCR_TRANSP: usize = 27;
|
pub const DC_STR_ENCR_TRANSP: usize = 27;
|
||||||
const DC_STR_ENCR_NONE: usize = 28;
|
pub const DC_STR_ENCR_NONE: usize = 28;
|
||||||
const DC_STR_CANTDECRYPT_MSG_BODY: usize = 29;
|
pub const DC_STR_CANTDECRYPT_MSG_BODY: usize = 29;
|
||||||
const DC_STR_FINGERPRINTS: usize = 30;
|
pub const DC_STR_FINGERPRINTS: usize = 30;
|
||||||
const DC_STR_READRCPT: usize = 31;
|
pub const DC_STR_READRCPT: usize = 31;
|
||||||
const DC_STR_READRCPT_MAILBODY: usize = 32;
|
pub const DC_STR_READRCPT_MAILBODY: usize = 32;
|
||||||
const DC_STR_MSGGRPIMGDELETED: usize = 33;
|
pub const DC_STR_MSGGRPIMGDELETED: usize = 33;
|
||||||
const DC_STR_E2E_PREFERRED: usize = 34;
|
pub const DC_STR_E2E_PREFERRED: usize = 34;
|
||||||
const DC_STR_CONTACT_VERIFIED: usize = 35;
|
pub const DC_STR_CONTACT_VERIFIED: usize = 35;
|
||||||
const DC_STR_CONTACT_NOT_VERIFIED: usize = 36;
|
pub const DC_STR_CONTACT_NOT_VERIFIED: usize = 36;
|
||||||
const DC_STR_CONTACT_SETUP_CHANGED: usize = 37;
|
pub const DC_STR_CONTACT_SETUP_CHANGED: usize = 37;
|
||||||
const DC_STR_ARCHIVEDCHATS: usize = 40;
|
pub const DC_STR_ARCHIVEDCHATS: usize = 40;
|
||||||
const DC_STR_STARREDMSGS: usize = 41;
|
pub const DC_STR_STARREDMSGS: usize = 41;
|
||||||
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
|
pub const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
|
||||||
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
|
pub const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
|
||||||
const DC_STR_SELFTALK_SUBTITLE: usize = 50;
|
pub const DC_STR_SELFTALK_SUBTITLE: usize = 50;
|
||||||
const DC_STR_CANNOT_LOGIN: usize = 60;
|
pub const DC_STR_CANNOT_LOGIN: usize = 60;
|
||||||
const DC_STR_SERVER_RESPONSE: usize = 61;
|
pub const DC_STR_SERVER_RESPONSE: usize = 61;
|
||||||
const DC_STR_MSGACTIONBYUSER: usize = 62;
|
pub const DC_STR_MSGACTIONBYUSER: usize = 62;
|
||||||
const DC_STR_MSGACTIONBYME: usize = 63;
|
pub const DC_STR_MSGACTIONBYME: usize = 63;
|
||||||
const DC_STR_MSGLOCATIONENABLED: usize = 64;
|
pub const DC_STR_MSGLOCATIONENABLED: usize = 64;
|
||||||
const DC_STR_MSGLOCATIONDISABLED: usize = 65;
|
pub const DC_STR_MSGLOCATIONDISABLED: usize = 65;
|
||||||
const DC_STR_LOCATION: usize = 66;
|
pub const DC_STR_LOCATION: usize = 66;
|
||||||
const DC_STR_STICKER: usize = 67;
|
pub const DC_STR_COUNT: usize = 66;
|
||||||
const DC_STR_COUNT: usize = 67;
|
|
||||||
|
|
||||||
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
|
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
|
||||||
|
|
||||||
@@ -312,3 +528,12 @@ pub enum KeyType {
|
|||||||
Public = 0,
|
Public = 0,
|
||||||
Private = 1,
|
Private = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const DC_CMD_GROUPNAME_CHANGED: libc::c_int = 2;
|
||||||
|
pub const DC_CMD_GROUPIMAGE_CHANGED: libc::c_int = 3;
|
||||||
|
pub const DC_CMD_MEMBER_ADDED_TO_GROUP: libc::c_int = 4;
|
||||||
|
pub const DC_CMD_MEMBER_REMOVED_FROM_GROUP: libc::c_int = 5;
|
||||||
|
pub const DC_CMD_AUTOCRYPT_SETUP_MESSAGE: libc::c_int = 6;
|
||||||
|
pub const DC_CMD_SECUREJOIN_MESSAGE: libc::c_int = 7;
|
||||||
|
pub const DC_CMD_LOCATION_STREAMING_ENABLED: libc::c_int = 8;
|
||||||
|
pub const DC_CMD_LOCATION_ONLY: libc::c_int = 9;
|
||||||
|
|||||||
503
src/contact.rs
503
src/contact.rs
@@ -1,41 +1,40 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use deltachat_derive::*;
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use num_traits::{FromPrimitive, ToPrimitive};
|
||||||
use rusqlite;
|
use rusqlite;
|
||||||
|
use rusqlite::types::*;
|
||||||
|
|
||||||
use crate::aheader::EncryptPreference;
|
use crate::aheader::EncryptPreference;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
|
use crate::dc_array::*;
|
||||||
|
use crate::dc_e2ee::*;
|
||||||
|
use crate::dc_loginparam::*;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::e2ee;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::events::Event;
|
|
||||||
use crate::key::*;
|
use crate::key::*;
|
||||||
use crate::login_param::LoginParam;
|
|
||||||
use crate::message::{MessageState, MsgId};
|
|
||||||
use crate::peerstate::*;
|
use crate::peerstate::*;
|
||||||
use crate::sql;
|
use crate::sql;
|
||||||
use crate::stock::StockMessage;
|
use crate::stock::StockMessage;
|
||||||
|
use crate::types::*;
|
||||||
|
|
||||||
|
const DC_GCL_VERIFIED_ONLY: u32 = 0x01;
|
||||||
|
|
||||||
/// Contacts with at least this origin value are shown in the contact list.
|
/// Contacts with at least this origin value are shown in the contact list.
|
||||||
const DC_ORIGIN_MIN_CONTACT_LIST: i32 = 0x100;
|
const DC_ORIGIN_MIN_CONTACT_LIST: i32 = 0x100;
|
||||||
|
|
||||||
/// An object representing a single contact in memory.
|
/// An object representing a single contact in memory.
|
||||||
///
|
|
||||||
/// The contact object is not updated.
|
/// The contact object is not updated.
|
||||||
/// If you want an update, you have to recreate the object.
|
/// If you want an update, you have to recreate the object.
|
||||||
///
|
///
|
||||||
/// The library makes sure
|
/// The library makes sure
|
||||||
/// only to use names _authorized_ by the contact in `To:` or `Cc:`.
|
/// only to use names _authorized_ by the contact in `To:` or `Cc:`.
|
||||||
/// *Given-names* as "Daddy" or "Honey" are not used there.
|
/// _Given-names _as "Daddy" or "Honey" are not used there.
|
||||||
/// For this purpose, internally, two names are tracked -
|
/// For this purpose, internally, two names are tracked -
|
||||||
/// authorized name and given name.
|
/// authorized-name and given-name.
|
||||||
/// By default, these names are equal, but functions working with contact names
|
/// By default, these names are equal, but functions working with contact names
|
||||||
/// only affect the given name.
|
pub struct Contact<'a> {
|
||||||
#[derive(Debug)]
|
context: &'a Context,
|
||||||
pub struct Contact {
|
|
||||||
/// The contact ID.
|
/// The contact ID.
|
||||||
///
|
///
|
||||||
/// Special message IDs:
|
/// Special message IDs:
|
||||||
@@ -49,8 +48,8 @@ pub struct Contact {
|
|||||||
/// May be empty, initially set to `authname`.
|
/// May be empty, initially set to `authname`.
|
||||||
name: String,
|
name: String,
|
||||||
/// Name authorized by the contact himself. Only this name may be spread to others,
|
/// Name authorized by the contact himself. Only this name may be spread to others,
|
||||||
/// e.g. in To:-lists. May be empty. It is recommended to use `Contact::get_authname`,
|
/// e.g. in To:-lists. May be empty. It is recommended to use `Contact::get_name`,
|
||||||
/// to access this field.
|
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
|
||||||
authname: String,
|
authname: String,
|
||||||
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr`` to access this field.
|
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr`` to access this field.
|
||||||
addr: String,
|
addr: String,
|
||||||
@@ -61,9 +60,7 @@ pub struct Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Possible origins of a contact.
|
/// Possible origins of a contact.
|
||||||
#[derive(
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive)]
|
||||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
|
||||||
)]
|
|
||||||
#[repr(i32)]
|
#[repr(i32)]
|
||||||
pub enum Origin {
|
pub enum Origin {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
@@ -101,13 +98,29 @@ pub enum Origin {
|
|||||||
ManuallyCreated = 0x4000000,
|
ManuallyCreated = 0x4000000,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Origin {
|
impl ToSql for Origin {
|
||||||
fn default() -> Self {
|
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
||||||
Origin::Unknown
|
let num: i64 = self
|
||||||
|
.to_i64()
|
||||||
|
.expect("impossible: Origin -> i64 conversion failed");
|
||||||
|
|
||||||
|
Ok(ToSqlOutput::Owned(Value::Integer(num)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromSql for Origin {
|
||||||
|
fn column_result(col: ValueRef) -> FromSqlResult<Self> {
|
||||||
|
let inner = FromSql::column_result(col)?;
|
||||||
|
FromPrimitive::from_i64(inner).ok_or(FromSqlError::InvalidType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Origin {
|
impl Origin {
|
||||||
|
/// Contacts that start a new "normal" chat, defaults to off.
|
||||||
|
pub fn is_start_new_chat(self) -> bool {
|
||||||
|
self as i32 >= 0x7FFFFFFF
|
||||||
|
}
|
||||||
|
|
||||||
/// Contacts that are verified and known not to be spam.
|
/// Contacts that are verified and known not to be spam.
|
||||||
pub fn is_verified(self) -> bool {
|
pub fn is_verified(self) -> bool {
|
||||||
self as i32 >= 0x100
|
self as i32 >= 0x100
|
||||||
@@ -126,7 +139,7 @@ pub enum Modifier {
|
|||||||
Created,
|
Created,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum VerifiedStatus {
|
pub enum VerifiedStatus {
|
||||||
/// Contact is not verified.
|
/// Contact is not verified.
|
||||||
@@ -137,10 +150,11 @@ pub enum VerifiedStatus {
|
|||||||
BidirectVerified = 2,
|
BidirectVerified = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Contact {
|
impl<'a> Contact<'a> {
|
||||||
pub fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
|
pub fn load_from_db(context: &'a Context, contact_id: u32) -> Result<Self> {
|
||||||
if contact_id == DC_CONTACT_ID_SELF {
|
if contact_id == DC_CONTACT_ID_SELF as u32 {
|
||||||
let contact = Contact {
|
let contact = Contact {
|
||||||
|
context,
|
||||||
id: contact_id,
|
id: contact_id,
|
||||||
name: context.stock_str(StockMessage::SelfMsg).into(),
|
name: context.stock_str(StockMessage::SelfMsg).into(),
|
||||||
authname: "".into(),
|
authname: "".into(),
|
||||||
@@ -150,16 +164,7 @@ impl Contact {
|
|||||||
blocked: false,
|
blocked: false,
|
||||||
origin: Origin::Unknown,
|
origin: Origin::Unknown,
|
||||||
};
|
};
|
||||||
return Ok(contact);
|
|
||||||
} else if contact_id == DC_CONTACT_ID_DEVICE {
|
|
||||||
let contact = Contact {
|
|
||||||
id: contact_id,
|
|
||||||
name: context.stock_str(StockMessage::DeviceMessages).into(),
|
|
||||||
authname: "".into(),
|
|
||||||
addr: "device@localhost".into(),
|
|
||||||
blocked: false,
|
|
||||||
origin: Origin::Unknown,
|
|
||||||
};
|
|
||||||
return Ok(contact);
|
return Ok(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +173,7 @@ impl Contact {
|
|||||||
params![contact_id as i32],
|
params![contact_id as i32],
|
||||||
|row| {
|
|row| {
|
||||||
let contact = Self {
|
let contact = Self {
|
||||||
|
context,
|
||||||
id: contact_id,
|
id: contact_id,
|
||||||
name: row.get::<_, String>(0)?,
|
name: row.get::<_, String>(0)?,
|
||||||
authname: row.get::<_, String>(4)?,
|
authname: row.get::<_, String>(4)?,
|
||||||
@@ -186,7 +192,7 @@ impl Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a contact is blocked.
|
/// Check if a contact is blocked.
|
||||||
pub fn is_blocked_load(context: &Context, id: u32) -> bool {
|
pub fn is_blocked_load(context: &'a Context, id: u32) -> bool {
|
||||||
Self::load_from_db(context, id)
|
Self::load_from_db(context, id)
|
||||||
.map(|contact| contact.blocked)
|
.map(|contact| contact.blocked)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -205,7 +211,7 @@ impl Contact {
|
|||||||
/// Add a single contact as a result of an _explicit_ user action.
|
/// Add a single contact as a result of an _explicit_ user action.
|
||||||
///
|
///
|
||||||
/// We assume, the contact name, if any, is entered by the user and is used "as is" therefore,
|
/// We assume, the contact name, if any, is entered by the user and is used "as is" therefore,
|
||||||
/// normalize() is *not* called for the name. If the contact is blocked, it is unblocked.
|
/// normalize() is _not_ called for the name. If the contact is blocked, it is unblocked.
|
||||||
///
|
///
|
||||||
/// To add a number of contacts, see `dc_add_address_book()` which is much faster for adding
|
/// To add a number of contacts, see `dc_add_address_book()` which is much faster for adding
|
||||||
/// a bunch of addresses.
|
/// a bunch of addresses.
|
||||||
@@ -220,13 +226,15 @@ impl Contact {
|
|||||||
let (contact_id, sth_modified) =
|
let (contact_id, sth_modified) =
|
||||||
Contact::add_or_lookup(context, name, addr, Origin::ManuallyCreated)?;
|
Contact::add_or_lookup(context, name, addr, Origin::ManuallyCreated)?;
|
||||||
let blocked = Contact::is_blocked_load(context, contact_id);
|
let blocked = Contact::is_blocked_load(context, contact_id);
|
||||||
context.call_cb(Event::ContactsChanged(
|
context.call_cb(
|
||||||
if sth_modified == Modifier::Created {
|
Event::CONTACTS_CHANGED,
|
||||||
Some(contact_id)
|
(if sth_modified == Modifier::Created {
|
||||||
|
contact_id
|
||||||
} else {
|
} else {
|
||||||
None
|
0
|
||||||
},
|
}) as uintptr_t,
|
||||||
));
|
0 as uintptr_t,
|
||||||
|
);
|
||||||
if blocked {
|
if blocked {
|
||||||
Contact::unblock(context, contact_id);
|
Contact::unblock(context, contact_id);
|
||||||
}
|
}
|
||||||
@@ -235,7 +243,7 @@ impl Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Mark all messages sent by the given contact
|
/// Mark all messages sent by the given contact
|
||||||
/// as *noticed*. See also dc_marknoticed_chat() and dc_markseen_msgs()
|
/// as _noticed_. See also dc_marknoticed_chat() and dc_markseen_msgs()
|
||||||
///
|
///
|
||||||
/// Calling this function usually results in the event `#DC_EVENT_MSGS_CHANGED`.
|
/// Calling this function usually results in the event `#DC_EVENT_MSGS_CHANGED`.
|
||||||
pub fn mark_noticed(context: &Context, id: u32) {
|
pub fn mark_noticed(context: &Context, id: u32) {
|
||||||
@@ -243,14 +251,11 @@ impl Contact {
|
|||||||
context,
|
context,
|
||||||
&context.sql,
|
&context.sql,
|
||||||
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
|
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
|
||||||
params![MessageState::InNoticed, id as i32, MessageState::InFresh],
|
params![DC_STATE_IN_NOTICED, id as i32, DC_STATE_IN_FRESH],
|
||||||
)
|
)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
context.call_cb(Event::MsgsChanged {
|
context.call_cb(Event::MSGS_CHANGED, 0, 0);
|
||||||
chat_id: 0,
|
|
||||||
msg_id: MsgId::new(0),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,11 +274,11 @@ impl Contact {
|
|||||||
.get_config(Config::ConfiguredAddr)
|
.get_config(Config::ConfiguredAddr)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if addr_cmp(addr_normalized, addr_self) {
|
if addr_normalized == addr_self {
|
||||||
return DC_CONTACT_ID_SELF;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.sql.query_get_value(
|
context.sql.query_row_col(
|
||||||
context,
|
context,
|
||||||
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
|
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||||
params![
|
params![
|
||||||
@@ -281,6 +286,7 @@ impl Contact {
|
|||||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||||
DC_ORIGIN_MIN_CONTACT_LIST,
|
DC_ORIGIN_MIN_CONTACT_LIST,
|
||||||
],
|
],
|
||||||
|
0
|
||||||
).unwrap_or_default()
|
).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,13 +312,14 @@ impl Contact {
|
|||||||
.get_config(Config::ConfiguredAddr)
|
.get_config(Config::ConfiguredAddr)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if addr_cmp(addr, addr_self) {
|
if addr == addr_self {
|
||||||
return Ok((DC_CONTACT_ID_SELF, sth_modified));
|
return Ok((1, sth_modified));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !may_be_valid_addr(&addr) {
|
if !may_be_valid_addr(&addr) {
|
||||||
warn!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
|
0,
|
||||||
"Bad address \"{}\" for contact \"{}\".",
|
"Bad address \"{}\" for contact \"{}\".",
|
||||||
addr,
|
addr,
|
||||||
if !name.as_ref().is_empty() {
|
if !name.as_ref().is_empty() {
|
||||||
@@ -321,7 +328,7 @@ impl Contact {
|
|||||||
"<unset>"
|
"<unset>"
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
bail!("Bad address supplied: {:?}", addr);
|
bail!("Bad address supplied");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut update_addr = false;
|
let mut update_addr = false;
|
||||||
@@ -336,22 +343,19 @@ impl Contact {
|
|||||||
let row_id = row.get(0)?;
|
let row_id = row.get(0)?;
|
||||||
let row_name: String = row.get(1)?;
|
let row_name: String = row.get(1)?;
|
||||||
let row_addr: String = row.get(2)?;
|
let row_addr: String = row.get(2)?;
|
||||||
let row_origin: Origin = row.get(3)?;
|
let row_origin = row.get(3)?;
|
||||||
let row_authname: String = row.get(4)?;
|
let row_authname: String = row.get(4)?;
|
||||||
|
|
||||||
if !name.as_ref().is_empty() {
|
if !name.as_ref().is_empty() && !row_name.is_empty() {
|
||||||
if !row_name.is_empty() {
|
if origin >= row_origin && name.as_ref() != row_name {
|
||||||
if origin >= row_origin && name.as_ref() != row_name {
|
|
||||||
update_name = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
update_name = true;
|
update_name = true;
|
||||||
}
|
}
|
||||||
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
|
} else {
|
||||||
update_authname = true;
|
update_name = true;
|
||||||
}
|
}
|
||||||
|
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
|
||||||
|
update_authname = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -391,23 +395,25 @@ impl Contact {
|
|||||||
context,
|
context,
|
||||||
&context.sql,
|
&context.sql,
|
||||||
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
|
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
|
||||||
params![name.as_ref(), Chattype::Single, row_id]
|
params![name.as_ref(), 100, row_id]
|
||||||
).ok();
|
).ok();
|
||||||
}
|
}
|
||||||
sth_modified = Modifier::Modified;
|
sth_modified = Modifier::Modified;
|
||||||
}
|
}
|
||||||
} else if sql::execute(
|
|
||||||
context,
|
|
||||||
&context.sql,
|
|
||||||
"INSERT INTO contacts (name, addr, origin) VALUES(?, ?, ?);",
|
|
||||||
params![name.as_ref(), addr, origin,],
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
row_id = sql::get_rowid(context, &context.sql, "contacts", "addr", addr);
|
|
||||||
sth_modified = Modifier::Created;
|
|
||||||
} else {
|
} else {
|
||||||
error!(context, "Cannot add contact.");
|
if sql::execute(
|
||||||
|
context,
|
||||||
|
&context.sql,
|
||||||
|
"INSERT INTO contacts (name, addr, origin) VALUES(?, ?, ?);",
|
||||||
|
params![name.as_ref(), addr, origin,],
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
row_id = sql::get_rowid(context, &context.sql, "contacts", "addr", addr);
|
||||||
|
sth_modified = Modifier::Created;
|
||||||
|
} else {
|
||||||
|
error!(context, 0, "Cannot add contact.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((row_id, sth_modified))
|
Ok((row_id, sth_modified))
|
||||||
@@ -425,15 +431,21 @@ impl Contact {
|
|||||||
/// the event `DC_EVENT_CONTACTS_CHANGED` is sent.
|
/// the event `DC_EVENT_CONTACTS_CHANGED` is sent.
|
||||||
///
|
///
|
||||||
/// To add a single contact entered by the user, you should prefer `Contact::create`,
|
/// To add a single contact entered by the user, you should prefer `Contact::create`,
|
||||||
/// however, for adding a bunch of addresses, this function is much faster.
|
/// however, for adding a bunch of addresses, this function is _much_ faster.
|
||||||
///
|
///
|
||||||
/// The `addr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
|
/// The `adr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
|
||||||
///
|
///
|
||||||
/// Returns the number of modified contacts.
|
/// Returns the number of modified contacts.
|
||||||
pub fn add_address_book(context: &Context, addr_book: impl AsRef<str>) -> Result<usize> {
|
pub fn add_address_book(context: &Context, adr_book: impl AsRef<str>) -> Result<usize> {
|
||||||
let mut modify_cnt = 0;
|
let mut modify_cnt = 0;
|
||||||
|
|
||||||
for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() {
|
for chunk in &adr_book.as_ref().lines().chunks(2) {
|
||||||
|
let chunk = chunk.collect::<Vec<_>>();
|
||||||
|
if chunk.len() < 2 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let name = chunk[0];
|
||||||
|
let addr = chunk[1];
|
||||||
let name = normalize_name(name);
|
let name = normalize_name(name);
|
||||||
let (_, modified) = Contact::add_or_lookup(context, name, addr, Origin::AdressBook)?;
|
let (_, modified) = Contact::add_or_lookup(context, name, addr, Origin::AdressBook)?;
|
||||||
if modified != Modifier::None {
|
if modified != Modifier::None {
|
||||||
@@ -441,7 +453,7 @@ impl Contact {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if modify_cnt > 0 {
|
if modify_cnt > 0 {
|
||||||
context.call_cb(Event::ContactsChanged(None));
|
context.call_cb(Event::CONTACTS_CHANGED, 0 as uintptr_t, 0 as uintptr_t);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(modify_cnt)
|
Ok(modify_cnt)
|
||||||
@@ -460,17 +472,15 @@ impl Contact {
|
|||||||
context: &Context,
|
context: &Context,
|
||||||
listflags: u32,
|
listflags: u32,
|
||||||
query: Option<impl AsRef<str>>,
|
query: Option<impl AsRef<str>>,
|
||||||
) -> Result<Vec<u32>> {
|
) -> Result<*mut dc_array_t> {
|
||||||
let self_addr = context
|
let self_addr = context
|
||||||
.get_config(Config::ConfiguredAddr)
|
.get_config(Config::ConfiguredAddr)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut add_self = false;
|
let mut add_self = false;
|
||||||
let mut ret = Vec::new();
|
let mut ret = dc_array_t::new(100);
|
||||||
let flag_verified_only = listflags_has(listflags, DC_GCL_VERIFIED_ONLY);
|
|
||||||
let flag_add_self = listflags_has(listflags, DC_GCL_ADD_SELF);
|
|
||||||
|
|
||||||
if flag_verified_only || query.is_some() {
|
if (listflags & DC_GCL_VERIFIED_ONLY) > 0 || query.is_some() {
|
||||||
let s3str_like_cmd = format!(
|
let s3str_like_cmd = format!(
|
||||||
"%{}%",
|
"%{}%",
|
||||||
query
|
query
|
||||||
@@ -494,12 +504,12 @@ impl Contact {
|
|||||||
0x100,
|
0x100,
|
||||||
&s3str_like_cmd,
|
&s3str_like_cmd,
|
||||||
&s3str_like_cmd,
|
&s3str_like_cmd,
|
||||||
if flag_verified_only { 0 } else { 1 },
|
if 0 != listflags & 0x1 { 0 } else { 1 },
|
||||||
],
|
],
|
||||||
|row| row.get::<_, i32>(0),
|
|row| row.get::<_, i32>(0),
|
||||||
|ids| {
|
|ids| {
|
||||||
for id in ids {
|
for id in ids {
|
||||||
ret.push(id? as u32);
|
ret.add_id(id? as u32);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
@@ -527,67 +537,63 @@ impl Contact {
|
|||||||
|row| row.get::<_, i32>(0),
|
|row| row.get::<_, i32>(0),
|
||||||
|ids| {
|
|ids| {
|
||||||
for id in ids {
|
for id in ids {
|
||||||
ret.push(id? as u32);
|
ret.add_id(id? as u32);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if flag_add_self && add_self {
|
if 0 != listflags & DC_GCL_ADD_SELF as u32 && add_self {
|
||||||
ret.push(DC_CONTACT_ID_SELF);
|
ret.add_id(DC_CONTACT_ID_SELF as u32);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ret)
|
Ok(ret.into_raw())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_blocked_cnt(context: &Context) -> usize {
|
pub fn get_blocked_cnt(context: &Context) -> usize {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value::<_, isize>(
|
.query_row_col::<_, isize>(
|
||||||
context,
|
context,
|
||||||
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
|
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
|
||||||
params![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
params![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.unwrap_or_default() as usize
|
.unwrap_or_default() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get blocked contacts.
|
/// Get blocked contacts.
|
||||||
pub fn get_all_blocked(context: &Context) -> Vec<u32> {
|
pub fn get_all_blocked(context: &Context) -> *mut dc_array_t {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.query_map(
|
.query_map(
|
||||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(name||addr),id;",
|
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(name||addr),id;",
|
||||||
params![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
params![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||||
|row| row.get::<_, u32>(0),
|
|row| row.get::<_, i32>(0),
|
||||||
|ids| {
|
|ids| {
|
||||||
ids.collect::<std::result::Result<Vec<_>, _>>()
|
let mut ret = dc_array_t::new(100);
|
||||||
.map_err(Into::into)
|
|
||||||
|
for id in ids {
|
||||||
|
ret.add_id(id? as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ret.into_raw())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap_or_default()
|
.unwrap_or_else(|_| std::ptr::null_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a textual summary of the encryption state for the contact.
|
pub fn get_encrinfo(context: &Context, contact_id: u32) -> String {
|
||||||
///
|
|
||||||
/// This function returns a string explaining the encryption state
|
|
||||||
/// of the contact and if the connection is encrypted the
|
|
||||||
/// fingerprints of the keys involved.
|
|
||||||
pub fn get_encrinfo(context: &Context, contact_id: u32) -> Result<String> {
|
|
||||||
let mut ret = String::new();
|
let mut ret = String::new();
|
||||||
|
|
||||||
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
|
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
|
||||||
let peerstate = Peerstate::from_addr(context, &context.sql, &contact.addr);
|
let peerstate = Peerstate::from_addr(context, &context.sql, &contact.addr);
|
||||||
let loginparam = LoginParam::from_database(context, "configured_");
|
let loginparam = dc_loginparam_read(context, &context.sql, "configured_");
|
||||||
|
|
||||||
let mut self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
|
let mut self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
|
||||||
|
|
||||||
if peerstate.is_some()
|
if peerstate.is_some() && peerstate.as_ref().and_then(|p| p.peek_key(0)).is_some() {
|
||||||
&& peerstate
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|p| p.peek_key(PeerstateVerifiedStatus::Unverified))
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
let peerstate = peerstate.as_ref().unwrap();
|
let peerstate = peerstate.as_ref().unwrap();
|
||||||
let p =
|
let p =
|
||||||
context.stock_str(if peerstate.prefer_encrypt == EncryptPreference::Mutual {
|
context.stock_str(if peerstate.prefer_encrypt == EncryptPreference::Mutual {
|
||||||
@@ -597,7 +603,7 @@ impl Contact {
|
|||||||
});
|
});
|
||||||
ret += &p;
|
ret += &p;
|
||||||
if self_key.is_none() {
|
if self_key.is_none() {
|
||||||
e2ee::ensure_secret_key_exists(context)?;
|
unsafe { dc_ensure_secret_key_exists(context) };
|
||||||
self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
|
self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
|
||||||
}
|
}
|
||||||
let p = context.stock_str(StockMessage::FingerPrints);
|
let p = context.stock_str(StockMessage::FingerPrints);
|
||||||
@@ -607,25 +613,25 @@ impl Contact {
|
|||||||
.map(|k| k.formatted_fingerprint())
|
.map(|k| k.formatted_fingerprint())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let fingerprint_other_verified = peerstate
|
let fingerprint_other_verified = peerstate
|
||||||
.peek_key(PeerstateVerifiedStatus::BidirectVerified)
|
.peek_key(2)
|
||||||
.map(|k| k.formatted_fingerprint())
|
.map(|k| k.formatted_fingerprint())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let fingerprint_other_unverified = peerstate
|
let fingerprint_other_unverified = peerstate
|
||||||
.peek_key(PeerstateVerifiedStatus::Unverified)
|
.peek_key(0)
|
||||||
.map(|k| k.formatted_fingerprint())
|
.map(|k| k.formatted_fingerprint())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if loginparam.addr < peerstate.addr {
|
if peerstate.addr.is_some() && &loginparam.addr < peerstate.addr.as_ref().unwrap() {
|
||||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||||
cat_fingerprint(
|
cat_fingerprint(
|
||||||
&mut ret,
|
&mut ret,
|
||||||
peerstate.addr.clone(),
|
peerstate.addr.as_ref().unwrap(),
|
||||||
&fingerprint_other_verified,
|
&fingerprint_other_verified,
|
||||||
&fingerprint_other_unverified,
|
&fingerprint_other_unverified,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cat_fingerprint(
|
cat_fingerprint(
|
||||||
&mut ret,
|
&mut ret,
|
||||||
peerstate.addr.clone(),
|
peerstate.addr.as_ref().unwrap(),
|
||||||
&fingerprint_other_verified,
|
&fingerprint_other_verified,
|
||||||
&fingerprint_other_unverified,
|
&fingerprint_other_unverified,
|
||||||
);
|
);
|
||||||
@@ -640,7 +646,7 @@ impl Contact {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ret)
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a contact. The contact is deleted from the local device. It may happen that this is not
|
/// Delete a contact. The contact is deleted from the local device. It may happen that this is not
|
||||||
@@ -649,26 +655,28 @@ impl Contact {
|
|||||||
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
|
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
|
||||||
pub fn delete(context: &Context, contact_id: u32) -> Result<()> {
|
pub fn delete(context: &Context, contact_id: u32) -> Result<()> {
|
||||||
ensure!(
|
ensure!(
|
||||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
contact_id > DC_CONTACT_ID_LAST_SPECIAL as u32,
|
||||||
"Can not delete special contact"
|
"Can not delete special contact"
|
||||||
);
|
);
|
||||||
|
|
||||||
let count_contacts: i32 = context
|
let count_contacts: i32 = context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_row_col(
|
||||||
context,
|
context,
|
||||||
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
|
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
|
||||||
params![contact_id as i32],
|
params![contact_id as i32],
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let count_msgs: i32 = if count_contacts > 0 {
|
let count_msgs: i32 = if count_contacts > 0 {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value(
|
.query_row_col(
|
||||||
context,
|
context,
|
||||||
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
|
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
|
||||||
params![contact_id as i32, contact_id as i32],
|
params![contact_id as i32, contact_id as i32],
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
@@ -683,19 +691,19 @@ impl Contact {
|
|||||||
params![contact_id as i32],
|
params![contact_id as i32],
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
context.call_cb(Event::ContactsChanged(None));
|
context.call_cb(Event::CONTACTS_CHANGED, 0, 0);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(context, "delete_contact {} failed ({})", contact_id, err);
|
error!(context, 0, "delete_contact {} failed ({})", contact_id, err);
|
||||||
return Err(err.into());
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"could not delete contact {}, there are {} messages with it", contact_id, count_msgs
|
0, "could not delete contact {}, there are {} messages with it", contact_id, count_msgs
|
||||||
);
|
);
|
||||||
bail!("Could not delete contact with messages in it");
|
bail!("Could not delete contact with messages in it");
|
||||||
}
|
}
|
||||||
@@ -706,7 +714,7 @@ impl Contact {
|
|||||||
/// like "Me" in the selected language and the email address
|
/// like "Me" in the selected language and the email address
|
||||||
/// defined by dc_set_config().
|
/// defined by dc_set_config().
|
||||||
pub fn get_by_id(context: &Context, contact_id: u32) -> Result<Contact> {
|
pub fn get_by_id(context: &Context, contact_id: u32) -> Result<Contact> {
|
||||||
Ok(Contact::load_from_db(context, contact_id)?)
|
Contact::load_from_db(context, contact_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the ID of the contact.
|
/// Get the ID of the contact.
|
||||||
@@ -719,7 +727,6 @@ impl Contact {
|
|||||||
&self.addr
|
&self.addr
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get name authorized by the contact.
|
|
||||||
pub fn get_authname(&self) -> &str {
|
pub fn get_authname(&self) -> &str {
|
||||||
&self.authname
|
&self.authname
|
||||||
}
|
}
|
||||||
@@ -772,11 +779,9 @@ impl Contact {
|
|||||||
/// Get the contact's profile image.
|
/// Get the contact's profile image.
|
||||||
/// This is the image set by each remote user on their own
|
/// This is the image set by each remote user on their own
|
||||||
/// using dc_set_config(context, "selfavatar", image).
|
/// using dc_set_config(context, "selfavatar", image).
|
||||||
pub fn get_profile_image(&self, context: &Context) -> Option<PathBuf> {
|
pub fn get_profile_image(&self) -> Option<String> {
|
||||||
if self.id == DC_CONTACT_ID_SELF {
|
if self.id == DC_CONTACT_ID_SELF as u32 {
|
||||||
if let Some(p) = context.get_config(Config::Selfavatar) {
|
return self.context.get_config(Config::Selfavatar);
|
||||||
return Some(PathBuf::from(p));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// TODO: else get image_abs from contact param
|
// TODO: else get image_abs from contact param
|
||||||
None
|
None
|
||||||
@@ -787,7 +792,7 @@ impl Contact {
|
|||||||
/// and can be used for an fallback avatar with white initials
|
/// and can be used for an fallback avatar with white initials
|
||||||
/// as well as for headlines in bubbles of group chats.
|
/// as well as for headlines in bubbles of group chats.
|
||||||
pub fn get_color(&self) -> u32 {
|
pub fn get_color(&self) -> u32 {
|
||||||
dc_str_to_color(&self.addr)
|
dc_str_to_color_safe(&self.addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a contact was verified. E.g. by a secure-join QR code scan
|
/// Check if a contact was verified. E.g. by a secure-join QR code scan
|
||||||
@@ -795,33 +800,29 @@ impl Contact {
|
|||||||
///
|
///
|
||||||
/// The UI may draw a checkbox or something like that beside verified contacts.
|
/// The UI may draw a checkbox or something like that beside verified contacts.
|
||||||
///
|
///
|
||||||
pub fn is_verified(&self, context: &Context) -> VerifiedStatus {
|
pub fn is_verified(&self) -> VerifiedStatus {
|
||||||
self.is_verified_ex(context, None)
|
self.is_verified_ex(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as `Contact::is_verified` but allows speeding up things
|
/// Same as `Contact::is_verified` but allows speeding up things
|
||||||
/// by adding the peerstate belonging to the contact.
|
/// by adding the peerstate belonging to the contact.
|
||||||
/// If you do not have the peerstate available, it is loaded automatically.
|
/// If you do not have the peerstate available, it is loaded automatically.
|
||||||
pub fn is_verified_ex(
|
pub fn is_verified_ex(&self, peerstate: Option<&Peerstate<'a>>) -> VerifiedStatus {
|
||||||
&self,
|
|
||||||
context: &Context,
|
|
||||||
peerstate: Option<&Peerstate>,
|
|
||||||
) -> VerifiedStatus {
|
|
||||||
// We're always sort of secured-verified as we could verify the key on this device any time with the key
|
// We're always sort of secured-verified as we could verify the key on this device any time with the key
|
||||||
// on this device
|
// on this device
|
||||||
if self.id == DC_CONTACT_ID_SELF {
|
if self.id == DC_CONTACT_ID_SELF as u32 {
|
||||||
return VerifiedStatus::BidirectVerified;
|
return VerifiedStatus::BidirectVerified;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(peerstate) = peerstate {
|
if let Some(peerstate) = peerstate {
|
||||||
if peerstate.verified_key.is_some() {
|
if peerstate.verified_key().is_some() {
|
||||||
return VerifiedStatus::BidirectVerified;
|
return VerifiedStatus::BidirectVerified;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let peerstate = Peerstate::from_addr(context, &context.sql, &self.addr);
|
let peerstate = Peerstate::from_addr(self.context, &self.context.sql, &self.addr);
|
||||||
if let Some(ps) = peerstate {
|
if let Some(ps) = peerstate {
|
||||||
if ps.verified_key.is_some() {
|
if ps.verified_key().is_some() {
|
||||||
return VerifiedStatus::BidirectVerified;
|
return VerifiedStatus::BidirectVerified;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -837,7 +838,7 @@ impl Contact {
|
|||||||
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
|
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
|
||||||
if !contact.addr.is_empty() {
|
if !contact.addr.is_empty() {
|
||||||
let normalized_addr = addr_normalize(addr.as_ref());
|
let normalized_addr = addr_normalize(addr.as_ref());
|
||||||
if contact.addr == normalized_addr {
|
if &contact.addr == &normalized_addr {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -853,10 +854,11 @@ impl Contact {
|
|||||||
|
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.query_get_value::<_, isize>(
|
.query_row_col::<_, isize>(
|
||||||
context,
|
context,
|
||||||
"SELECT COUNT(*) FROM contacts WHERE id>?;",
|
"SELECT COUNT(*) FROM contacts WHERE id>?;",
|
||||||
params![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
params![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.unwrap_or_default() as usize
|
.unwrap_or_default() as usize
|
||||||
}
|
}
|
||||||
@@ -878,7 +880,7 @@ impl Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
|
pub fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
|
||||||
if !context.sql.is_open() || contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
if !context.sql.is_open() || contact_id <= 9 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,18 +904,28 @@ impl Contact {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts first name from full name.
|
pub fn get_first_name<'a>(full_name: &'a str) -> &'a str {
|
||||||
fn get_first_name(full_name: &str) -> &str {
|
|
||||||
full_name.splitn(2, ' ').next().unwrap_or_default()
|
full_name.splitn(2, ' ').next().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns false if addr is an invalid address, otherwise true.
|
/// Returns false if addr is an invalid address, otherwise true.
|
||||||
pub fn may_be_valid_addr(addr: &str) -> bool {
|
pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||||
let res = addr.parse::<EmailAddress>();
|
if addr.is_empty() {
|
||||||
res.is_ok()
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let at = addr.find('@').unwrap_or_default();
|
||||||
|
if at < 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let dot = addr.find('.').unwrap_or_default();
|
||||||
|
if dot < 1 || dot > addr.len() - 3 || dot < at + 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns address with whitespace trimmed and `mailto:` prefix removed.
|
|
||||||
pub fn addr_normalize(addr: &str) -> &str {
|
pub fn addr_normalize(addr: &str) -> &str {
|
||||||
let norm = addr.trim();
|
let norm = addr.trim();
|
||||||
|
|
||||||
@@ -925,34 +937,39 @@ pub fn addr_normalize(addr: &str) -> &str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
|
fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
|
||||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
if contact_id <= 9 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
|
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
|
||||||
if contact.blocked != new_blocking
|
if contact.blocked != new_blocking {
|
||||||
&& sql::execute(
|
if sql::execute(
|
||||||
context,
|
context,
|
||||||
&context.sql,
|
&context.sql,
|
||||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||||
params![new_blocking as i32, contact_id as i32],
|
params![new_blocking as i32, contact_id as i32],
|
||||||
)
|
)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
|
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
|
||||||
// non-destructive blocking->unblocking.
|
// non-destructive blocking->unblocking.
|
||||||
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
|
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
|
||||||
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
|
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
|
||||||
// this would result in recreating the same group...)
|
// this would result in recreating the same group...)
|
||||||
if sql::execute(
|
if sql::execute(
|
||||||
context,
|
context,
|
||||||
&context.sql,
|
&context.sql,
|
||||||
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
|
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
|
||||||
params![new_blocking, 100, contact_id as i32],
|
params![new_blocking, 100, contact_id as i32],
|
||||||
).is_ok() {
|
).is_ok() {
|
||||||
Contact::mark_noticed(context, contact_id);
|
Contact::mark_noticed(context, contact_id);
|
||||||
context.call_cb(Event::ContactsChanged(None));
|
context.call_cb(
|
||||||
|
Event::CONTACTS_CHANGED,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -974,9 +991,9 @@ pub fn normalize_name(full_name: impl AsRef<str>) -> String {
|
|||||||
if len > 0 {
|
if len > 0 {
|
||||||
let firstchar = full_name.as_bytes()[0];
|
let firstchar = full_name.as_bytes()[0];
|
||||||
let lastchar = full_name.as_bytes()[len - 1];
|
let lastchar = full_name.as_bytes()[len - 1];
|
||||||
if firstchar == b'\'' && lastchar == b'\''
|
if firstchar == '\'' as u8 && lastchar == '\'' as u8
|
||||||
|| firstchar == b'\"' && lastchar == b'\"'
|
|| firstchar == '\"' as u8 && lastchar == '\"' as u8
|
||||||
|| firstchar == b'<' && lastchar == b'>'
|
|| firstchar == '<' as u8 && lastchar == '>' as u8
|
||||||
{
|
{
|
||||||
full_name = &full_name[1..len - 1];
|
full_name = &full_name[1..len - 1];
|
||||||
}
|
}
|
||||||
@@ -1022,42 +1039,26 @@ fn cat_fingerprint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
|
pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
|
||||||
let norm1 = addr_normalize(addr1.as_ref()).to_lowercase();
|
let norm1 = addr_normalize(addr1.as_ref());
|
||||||
let norm2 = addr_normalize(addr2.as_ref()).to_lowercase();
|
let norm2 = addr_normalize(addr2.as_ref());
|
||||||
|
|
||||||
norm1 == norm2
|
norm1 == norm2
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn addr_equals_self(context: &Context, addr: impl AsRef<str>) -> bool {
|
pub fn addr_equals_self(context: &Context, addr: impl AsRef<str>) -> bool {
|
||||||
if !addr.as_ref().is_empty() {
|
if !addr.as_ref().is_empty() {
|
||||||
|
let normalized_addr = addr_normalize(addr.as_ref());
|
||||||
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr) {
|
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr) {
|
||||||
return addr_cmp(addr, self_addr);
|
return normalized_addr == self_addr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
|
||||||
book.lines()
|
|
||||||
.chunks(2)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|mut chunk| {
|
|
||||||
let name = chunk.next().unwrap();
|
|
||||||
let addr = match chunk.next() {
|
|
||||||
Some(a) => a,
|
|
||||||
None => return None,
|
|
||||||
};
|
|
||||||
Some((name, addr))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::test_utils::*;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_may_be_valid_addr() {
|
fn test_may_be_valid_addr() {
|
||||||
assert_eq!(may_be_valid_addr(""), false);
|
assert_eq!(may_be_valid_addr(""), false);
|
||||||
@@ -1083,136 +1084,10 @@ mod tests {
|
|||||||
fn test_normalize_addr() {
|
fn test_normalize_addr() {
|
||||||
assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com");
|
assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com");
|
||||||
assert_eq!(addr_normalize(" hello@world.com "), "hello@world.com");
|
assert_eq!(addr_normalize(" hello@world.com "), "hello@world.com");
|
||||||
|
|
||||||
// normalisation preserves case to allow user-defined spelling.
|
|
||||||
// however, case is ignored on addr_cmp()
|
|
||||||
assert_ne!(addr_normalize("John@Doe.com"), "john@doe.com");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_first_name() {
|
fn test_get_first_name() {
|
||||||
assert_eq!(get_first_name("John Doe"), "John");
|
assert_eq!(get_first_name("John Doe"), "John");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_split_address_book() {
|
|
||||||
let book = "Name one\nAddress one\nName two\nAddress two\nrest name";
|
|
||||||
let list = split_address_book(&book);
|
|
||||||
assert_eq!(
|
|
||||||
list,
|
|
||||||
vec![("Name one", "Address one"), ("Name two", "Address two")]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_contacts() {
|
|
||||||
let context = dummy_context();
|
|
||||||
let contacts = Contact::get_all(&context.ctx, 0, Some("some2")).unwrap();
|
|
||||||
assert_eq!(contacts.len(), 0);
|
|
||||||
|
|
||||||
let id = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap();
|
|
||||||
assert_ne!(id, 0);
|
|
||||||
|
|
||||||
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).unwrap();
|
|
||||||
assert_eq!(contacts.len(), 1);
|
|
||||||
|
|
||||||
let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).unwrap();
|
|
||||||
assert_eq!(contacts.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_add_or_lookup() {
|
|
||||||
// add some contacts, this also tests add_address_book()
|
|
||||||
let t = dummy_context();
|
|
||||||
let book = concat!(
|
|
||||||
" Name one \n one@eins.org \n",
|
|
||||||
"Name two\ntwo@deux.net\n",
|
|
||||||
"\nthree@drei.sam\n",
|
|
||||||
"Name two\ntwo@deux.net\n" // should not be added again
|
|
||||||
);
|
|
||||||
assert_eq!(Contact::add_address_book(&t.ctx, book).unwrap(), 3);
|
|
||||||
|
|
||||||
// check first added contact, this does not modify because of lower origin
|
|
||||||
let (contact_id, sth_modified) =
|
|
||||||
Contact::add_or_lookup(&t.ctx, "bla foo", "one@eins.org", Origin::IncomingUnknownTo)
|
|
||||||
.unwrap();
|
|
||||||
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
|
|
||||||
assert_eq!(sth_modified, Modifier::None);
|
|
||||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
|
||||||
assert_eq!(contact.get_id(), contact_id);
|
|
||||||
assert_eq!(contact.get_name(), "Name one");
|
|
||||||
assert_eq!(contact.get_display_name(), "Name one");
|
|
||||||
assert_eq!(contact.get_addr(), "one@eins.org");
|
|
||||||
assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)");
|
|
||||||
|
|
||||||
// modify first added contact
|
|
||||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
|
||||||
&t.ctx,
|
|
||||||
"Real one",
|
|
||||||
" one@eins.org ",
|
|
||||||
Origin::ManuallyCreated,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(contact_id, contact_id_test);
|
|
||||||
assert_eq!(sth_modified, Modifier::Modified);
|
|
||||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
|
||||||
assert_eq!(contact.get_name(), "Real one");
|
|
||||||
assert_eq!(contact.get_addr(), "one@eins.org");
|
|
||||||
assert!(!contact.is_blocked());
|
|
||||||
|
|
||||||
// check third added contact (contact without name)
|
|
||||||
let (contact_id, sth_modified) =
|
|
||||||
Contact::add_or_lookup(&t.ctx, "", "three@drei.sam", Origin::IncomingUnknownTo)
|
|
||||||
.unwrap();
|
|
||||||
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
|
|
||||||
assert_eq!(sth_modified, Modifier::None);
|
|
||||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
|
||||||
assert_eq!(contact.get_name(), "");
|
|
||||||
assert_eq!(contact.get_display_name(), "three@drei.sam");
|
|
||||||
assert_eq!(contact.get_addr(), "three@drei.sam");
|
|
||||||
assert_eq!(contact.get_name_n_addr(), "three@drei.sam");
|
|
||||||
|
|
||||||
// add name to third contact from incoming message (this becomes authorized name)
|
|
||||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
|
||||||
&t.ctx,
|
|
||||||
"m. serious",
|
|
||||||
"three@drei.sam",
|
|
||||||
Origin::IncomingUnknownFrom,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(contact_id, contact_id_test);
|
|
||||||
assert_eq!(sth_modified, Modifier::Modified);
|
|
||||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
|
||||||
assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)");
|
|
||||||
assert!(!contact.is_blocked());
|
|
||||||
|
|
||||||
// manually edit name of third contact (does not changed authorized name)
|
|
||||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
|
||||||
&t.ctx,
|
|
||||||
"schnucki",
|
|
||||||
"three@drei.sam",
|
|
||||||
Origin::ManuallyCreated,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(contact_id, contact_id_test);
|
|
||||||
assert_eq!(sth_modified, Modifier::Modified);
|
|
||||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
|
||||||
assert_eq!(contact.get_authname(), "m. serious");
|
|
||||||
assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)");
|
|
||||||
assert!(!contact.is_blocked());
|
|
||||||
|
|
||||||
// check SELF
|
|
||||||
let contact = Contact::load_from_db(&t.ctx, DC_CONTACT_ID_SELF).unwrap();
|
|
||||||
assert_eq!(DC_CONTACT_ID_SELF, 1);
|
|
||||||
assert_eq!(contact.get_name(), t.ctx.stock_str(StockMessage::SelfMsg));
|
|
||||||
assert_eq!(contact.get_addr(), ""); // we're not configured
|
|
||||||
assert!(!contact.is_blocked());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_addr_cmp() {
|
|
||||||
assert!(addr_cmp("AA@AA.ORG", "aa@aa.ORG"));
|
|
||||||
assert!(addr_cmp(" aa@aa.ORG ", "AA@AA.ORG"));
|
|
||||||
assert!(addr_cmp(" mailto:AA@AA.ORG", "Aa@Aa.orG"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
1052
src/context.rs
1052
src/context.rs
File diff suppressed because it is too large
Load Diff
409
src/dc_array.rs
409
src/dc_array.rs
@@ -1,11 +1,13 @@
|
|||||||
use crate::location::Location;
|
use crate::dc_location::dc_location;
|
||||||
|
use crate::dc_tools::*;
|
||||||
|
use crate::types::*;
|
||||||
|
|
||||||
/* * the structure behind dc_array_t */
|
/* * the structure behind dc_array_t */
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub enum dc_array_t {
|
pub enum dc_array_t {
|
||||||
Locations(Vec<Location>),
|
Locations(Vec<dc_location>),
|
||||||
Uint(Vec<u32>),
|
Uint(Vec<uintptr_t>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl dc_array_t {
|
impl dc_array_t {
|
||||||
@@ -18,15 +20,23 @@ impl dc_array_t {
|
|||||||
dc_array_t::Locations(Vec::with_capacity(capacity))
|
dc_array_t::Locations(Vec::with_capacity(capacity))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_id(&mut self, item: u32) {
|
pub fn into_raw(self) -> *mut Self {
|
||||||
|
Box::into_raw(Box::new(self))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_uint(&mut self, item: uintptr_t) {
|
||||||
if let Self::Uint(array) = self {
|
if let Self::Uint(array) = self {
|
||||||
array.push(item);
|
array.push(item);
|
||||||
} else {
|
} else {
|
||||||
panic!("Attempt to add id to array of other type");
|
panic!("Attempt to add uint to array of other type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_location(&mut self, location: Location) {
|
pub fn add_id(&mut self, item: uint32_t) {
|
||||||
|
self.add_uint(item as uintptr_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_location(&mut self, location: dc_location) {
|
||||||
if let Self::Locations(array) = self {
|
if let Self::Locations(array) = self {
|
||||||
array.push(location)
|
array.push(location)
|
||||||
} else {
|
} else {
|
||||||
@@ -34,14 +44,30 @@ impl dc_array_t {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_id(&self, index: usize) -> u32 {
|
pub fn get_uint(&self, index: usize) -> uintptr_t {
|
||||||
match self {
|
if let Self::Uint(array) = self {
|
||||||
Self::Locations(array) => array[index].location_id,
|
array[index]
|
||||||
Self::Uint(array) => array[index] as u32,
|
} else {
|
||||||
|
panic!("Attempt to get uint from array of other type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_location(&self, index: usize) -> &Location {
|
pub fn get_id(&self, index: usize) -> uint32_t {
|
||||||
|
match self {
|
||||||
|
Self::Locations(array) => array[index].location_id,
|
||||||
|
Self::Uint(array) => array[index] as uint32_t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ptr(&self, index: size_t) -> *mut libc::c_void {
|
||||||
|
if let Self::Uint(array) = self {
|
||||||
|
array[index] as *mut libc::c_void
|
||||||
|
} else {
|
||||||
|
panic!("Not an array of pointers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_location(&self, index: usize) -> &dc_location {
|
||||||
if let Self::Locations(array) = self {
|
if let Self::Locations(array) = self {
|
||||||
&array[index]
|
&array[index]
|
||||||
} else {
|
} else {
|
||||||
@@ -49,6 +75,34 @@ impl dc_array_t {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_latitude(&self, index: usize) -> libc::c_double {
|
||||||
|
self.get_location(index).latitude
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_longitude(&self, index: size_t) -> libc::c_double {
|
||||||
|
self.get_location(index).longitude
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_accuracy(&self, index: size_t) -> libc::c_double {
|
||||||
|
self.get_location(index).accuracy
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_timestamp(&self, index: size_t) -> i64 {
|
||||||
|
self.get_location(index).timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_chat_id(&self, index: size_t) -> uint32_t {
|
||||||
|
self.get_location(index).chat_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_contact_id(&self, index: size_t) -> uint32_t {
|
||||||
|
self.get_location(index).contact_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_msg_id(&self, index: size_t) -> uint32_t {
|
||||||
|
self.get_location(index).msg_id
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Locations(array) => array.is_empty(),
|
Self::Locations(array) => array.is_empty(),
|
||||||
@@ -71,7 +125,7 @@ impl dc_array_t {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search_id(&self, needle: u32) -> Option<usize> {
|
pub fn search_id(&self, needle: uintptr_t) -> Option<usize> {
|
||||||
if let Self::Uint(array) = self {
|
if let Self::Uint(array) = self {
|
||||||
for (i, &u) in array.iter().enumerate() {
|
for (i, &u) in array.iter().enumerate() {
|
||||||
if u == needle {
|
if u == needle {
|
||||||
@@ -91,72 +145,297 @@ impl dc_array_t {
|
|||||||
panic!("Attempt to sort array of something other than uints");
|
panic!("Attempt to sort array of something other than uints");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_ptr(&self) -> *const u32 {
|
|
||||||
if let dc_array_t::Uint(v) = self {
|
|
||||||
v.as_ptr()
|
|
||||||
} else {
|
|
||||||
panic!("Attempt to convert array of something other than uints to raw");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<u32>> for dc_array_t {
|
impl From<Vec<dc_location>> for dc_array_t {
|
||||||
fn from(array: Vec<u32>) -> Self {
|
fn from(array: Vec<dc_location>) -> Self {
|
||||||
dc_array_t::Uint(array)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<Location>> for dc_array_t {
|
|
||||||
fn from(array: Vec<Location>) -> Self {
|
|
||||||
dc_array_t::Locations(array)
|
dc_array_t::Locations(array)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_unref(array: *mut dc_array_t) {
|
||||||
|
if array.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Box::from_raw(array);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_add_uint(array: *mut dc_array_t, item: uintptr_t) {
|
||||||
|
if !array.is_null() {
|
||||||
|
(*array).add_uint(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_add_id(array: *mut dc_array_t, item: uint32_t) {
|
||||||
|
if !array.is_null() {
|
||||||
|
(*array).add_id(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_add_ptr(array: *mut dc_array_t, item: *mut libc::c_void) {
|
||||||
|
dc_array_add_uint(array, item as uintptr_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_cnt(array: *const dc_array_t) -> size_t {
|
||||||
|
if array.is_null() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(*array).len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_uint(array: *const dc_array_t, index: size_t) -> uintptr_t {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(*array).get_uint(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_id(array: *const dc_array_t, index: size_t) -> uint32_t {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(*array).get_id(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_ptr(array: *const dc_array_t, index: size_t) -> *mut libc::c_void {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
std::ptr::null_mut()
|
||||||
|
} else {
|
||||||
|
(*array).get_ptr(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_latitude(array: *const dc_array_t, index: size_t) -> libc::c_double {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(*array).get_latitude(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_longitude(array: *const dc_array_t, index: size_t) -> libc::c_double {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(*array).get_longitude(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_accuracy(array: *const dc_array_t, index: size_t) -> libc::c_double {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(*array).get_accuracy(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_timestamp(array: *const dc_array_t, index: size_t) -> i64 {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(*array).get_timestamp(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_chat_id(array: *const dc_array_t, index: size_t) -> uint32_t {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(*array).get_chat_id(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_contact_id(array: *const dc_array_t, index: size_t) -> uint32_t {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(*array).get_contact_id(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_msg_id(array: *const dc_array_t, index: size_t) -> uint32_t {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(*array).get_msg_id(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_marker(array: *const dc_array_t, index: size_t) -> *mut libc::c_char {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
return std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let dc_array_t::Locations(v) = &*array {
|
||||||
|
if let Some(s) = &v[index].marker {
|
||||||
|
s.strdup()
|
||||||
|
} else {
|
||||||
|
std::ptr::null_mut()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the independent-state of the location at the given index.
|
||||||
|
* Independent locations do not belong to the track of the user.
|
||||||
|
*
|
||||||
|
* @memberof dc_array_t
|
||||||
|
* @param array The array object.
|
||||||
|
* @param index Index of the item. Must be between 0 and dc_array_get_cnt()-1.
|
||||||
|
* @return 0=Location belongs to the track of the user,
|
||||||
|
* 1=Location was reported independently.
|
||||||
|
*/
|
||||||
|
pub unsafe fn dc_array_is_independent(array: *const dc_array_t, index: size_t) -> libc::c_int {
|
||||||
|
if array.is_null() || index >= (*array).len() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let dc_array_t::Locations(v) = &*array {
|
||||||
|
v[index].independent as libc::c_int
|
||||||
|
} else {
|
||||||
|
panic!("Attempt to get location independent field from array of something other than locations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_search_id(
|
||||||
|
array: *const dc_array_t,
|
||||||
|
needle: uint32_t,
|
||||||
|
ret_index: *mut size_t,
|
||||||
|
) -> bool {
|
||||||
|
if array.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(i) = (*array).search_id(needle as uintptr_t) {
|
||||||
|
if !ret_index.is_null() {
|
||||||
|
*ret_index = i
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_raw(array: *const dc_array_t) -> *const uintptr_t {
|
||||||
|
if array.is_null() {
|
||||||
|
return 0 as *const uintptr_t;
|
||||||
|
}
|
||||||
|
if let dc_array_t::Uint(v) = &*array {
|
||||||
|
v.as_ptr()
|
||||||
|
} else {
|
||||||
|
panic!("Attempt to convert array of something other than uints to raw");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_array_new(initsize: size_t) -> *mut dc_array_t {
|
||||||
|
dc_array_t::new(initsize).into_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_array_new_locations(initsize: size_t) -> *mut dc_array_t {
|
||||||
|
dc_array_t::new_locations(initsize).into_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_empty(array: *mut dc_array_t) {
|
||||||
|
if array.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(*array).clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_duplicate(array: *const dc_array_t) -> *mut dc_array_t {
|
||||||
|
if array.is_null() {
|
||||||
|
std::ptr::null_mut()
|
||||||
|
} else {
|
||||||
|
(*array).clone().into_raw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_array_get_string(
|
||||||
|
array: *const dc_array_t,
|
||||||
|
sep: *const libc::c_char,
|
||||||
|
) -> *mut libc::c_char {
|
||||||
|
if array.is_null() || sep.is_null() {
|
||||||
|
return dc_strdup(b"\x00" as *const u8 as *const libc::c_char);
|
||||||
|
}
|
||||||
|
if let dc_array_t::Uint(v) = &*array {
|
||||||
|
let cnt = v.len();
|
||||||
|
let sep = as_str(sep);
|
||||||
|
|
||||||
|
let res = v
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.fold(String::with_capacity(2 * cnt), |res, (i, n)| {
|
||||||
|
if i == 0 {
|
||||||
|
res + &n.to_string()
|
||||||
|
} else {
|
||||||
|
res + sep + &n.to_string()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.strdup()
|
||||||
|
} else {
|
||||||
|
panic!("Attempt to get string from array of other type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::x::*;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dc_array() {
|
fn test_dc_array() {
|
||||||
let mut arr = dc_array_t::new(7);
|
unsafe {
|
||||||
assert!(arr.is_empty());
|
let arr = dc_array_new(7 as size_t);
|
||||||
|
assert_eq!(dc_array_get_cnt(arr), 0);
|
||||||
|
|
||||||
for i in 0..1000 {
|
for i in 0..1000 {
|
||||||
arr.add_id(i + 2);
|
dc_array_add_id(arr, (i + 2) as uint32_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(dc_array_get_cnt(arr), 1000);
|
||||||
|
|
||||||
|
for i in 0..1000 {
|
||||||
|
assert_eq!(
|
||||||
|
dc_array_get_id(arr, i as size_t),
|
||||||
|
(i + 1i32 * 2i32) as libc::c_uint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(dc_array_get_id(arr, -1i32 as size_t), 0);
|
||||||
|
assert_eq!(dc_array_get_id(arr, 1000 as size_t), 0);
|
||||||
|
assert_eq!(dc_array_get_id(arr, 1001 as size_t), 0);
|
||||||
|
|
||||||
|
dc_array_empty(arr);
|
||||||
|
|
||||||
|
assert_eq!(dc_array_get_cnt(arr), 0);
|
||||||
|
|
||||||
|
dc_array_add_id(arr, 13 as uint32_t);
|
||||||
|
dc_array_add_id(arr, 7 as uint32_t);
|
||||||
|
dc_array_add_id(arr, 666 as uint32_t);
|
||||||
|
dc_array_add_id(arr, 0 as uint32_t);
|
||||||
|
dc_array_add_id(arr, 5000 as uint32_t);
|
||||||
|
|
||||||
|
(*arr).sort_ids();
|
||||||
|
|
||||||
|
assert_eq!(dc_array_get_id(arr, 0 as size_t), 0);
|
||||||
|
assert_eq!(dc_array_get_id(arr, 1 as size_t), 7);
|
||||||
|
assert_eq!(dc_array_get_id(arr, 2 as size_t), 13);
|
||||||
|
assert_eq!(dc_array_get_id(arr, 3 as size_t), 666);
|
||||||
|
|
||||||
|
let str = dc_array_get_string(arr, b"-\x00" as *const u8 as *const libc::c_char);
|
||||||
|
assert_eq!(
|
||||||
|
CStr::from_ptr(str as *const libc::c_char).to_str().unwrap(),
|
||||||
|
"0-7-13-666-5000"
|
||||||
|
);
|
||||||
|
free(str as *mut libc::c_void);
|
||||||
|
|
||||||
|
dc_array_unref(arr);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(arr.len(), 1000);
|
|
||||||
|
|
||||||
for i in 0..1000 {
|
|
||||||
assert_eq!(arr.get_id(i), (i + 2) as u32);
|
|
||||||
}
|
|
||||||
|
|
||||||
arr.clear();
|
|
||||||
|
|
||||||
assert!(arr.is_empty());
|
|
||||||
|
|
||||||
arr.add_id(13);
|
|
||||||
arr.add_id(7);
|
|
||||||
arr.add_id(666);
|
|
||||||
arr.add_id(0);
|
|
||||||
arr.add_id(5000);
|
|
||||||
|
|
||||||
arr.sort_ids();
|
|
||||||
|
|
||||||
assert_eq!(arr.get_id(0), 0);
|
|
||||||
assert_eq!(arr.get_id(1), 7);
|
|
||||||
assert_eq!(arr.get_id(2), 13);
|
|
||||||
assert_eq!(arr.get_id(3), 666);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_dc_array_out_of_bounds() {
|
|
||||||
let mut arr = dc_array_t::new(7);
|
|
||||||
for i in 0..1000 {
|
|
||||||
arr.add_id(i + 2);
|
|
||||||
}
|
|
||||||
arr.get_id(1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
2275
src/dc_chat.rs
Normal file
2275
src/dc_chat.rs
Normal file
File diff suppressed because it is too large
Load Diff
1535
src/dc_configure.rs
Normal file
1535
src/dc_configure.rs
Normal file
File diff suppressed because it is too large
Load Diff
151
src/dc_dehtml.rs
Normal file
151
src/dc_dehtml.rs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
use crate::dc_saxparser::*;
|
||||||
|
use crate::dc_tools::*;
|
||||||
|
use crate::x::*;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref LINE_RE: regex::Regex = regex::Regex::new(r"(\r?\n)+").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Dehtml {
|
||||||
|
strbuilder: String,
|
||||||
|
add_text: AddText,
|
||||||
|
last_href: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
enum AddText {
|
||||||
|
No,
|
||||||
|
YesRemoveLineEnds,
|
||||||
|
YesPreserveLineEnds,
|
||||||
|
}
|
||||||
|
|
||||||
|
// dc_dehtml() returns way too many lineends; however, an optimisation on this issue is not needed as
|
||||||
|
// the lineends are typically remove in further processing by the caller
|
||||||
|
pub unsafe fn dc_dehtml(buf_terminated: *mut libc::c_char) -> *mut libc::c_char {
|
||||||
|
dc_trim(buf_terminated);
|
||||||
|
if *buf_terminated.offset(0isize) as libc::c_int == 0i32 {
|
||||||
|
return dc_strdup(b"\x00" as *const u8 as *const libc::c_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dehtml = Dehtml {
|
||||||
|
strbuilder: String::with_capacity(strlen(buf_terminated)),
|
||||||
|
add_text: AddText::YesRemoveLineEnds,
|
||||||
|
last_href: None,
|
||||||
|
};
|
||||||
|
let mut saxparser = dc_saxparser_t {
|
||||||
|
starttag_cb: None,
|
||||||
|
endtag_cb: None,
|
||||||
|
text_cb: None,
|
||||||
|
userdata: 0 as *mut libc::c_void,
|
||||||
|
};
|
||||||
|
dc_saxparser_init(
|
||||||
|
&mut saxparser,
|
||||||
|
&mut dehtml as *mut Dehtml as *mut libc::c_void,
|
||||||
|
);
|
||||||
|
dc_saxparser_set_tag_handler(
|
||||||
|
&mut saxparser,
|
||||||
|
Some(dehtml_starttag_cb),
|
||||||
|
Some(dehtml_endtag_cb),
|
||||||
|
);
|
||||||
|
dc_saxparser_set_text_handler(&mut saxparser, Some(dehtml_text_cb));
|
||||||
|
dc_saxparser_parse(&mut saxparser, buf_terminated);
|
||||||
|
|
||||||
|
dehtml.strbuilder.strdup()
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn dehtml_text_cb(
|
||||||
|
userdata: *mut libc::c_void,
|
||||||
|
text: *const libc::c_char,
|
||||||
|
_len: libc::c_int,
|
||||||
|
) {
|
||||||
|
let dehtml = &mut *(userdata as *mut Dehtml);
|
||||||
|
|
||||||
|
if dehtml.add_text == AddText::YesPreserveLineEnds
|
||||||
|
|| dehtml.add_text == AddText::YesRemoveLineEnds
|
||||||
|
{
|
||||||
|
let last_added = std::ffi::CStr::from_ptr(text)
|
||||||
|
.to_str()
|
||||||
|
.expect("invalid utf8");
|
||||||
|
// TODO: why does len does not match?
|
||||||
|
// assert_eq!(last_added.len(), len as usize);
|
||||||
|
|
||||||
|
if dehtml.add_text == AddText::YesRemoveLineEnds {
|
||||||
|
dehtml.strbuilder += LINE_RE.replace_all(last_added.as_ref(), "\r").as_ref();
|
||||||
|
} else {
|
||||||
|
dehtml.strbuilder += last_added.as_ref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn dehtml_endtag_cb(userdata: *mut libc::c_void, tag: *const libc::c_char) {
|
||||||
|
let mut dehtml = &mut *(userdata as *mut Dehtml);
|
||||||
|
let tag = std::ffi::CStr::from_ptr(tag).to_string_lossy();
|
||||||
|
|
||||||
|
match tag.as_ref() {
|
||||||
|
"p" | "div" | "table" | "td" | "style" | "script" | "title" | "pre" => {
|
||||||
|
dehtml.strbuilder += "\n\n";
|
||||||
|
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||||
|
}
|
||||||
|
"a" => {
|
||||||
|
if let Some(ref last_href) = dehtml.last_href.take() {
|
||||||
|
dehtml.strbuilder += "](";
|
||||||
|
dehtml.strbuilder += last_href;
|
||||||
|
dehtml.strbuilder += ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"b" | "strong" => {
|
||||||
|
dehtml.strbuilder += "*";
|
||||||
|
}
|
||||||
|
"i" | "em" => {
|
||||||
|
dehtml.strbuilder += "_";
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn dehtml_starttag_cb(
|
||||||
|
userdata: *mut libc::c_void,
|
||||||
|
tag: *const libc::c_char,
|
||||||
|
attr: *mut *mut libc::c_char,
|
||||||
|
) {
|
||||||
|
let mut dehtml = &mut *(userdata as *mut Dehtml);
|
||||||
|
let tag = std::ffi::CStr::from_ptr(tag).to_string_lossy();
|
||||||
|
|
||||||
|
match tag.as_ref() {
|
||||||
|
"p" | "div" | "table" | "td" => {
|
||||||
|
dehtml.strbuilder += "\n\n";
|
||||||
|
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||||
|
}
|
||||||
|
"br" => {
|
||||||
|
dehtml.strbuilder += "\n";
|
||||||
|
dehtml.add_text = AddText::YesRemoveLineEnds;
|
||||||
|
}
|
||||||
|
"style" | "script" | "title" => {
|
||||||
|
dehtml.add_text = AddText::No;
|
||||||
|
}
|
||||||
|
"pre" => {
|
||||||
|
dehtml.strbuilder += "\n\n";
|
||||||
|
dehtml.add_text = AddText::YesPreserveLineEnds;
|
||||||
|
}
|
||||||
|
"a" => {
|
||||||
|
let text_c = std::ffi::CStr::from_ptr(dc_attr_find(
|
||||||
|
attr,
|
||||||
|
b"href\x00" as *const u8 as *const libc::c_char,
|
||||||
|
));
|
||||||
|
let text_r = text_c.to_str().expect("invalid utf8");
|
||||||
|
if !text_r.is_empty() {
|
||||||
|
dehtml.last_href = Some(text_r.to_string());
|
||||||
|
dehtml.strbuilder += "[";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"b" | "strong" => {
|
||||||
|
dehtml.strbuilder += "*";
|
||||||
|
}
|
||||||
|
"i" | "em" => {
|
||||||
|
dehtml.strbuilder += "_";
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
1124
src/dc_e2ee.rs
Normal file
1124
src/dc_e2ee.rs
Normal file
File diff suppressed because it is too large
Load Diff
1479
src/dc_imex.rs
Normal file
1479
src/dc_imex.rs
Normal file
File diff suppressed because it is too large
Load Diff
1285
src/dc_job.rs
Normal file
1285
src/dc_job.rs
Normal file
File diff suppressed because it is too large
Load Diff
209
src/dc_jobthread.rs
Normal file
209
src/dc_jobthread.rs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
use std::sync::{Arc, Condvar, Mutex};
|
||||||
|
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::dc_configure::*;
|
||||||
|
use crate::imap::Imap;
|
||||||
|
use crate::x::*;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct dc_jobthread_t {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub folder_config_name: &'static str,
|
||||||
|
pub imap: Imap,
|
||||||
|
pub state: Arc<(Mutex<JobState>, Condvar)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_jobthread_init(
|
||||||
|
name: &'static str,
|
||||||
|
folder_config_name: &'static str,
|
||||||
|
imap: Imap,
|
||||||
|
) -> dc_jobthread_t {
|
||||||
|
dc_jobthread_t {
|
||||||
|
name,
|
||||||
|
folder_config_name,
|
||||||
|
imap,
|
||||||
|
state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct JobState {
|
||||||
|
idle: bool,
|
||||||
|
jobs_needed: i32,
|
||||||
|
suspended: i32,
|
||||||
|
using_handle: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_jobthread_suspend(
|
||||||
|
context: &Context,
|
||||||
|
jobthread: &dc_jobthread_t,
|
||||||
|
suspend: libc::c_int,
|
||||||
|
) {
|
||||||
|
if 0 != suspend {
|
||||||
|
info!(context, 0, "Suspending {}-thread.", jobthread.name,);
|
||||||
|
{
|
||||||
|
jobthread.state.0.lock().unwrap().suspended = 1;
|
||||||
|
}
|
||||||
|
dc_jobthread_interrupt_idle(context, jobthread);
|
||||||
|
loop {
|
||||||
|
let using_handle = jobthread.state.0.lock().unwrap().using_handle;
|
||||||
|
if using_handle == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_micros(300 * 1000));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!(context, 0, "Unsuspending {}-thread.", jobthread.name);
|
||||||
|
|
||||||
|
let &(ref lock, ref cvar) = &*jobthread.state.clone();
|
||||||
|
let mut state = lock.lock().unwrap();
|
||||||
|
|
||||||
|
state.suspended = 0;
|
||||||
|
state.idle = true;
|
||||||
|
cvar.notify_one();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_jobthread_interrupt_idle(context: &Context, jobthread: &dc_jobthread_t) {
|
||||||
|
{
|
||||||
|
jobthread.state.0.lock().unwrap().jobs_needed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(context, 0, "Interrupting {}-IDLE...", jobthread.name);
|
||||||
|
|
||||||
|
jobthread.imap.interrupt_idle();
|
||||||
|
|
||||||
|
let &(ref lock, ref cvar) = &*jobthread.state.clone();
|
||||||
|
let mut state = lock.lock().unwrap();
|
||||||
|
|
||||||
|
state.idle = true;
|
||||||
|
cvar.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_jobthread_fetch(
|
||||||
|
context: &Context,
|
||||||
|
jobthread: &mut dc_jobthread_t,
|
||||||
|
use_network: libc::c_int,
|
||||||
|
) {
|
||||||
|
let start;
|
||||||
|
|
||||||
|
{
|
||||||
|
let &(ref lock, _) = &*jobthread.state.clone();
|
||||||
|
let mut state = lock.lock().unwrap();
|
||||||
|
|
||||||
|
if 0 != state.suspended {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.using_handle = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if 0 != use_network {
|
||||||
|
start = clock();
|
||||||
|
if !(0 == connect_to_imap(context, jobthread)) {
|
||||||
|
info!(context, 0, "{}-fetch started...", jobthread.name);
|
||||||
|
jobthread.imap.fetch(context);
|
||||||
|
|
||||||
|
if jobthread.imap.should_reconnect() {
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
0, "{}-fetch aborted, starting over...", jobthread.name,
|
||||||
|
);
|
||||||
|
jobthread.imap.fetch(context);
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
"{}-fetch done in {:.3} ms.",
|
||||||
|
jobthread.name,
|
||||||
|
clock().wrapping_sub(start) as f64 / 1000.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jobthread.state.0.lock().unwrap().using_handle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ******************************************************************************
|
||||||
|
* the typical fetch, idle, interrupt-idle
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
unsafe fn connect_to_imap(context: &Context, jobthread: &dc_jobthread_t) -> libc::c_int {
|
||||||
|
if jobthread.imap.is_connected() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ret_connected = dc_connect_to_configured_imap(context, &jobthread.imap);
|
||||||
|
|
||||||
|
if !(0 == ret_connected) {
|
||||||
|
if context
|
||||||
|
.sql
|
||||||
|
.get_config_int(context, "folders_configured")
|
||||||
|
.unwrap_or_default()
|
||||||
|
< 3
|
||||||
|
{
|
||||||
|
jobthread.imap.configure_folders(context, 0x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mvbox_name) = context
|
||||||
|
.sql
|
||||||
|
.get_config(context, jobthread.folder_config_name)
|
||||||
|
{
|
||||||
|
jobthread.imap.set_watch_folder(mvbox_name);
|
||||||
|
} else {
|
||||||
|
jobthread.imap.disconnect(context);
|
||||||
|
ret_connected = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret_connected
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_jobthread_idle(
|
||||||
|
context: &Context,
|
||||||
|
jobthread: &dc_jobthread_t,
|
||||||
|
use_network: libc::c_int,
|
||||||
|
) {
|
||||||
|
{
|
||||||
|
let &(ref lock, ref cvar) = &*jobthread.state.clone();
|
||||||
|
let mut state = lock.lock().unwrap();
|
||||||
|
|
||||||
|
if 0 != state.jobs_needed {
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
"{}-IDLE will not be started as it was interrupted while not ideling.",
|
||||||
|
jobthread.name,
|
||||||
|
);
|
||||||
|
state.jobs_needed = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if 0 != state.suspended {
|
||||||
|
while !state.idle {
|
||||||
|
state = cvar.wait(state).unwrap();
|
||||||
|
}
|
||||||
|
state.idle = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.using_handle = 1;
|
||||||
|
|
||||||
|
if 0 == use_network {
|
||||||
|
state.using_handle = 0;
|
||||||
|
|
||||||
|
while !state.idle {
|
||||||
|
state = cvar.wait(state).unwrap();
|
||||||
|
}
|
||||||
|
state.idle = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect_to_imap(context, jobthread);
|
||||||
|
info!(context, 0, "{}-IDLE started...", jobthread.name,);
|
||||||
|
jobthread.imap.idle(context);
|
||||||
|
info!(context, 0, "{}-IDLE ended.", jobthread.name);
|
||||||
|
|
||||||
|
jobthread.state.0.lock().unwrap().using_handle = 0;
|
||||||
|
}
|
||||||
754
src/dc_location.rs
Normal file
754
src/dc_location.rs
Normal file
@@ -0,0 +1,754 @@
|
|||||||
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
use crate::constants::Event;
|
||||||
|
use crate::constants::*;
|
||||||
|
use crate::context::*;
|
||||||
|
use crate::dc_array::*;
|
||||||
|
use crate::dc_chat::*;
|
||||||
|
use crate::dc_job::*;
|
||||||
|
use crate::dc_msg::*;
|
||||||
|
use crate::dc_saxparser::*;
|
||||||
|
use crate::dc_tools::*;
|
||||||
|
use crate::param::*;
|
||||||
|
use crate::sql;
|
||||||
|
use crate::stock::StockMessage;
|
||||||
|
use crate::types::*;
|
||||||
|
use crate::x::*;
|
||||||
|
|
||||||
|
// location handling
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub struct dc_location {
|
||||||
|
pub location_id: uint32_t,
|
||||||
|
pub latitude: libc::c_double,
|
||||||
|
pub longitude: libc::c_double,
|
||||||
|
pub accuracy: libc::c_double,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub contact_id: uint32_t,
|
||||||
|
pub msg_id: uint32_t,
|
||||||
|
pub chat_id: uint32_t,
|
||||||
|
pub marker: Option<String>,
|
||||||
|
pub independent: uint32_t,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl dc_location {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
dc_location {
|
||||||
|
location_id: 0,
|
||||||
|
latitude: 0.0,
|
||||||
|
longitude: 0.0,
|
||||||
|
accuracy: 0.0,
|
||||||
|
timestamp: 0,
|
||||||
|
contact_id: 0,
|
||||||
|
msg_id: 0,
|
||||||
|
chat_id: 0,
|
||||||
|
marker: None,
|
||||||
|
independent: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub struct dc_kml_t {
|
||||||
|
pub addr: *mut libc::c_char,
|
||||||
|
pub locations: Option<Vec<dc_location>>,
|
||||||
|
pub tag: libc::c_int,
|
||||||
|
pub curr: dc_location,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl dc_kml_t {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
dc_kml_t {
|
||||||
|
addr: std::ptr::null_mut(),
|
||||||
|
locations: None,
|
||||||
|
tag: 0,
|
||||||
|
curr: dc_location::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// location streaming
|
||||||
|
pub unsafe fn dc_send_locations_to_chat(
|
||||||
|
context: &Context,
|
||||||
|
chat_id: uint32_t,
|
||||||
|
seconds: libc::c_int,
|
||||||
|
) {
|
||||||
|
let now = time();
|
||||||
|
let mut msg: *mut dc_msg_t = 0 as *mut dc_msg_t;
|
||||||
|
let is_sending_locations_before: bool;
|
||||||
|
if !(seconds < 0i32 || chat_id <= 9i32 as libc::c_uint) {
|
||||||
|
is_sending_locations_before = dc_is_sending_locations_to_chat(context, chat_id);
|
||||||
|
if sql::execute(
|
||||||
|
context,
|
||||||
|
&context.sql,
|
||||||
|
"UPDATE chats \
|
||||||
|
SET locations_send_begin=?, \
|
||||||
|
locations_send_until=? \
|
||||||
|
WHERE id=?",
|
||||||
|
params![
|
||||||
|
if 0 != seconds { now } else { 0 },
|
||||||
|
if 0 != seconds {
|
||||||
|
now + seconds as i64
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
chat_id as i32,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
if 0 != seconds && !is_sending_locations_before {
|
||||||
|
msg = dc_msg_new(context, Viewtype::Text);
|
||||||
|
(*msg).text =
|
||||||
|
Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0));
|
||||||
|
(*msg).param.set_int(Param::Cmd, 8);
|
||||||
|
dc_send_msg(context, chat_id, msg);
|
||||||
|
} else if 0 == seconds && is_sending_locations_before {
|
||||||
|
let stock_str = CString::new(context.stock_system_msg(
|
||||||
|
StockMessage::MsgLocationDisabled,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
dc_add_device_msg(context, chat_id, stock_str.as_ptr());
|
||||||
|
}
|
||||||
|
context.call_cb(
|
||||||
|
Event::CHAT_MODIFIED,
|
||||||
|
chat_id as uintptr_t,
|
||||||
|
0i32 as uintptr_t,
|
||||||
|
);
|
||||||
|
if 0 != seconds {
|
||||||
|
schedule_MAYBE_SEND_LOCATIONS(context, 0i32);
|
||||||
|
dc_job_add(
|
||||||
|
context,
|
||||||
|
5007i32,
|
||||||
|
chat_id as libc::c_int,
|
||||||
|
Params::new(),
|
||||||
|
seconds + 1i32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dc_msg_unref(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* job to send locations out to all chats that want them
|
||||||
|
******************************************************************************/
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
unsafe fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, flags: libc::c_int) {
|
||||||
|
if 0 != flags & 0x1 || !dc_job_action_exists(context, 5005) {
|
||||||
|
dc_job_add(context, 5005, 0, Params::new(), 60);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_is_sending_locations_to_chat(context: &Context, chat_id: u32) -> bool {
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.exists(
|
||||||
|
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
|
||||||
|
params![if chat_id == 0 { 1 } else { 0 }, chat_id as i32, time()],
|
||||||
|
)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_set_location(
|
||||||
|
context: &Context,
|
||||||
|
latitude: libc::c_double,
|
||||||
|
longitude: libc::c_double,
|
||||||
|
accuracy: libc::c_double,
|
||||||
|
) -> libc::c_int {
|
||||||
|
if latitude == 0.0 && longitude == 0.0 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.sql.query_map(
|
||||||
|
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||||
|
params![time()], |row| row.get::<_, i32>(0),
|
||||||
|
|chats| {
|
||||||
|
let mut continue_streaming = false;
|
||||||
|
|
||||||
|
for chat in chats {
|
||||||
|
let chat_id = chat?;
|
||||||
|
context.sql.execute(
|
||||||
|
"INSERT INTO locations \
|
||||||
|
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||||
|
params![
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
accuracy,
|
||||||
|
time(),
|
||||||
|
chat_id,
|
||||||
|
1,
|
||||||
|
]
|
||||||
|
)?;
|
||||||
|
continue_streaming = true;
|
||||||
|
}
|
||||||
|
if continue_streaming {
|
||||||
|
context.call_cb(Event::LOCATION_CHANGED, 1, 0);
|
||||||
|
};
|
||||||
|
unsafe { schedule_MAYBE_SEND_LOCATIONS(context, 0) };
|
||||||
|
Ok(continue_streaming as libc::c_int)
|
||||||
|
}
|
||||||
|
).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_get_locations(
|
||||||
|
context: &Context,
|
||||||
|
chat_id: uint32_t,
|
||||||
|
contact_id: uint32_t,
|
||||||
|
timestamp_from: i64,
|
||||||
|
mut timestamp_to: i64,
|
||||||
|
) -> *mut dc_array_t {
|
||||||
|
if timestamp_to == 0 {
|
||||||
|
timestamp_to = time() + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.query_map(
|
||||||
|
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
|
||||||
|
m.id, l.from_id, l.chat_id, m.txt \
|
||||||
|
FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
|
||||||
|
AND (? OR l.from_id=?) \
|
||||||
|
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
|
||||||
|
ORDER BY l.timestamp DESC, l.id DESC, m.id DESC;",
|
||||||
|
params![
|
||||||
|
if chat_id == 0 { 1 } else { 0 },
|
||||||
|
chat_id as i32,
|
||||||
|
if contact_id == 0 { 1 } else { 0 },
|
||||||
|
contact_id as i32,
|
||||||
|
timestamp_from,
|
||||||
|
timestamp_to,
|
||||||
|
],
|
||||||
|
|row| {
|
||||||
|
let msg_id = row.get(6)?;
|
||||||
|
let txt: String = row.get(9)?;
|
||||||
|
let marker = if msg_id != 0 && is_marker(&txt) {
|
||||||
|
Some(txt)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let loc = dc_location {
|
||||||
|
location_id: row.get(0)?,
|
||||||
|
latitude: row.get(1)?,
|
||||||
|
longitude: row.get(2)?,
|
||||||
|
accuracy: row.get(3)?,
|
||||||
|
timestamp: row.get(4)?,
|
||||||
|
independent: row.get(5)?,
|
||||||
|
msg_id: msg_id,
|
||||||
|
contact_id: row.get(7)?,
|
||||||
|
chat_id: row.get(8)?,
|
||||||
|
marker: marker,
|
||||||
|
};
|
||||||
|
Ok(loc)
|
||||||
|
},
|
||||||
|
|locations| {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
|
||||||
|
for location in locations {
|
||||||
|
ret.push(location?);
|
||||||
|
}
|
||||||
|
Ok(dc_array_t::from(ret).into_raw())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|_| std::ptr::null_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_marker(txt: &str) -> bool {
|
||||||
|
txt.len() == 1 && txt.chars().next().unwrap() != ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_delete_all_locations(context: &Context) -> bool {
|
||||||
|
if sql::execute(context, &context.sql, "DELETE FROM locations;", params![]).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
context.call_cb(Event::LOCATION_CHANGED, 0, 0);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_get_location_kml(
|
||||||
|
context: &Context,
|
||||||
|
chat_id: uint32_t,
|
||||||
|
last_added_location_id: *mut uint32_t,
|
||||||
|
) -> *mut libc::c_char {
|
||||||
|
let mut success: libc::c_int = 0;
|
||||||
|
let now = time();
|
||||||
|
let mut location_count: libc::c_int = 0;
|
||||||
|
let mut ret = String::new();
|
||||||
|
|
||||||
|
let self_addr = context
|
||||||
|
.sql
|
||||||
|
.get_config(context, "configured_addr")
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if let Ok((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=?;",
|
||||||
|
params![chat_id as i32], |row| {
|
||||||
|
let send_begin: i64 = row.get(0)?;
|
||||||
|
let send_until: i64 = row.get(1)?;
|
||||||
|
let last_sent: i64 = row.get(2)?;
|
||||||
|
|
||||||
|
Ok((send_begin, send_until, last_sent))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if !(locations_send_begin == 0 || now > locations_send_until) {
|
||||||
|
ret += &format!(
|
||||||
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{}\">\n",
|
||||||
|
self_addr,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.sql.query_map(
|
||||||
|
"SELECT id, latitude, longitude, accuracy, timestamp\
|
||||||
|
FROM locations WHERE from_id=? \
|
||||||
|
AND timestamp>=? \
|
||||||
|
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
|
||||||
|
AND independent=0 \
|
||||||
|
GROUP BY timestamp \
|
||||||
|
ORDER BY timestamp;",
|
||||||
|
params![1, locations_send_begin, locations_last_sent, 1],
|
||||||
|
|row| {
|
||||||
|
let location_id: i32 = row.get(0)?;
|
||||||
|
let latitude: f64 = row.get(1)?;
|
||||||
|
let longitude: f64 = row.get(2)?;
|
||||||
|
let accuracy: f64 = row.get(3)?;
|
||||||
|
let timestamp = unsafe { get_kml_timestamp(row.get(4)?) };
|
||||||
|
|
||||||
|
Ok((location_id, latitude, longitude, accuracy, timestamp))
|
||||||
|
},
|
||||||
|
|rows| {
|
||||||
|
for row in rows {
|
||||||
|
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
|
||||||
|
ret += &format!(
|
||||||
|
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n\x00",
|
||||||
|
as_str(timestamp),
|
||||||
|
accuracy,
|
||||||
|
longitude,
|
||||||
|
latitude
|
||||||
|
);
|
||||||
|
location_count += 1;
|
||||||
|
if !last_added_location_id.is_null() {
|
||||||
|
unsafe { *last_added_location_id = location_id as u32 };
|
||||||
|
}
|
||||||
|
unsafe { free(timestamp as *mut libc::c_void) };
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
).unwrap(); // TODO: better error handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if location_count > 0 {
|
||||||
|
ret += "</Document>\n</kml>";
|
||||||
|
success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if 0 != success {
|
||||||
|
unsafe { ret.strdup() }
|
||||||
|
} else {
|
||||||
|
std::ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* create kml-files
|
||||||
|
******************************************************************************/
|
||||||
|
unsafe fn get_kml_timestamp(utc: i64) -> *mut libc::c_char {
|
||||||
|
// Returns a string formatted as YYYY-MM-DDTHH:MM:SSZ. The trailing `Z` indicates UTC.
|
||||||
|
let res = chrono::NaiveDateTime::from_timestamp(utc, 0)
|
||||||
|
.format("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
.to_string();
|
||||||
|
res.strdup()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_get_message_kml(
|
||||||
|
timestamp: i64,
|
||||||
|
latitude: libc::c_double,
|
||||||
|
longitude: libc::c_double,
|
||||||
|
) -> *mut libc::c_char {
|
||||||
|
let timestamp_str = get_kml_timestamp(timestamp);
|
||||||
|
let latitude_str = dc_ftoa(latitude);
|
||||||
|
let longitude_str = dc_ftoa(longitude);
|
||||||
|
|
||||||
|
let ret = dc_mprintf(
|
||||||
|
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||||
|
<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n\
|
||||||
|
<Document>\n\
|
||||||
|
<Placemark>\
|
||||||
|
<Timestamp><when>%s</when></Timestamp>\
|
||||||
|
<Point><coordinates>%s,%s</coordinates></Point>\
|
||||||
|
</Placemark>\n\
|
||||||
|
</Document>\n\
|
||||||
|
</kml>\x00" as *const u8 as *const libc::c_char,
|
||||||
|
timestamp_str,
|
||||||
|
longitude_str, // reverse order!
|
||||||
|
latitude_str,
|
||||||
|
);
|
||||||
|
|
||||||
|
free(latitude_str as *mut libc::c_void);
|
||||||
|
free(longitude_str as *mut libc::c_void);
|
||||||
|
free(timestamp_str as *mut libc::c_void);
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_set_kml_sent_timestamp(context: &Context, chat_id: u32, timestamp: i64) -> bool {
|
||||||
|
sql::execute(
|
||||||
|
context,
|
||||||
|
&context.sql,
|
||||||
|
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||||
|
params![timestamp, chat_id as i32],
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_set_msg_location_id(context: &Context, msg_id: u32, location_id: u32) -> bool {
|
||||||
|
sql::execute(
|
||||||
|
context,
|
||||||
|
&context.sql,
|
||||||
|
"UPDATE msgs SET location_id=? WHERE id=?;",
|
||||||
|
params![location_id, msg_id as i32],
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_save_locations(
|
||||||
|
context: &Context,
|
||||||
|
chat_id: u32,
|
||||||
|
contact_id: u32,
|
||||||
|
locations_opt: &Option<Vec<dc_location>>,
|
||||||
|
independent: libc::c_int,
|
||||||
|
) -> u32 {
|
||||||
|
if chat_id <= 9 || locations_opt.is_none() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let locations = locations_opt.as_ref().unwrap();
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.prepare2(
|
||||||
|
"SELECT id FROM locations WHERE timestamp=? AND from_id=?",
|
||||||
|
"INSERT INTO locations\
|
||||||
|
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
|
||||||
|
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(params![location.timestamp, contact_id as i32])?;
|
||||||
|
|
||||||
|
if 0 != independent || !exists {
|
||||||
|
stmt_insert.execute(params![
|
||||||
|
location.timestamp,
|
||||||
|
contact_id as i32,
|
||||||
|
chat_id as i32,
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_kml_parse(
|
||||||
|
context: &Context,
|
||||||
|
content: *const libc::c_char,
|
||||||
|
content_bytes: size_t,
|
||||||
|
) -> dc_kml_t {
|
||||||
|
let mut kml = dc_kml_t::new();
|
||||||
|
let mut content_nullterminated: *mut libc::c_char = 0 as *mut libc::c_char;
|
||||||
|
let mut saxparser: dc_saxparser_t = dc_saxparser_t {
|
||||||
|
starttag_cb: None,
|
||||||
|
endtag_cb: None,
|
||||||
|
text_cb: None,
|
||||||
|
userdata: 0 as *mut libc::c_void,
|
||||||
|
};
|
||||||
|
|
||||||
|
if content_bytes > (1 * 1024 * 1024) {
|
||||||
|
warn!(
|
||||||
|
context,
|
||||||
|
0, "A kml-files with {} bytes is larger than reasonably expected.", content_bytes,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content_nullterminated = dc_null_terminate(content, content_bytes as libc::c_int);
|
||||||
|
if !content_nullterminated.is_null() {
|
||||||
|
kml.locations = Some(Vec::with_capacity(100));
|
||||||
|
dc_saxparser_init(
|
||||||
|
&mut saxparser,
|
||||||
|
&mut kml as *mut dc_kml_t as *mut libc::c_void,
|
||||||
|
);
|
||||||
|
dc_saxparser_set_tag_handler(
|
||||||
|
&mut saxparser,
|
||||||
|
Some(kml_starttag_cb),
|
||||||
|
Some(kml_endtag_cb),
|
||||||
|
);
|
||||||
|
dc_saxparser_set_text_handler(&mut saxparser, Some(kml_text_cb));
|
||||||
|
dc_saxparser_parse(&mut saxparser, content_nullterminated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(content_nullterminated as *mut libc::c_void);
|
||||||
|
|
||||||
|
kml
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn kml_text_cb(userdata: *mut libc::c_void, text: *const libc::c_char, _len: libc::c_int) {
|
||||||
|
let mut kml: *mut dc_kml_t = userdata as *mut dc_kml_t;
|
||||||
|
if 0 != (*kml).tag & (0x4 | 0x10) {
|
||||||
|
let mut val: *mut libc::c_char = dc_strdup(text);
|
||||||
|
dc_str_replace(
|
||||||
|
&mut val,
|
||||||
|
b"\n\x00" as *const u8 as *const libc::c_char,
|
||||||
|
b"\x00" as *const u8 as *const libc::c_char,
|
||||||
|
);
|
||||||
|
dc_str_replace(
|
||||||
|
&mut val,
|
||||||
|
b"\r\x00" as *const u8 as *const libc::c_char,
|
||||||
|
b"\x00" as *const u8 as *const libc::c_char,
|
||||||
|
);
|
||||||
|
dc_str_replace(
|
||||||
|
&mut val,
|
||||||
|
b"\t\x00" as *const u8 as *const libc::c_char,
|
||||||
|
b"\x00" as *const u8 as *const libc::c_char,
|
||||||
|
);
|
||||||
|
dc_str_replace(
|
||||||
|
&mut val,
|
||||||
|
b" \x00" as *const u8 as *const libc::c_char,
|
||||||
|
b"\x00" as *const u8 as *const libc::c_char,
|
||||||
|
);
|
||||||
|
if 0 != (*kml).tag & 0x4 && strlen(val) >= 19 {
|
||||||
|
// YYYY-MM-DDTHH:MM:SSZ
|
||||||
|
// 0 4 7 10 13 16 19
|
||||||
|
let val_r = as_str(val);
|
||||||
|
match chrono::NaiveDateTime::parse_from_str(val_r, "%Y-%m-%dT%H:%M:%SZ") {
|
||||||
|
Ok(res) => {
|
||||||
|
(*kml).curr.timestamp = res.timestamp();
|
||||||
|
if (*kml).curr.timestamp > time() {
|
||||||
|
(*kml).curr.timestamp = time();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_err) => {
|
||||||
|
(*kml).curr.timestamp = time();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if 0 != (*kml).tag & 0x10 {
|
||||||
|
let mut comma: *mut libc::c_char = strchr(val, ',' as i32);
|
||||||
|
if !comma.is_null() {
|
||||||
|
let longitude: *mut libc::c_char = val;
|
||||||
|
let latitude: *mut libc::c_char = comma.offset(1isize);
|
||||||
|
*comma = 0 as libc::c_char;
|
||||||
|
comma = strchr(latitude, ',' as i32);
|
||||||
|
if !comma.is_null() {
|
||||||
|
*comma = 0 as libc::c_char
|
||||||
|
}
|
||||||
|
(*kml).curr.latitude = dc_atof(latitude);
|
||||||
|
(*kml).curr.longitude = dc_atof(longitude)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(val as *mut libc::c_void);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn kml_endtag_cb(userdata: *mut libc::c_void, tag: *const libc::c_char) {
|
||||||
|
let mut kml: *mut dc_kml_t = userdata as *mut dc_kml_t;
|
||||||
|
if strcmp(tag, b"placemark\x00" as *const u8 as *const libc::c_char) == 0 {
|
||||||
|
if 0 != (*kml).tag & 0x1
|
||||||
|
&& 0 != (*kml).curr.timestamp
|
||||||
|
&& 0. != (*kml).curr.latitude
|
||||||
|
&& 0. != (*kml).curr.longitude
|
||||||
|
{
|
||||||
|
let location = (*kml).curr.clone();
|
||||||
|
((*kml).locations.as_mut().unwrap()).push(location);
|
||||||
|
}
|
||||||
|
(*kml).tag = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* parse kml-files
|
||||||
|
******************************************************************************/
|
||||||
|
unsafe fn kml_starttag_cb(
|
||||||
|
userdata: *mut libc::c_void,
|
||||||
|
tag: *const libc::c_char,
|
||||||
|
attr: *mut *mut libc::c_char,
|
||||||
|
) {
|
||||||
|
let mut kml: *mut dc_kml_t = userdata as *mut dc_kml_t;
|
||||||
|
if strcmp(tag, b"document\x00" as *const u8 as *const libc::c_char) == 0 {
|
||||||
|
let addr: *const libc::c_char =
|
||||||
|
dc_attr_find(attr, b"addr\x00" as *const u8 as *const libc::c_char);
|
||||||
|
if !addr.is_null() {
|
||||||
|
(*kml).addr = dc_strdup(addr)
|
||||||
|
}
|
||||||
|
} else if strcmp(tag, b"placemark\x00" as *const u8 as *const libc::c_char) == 0 {
|
||||||
|
(*kml).tag = 0x1;
|
||||||
|
(*kml).curr.timestamp = 0;
|
||||||
|
(*kml).curr.latitude = 0 as libc::c_double;
|
||||||
|
(*kml).curr.longitude = 0.0f64;
|
||||||
|
(*kml).curr.accuracy = 0.0f64
|
||||||
|
} else if strcmp(tag, b"timestamp\x00" as *const u8 as *const libc::c_char) == 0
|
||||||
|
&& 0 != (*kml).tag & 0x1
|
||||||
|
{
|
||||||
|
(*kml).tag = 0x1 | 0x2
|
||||||
|
} else if strcmp(tag, b"when\x00" as *const u8 as *const libc::c_char) == 0
|
||||||
|
&& 0 != (*kml).tag & 0x2
|
||||||
|
{
|
||||||
|
(*kml).tag = 0x1 | 0x2 | 0x4
|
||||||
|
} else if strcmp(tag, b"point\x00" as *const u8 as *const libc::c_char) == 0
|
||||||
|
&& 0 != (*kml).tag & 0x1
|
||||||
|
{
|
||||||
|
(*kml).tag = 0x1 | 0x8
|
||||||
|
} else if strcmp(tag, b"coordinates\x00" as *const u8 as *const libc::c_char) == 0
|
||||||
|
&& 0 != (*kml).tag & 0x8
|
||||||
|
{
|
||||||
|
(*kml).tag = 0x1 | 0x8 | 0x10;
|
||||||
|
let accuracy: *const libc::c_char =
|
||||||
|
dc_attr_find(attr, b"accuracy\x00" as *const u8 as *const libc::c_char);
|
||||||
|
if !accuracy.is_null() {
|
||||||
|
(*kml).curr.accuracy = dc_atof(accuracy)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_kml_unref(kml: &mut dc_kml_t) {
|
||||||
|
free((*kml).addr as *mut libc::c_void);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub unsafe fn dc_job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context: &Context, _job: *mut dc_job_t) {
|
||||||
|
let now = time();
|
||||||
|
let mut continue_streaming: libc::c_int = 1;
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
0, " ----------------- MAYBE_SEND_LOCATIONS -------------- ",
|
||||||
|
);
|
||||||
|
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.query_map(
|
||||||
|
"SELECT id, locations_send_begin, locations_last_sent \
|
||||||
|
FROM chats \
|
||||||
|
WHERE locations_send_until>?;",
|
||||||
|
params![now],
|
||||||
|
|row| {
|
||||||
|
let chat_id: i32 = row.get(0)?;
|
||||||
|
let locations_send_begin: i64 = row.get(1)?;
|
||||||
|
let locations_last_sent: i64 = row.get(2)?;
|
||||||
|
continue_streaming = 1;
|
||||||
|
|
||||||
|
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
||||||
|
if now - locations_last_sent < (60 - 3) {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|rows| {
|
||||||
|
context.sql.prepare(
|
||||||
|
"SELECT id \
|
||||||
|
FROM locations \
|
||||||
|
WHERE from_id=? \
|
||||||
|
AND timestamp>=? \
|
||||||
|
AND timestamp>? \
|
||||||
|
AND independent=0 \
|
||||||
|
ORDER BY timestamp;",
|
||||||
|
|mut stmt_locations, _| {
|
||||||
|
for (chat_id, locations_send_begin, locations_last_sent) in
|
||||||
|
rows.filter_map(|r| match r {
|
||||||
|
Ok(Some(v)) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// TODO: do I need to reset?
|
||||||
|
if !stmt_locations
|
||||||
|
.exists(params![1, locations_send_begin, locations_last_sent,])
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
// if there is no new location, there's nothing to send.
|
||||||
|
// however, maybe we want to bypass this test eg. 15 minutes
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// pending locations are attached automatically to every message,
|
||||||
|
// so also to this empty text message.
|
||||||
|
// DC_CMD_LOCATION is only needed to create a nicer subject.
|
||||||
|
//
|
||||||
|
// for optimisation and to avoid flooding the sending queue,
|
||||||
|
// 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.
|
||||||
|
// (might not be 100%, however, as positions are sent combined later
|
||||||
|
// and dc_set_location() is typically called periodically, this is ok)
|
||||||
|
let mut msg = dc_msg_new(context, Viewtype::Text);
|
||||||
|
(*msg).hidden = 1;
|
||||||
|
(*msg).param.set_int(Param::Cmd, 9);
|
||||||
|
dc_send_msg(context, chat_id as u32, msg);
|
||||||
|
dc_msg_unref(msg);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap(); // TODO: Better error handling
|
||||||
|
|
||||||
|
if 0 != continue_streaming {
|
||||||
|
schedule_MAYBE_SEND_LOCATIONS(context, 0x1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub unsafe fn dc_job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context: &Context, job: &mut dc_job_t) {
|
||||||
|
// this function is called when location-streaming _might_ have ended for a chat.
|
||||||
|
// the function checks, if location-streaming is really ended;
|
||||||
|
// if so, a device-message is added if not yet done.
|
||||||
|
|
||||||
|
let chat_id = (*job).foreign_id;
|
||||||
|
|
||||||
|
if let Ok((send_begin, send_until)) = context.sql.query_row(
|
||||||
|
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
|
||||||
|
params![chat_id as i32],
|
||||||
|
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
|
||||||
|
) {
|
||||||
|
if !(send_begin != 0 && time() <= send_until) {
|
||||||
|
// still streaming -
|
||||||
|
// may happen as several calls to dc_send_locations_to_chat()
|
||||||
|
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
|
||||||
|
if !(send_begin == 0 && send_until == 0) {
|
||||||
|
// not streaming, device-message already sent
|
||||||
|
if context.sql.execute(
|
||||||
|
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
|
||||||
|
params![chat_id as i32],
|
||||||
|
).is_ok() {
|
||||||
|
let stock_str = CString::new(context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0)).unwrap();
|
||||||
|
dc_add_device_msg(context, chat_id, stock_str.as_ptr());
|
||||||
|
context.call_cb(
|
||||||
|
Event::CHAT_MODIFIED,
|
||||||
|
chat_id as usize,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
214
src/dc_loginparam.rs
Normal file
214
src/dc_loginparam.rs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::sql::Sql;
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub struct dc_loginparam_t {
|
||||||
|
pub addr: String,
|
||||||
|
pub mail_server: String,
|
||||||
|
pub mail_user: String,
|
||||||
|
pub mail_pw: String,
|
||||||
|
pub mail_port: i32,
|
||||||
|
pub send_server: String,
|
||||||
|
pub send_user: String,
|
||||||
|
pub send_pw: String,
|
||||||
|
pub send_port: i32,
|
||||||
|
pub server_flags: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl dc_loginparam_t {
|
||||||
|
pub fn addr_str(&self) -> &str {
|
||||||
|
self.addr.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_loginparam_new() -> dc_loginparam_t {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_loginparam_read(
|
||||||
|
context: &Context,
|
||||||
|
sql: &Sql,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
) -> dc_loginparam_t {
|
||||||
|
let prefix = prefix.as_ref();
|
||||||
|
|
||||||
|
let key = format!("{}addr", prefix);
|
||||||
|
let addr = sql
|
||||||
|
.get_config(context, key)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let key = format!("{}mail_server", prefix);
|
||||||
|
let mail_server = sql.get_config(context, key).unwrap_or_default();
|
||||||
|
|
||||||
|
let key = format!("{}mail_port", prefix);
|
||||||
|
let mail_port = sql.get_config_int(context, key).unwrap_or_default();
|
||||||
|
|
||||||
|
let key = format!("{}mail_user", prefix);
|
||||||
|
let mail_user = sql.get_config(context, key).unwrap_or_default();
|
||||||
|
|
||||||
|
let key = format!("{}mail_pw", prefix);
|
||||||
|
let mail_pw = sql.get_config(context, key).unwrap_or_default();
|
||||||
|
|
||||||
|
let key = format!("{}send_server", prefix);
|
||||||
|
let send_server = sql.get_config(context, key).unwrap_or_default();
|
||||||
|
|
||||||
|
let key = format!("{}send_port", prefix);
|
||||||
|
let send_port = sql.get_config_int(context, key).unwrap_or_default();
|
||||||
|
|
||||||
|
let key = format!("{}send_user", prefix);
|
||||||
|
let send_user = sql.get_config(context, key).unwrap_or_default();
|
||||||
|
|
||||||
|
let key = format!("{}send_pw", prefix);
|
||||||
|
let send_pw = sql.get_config(context, key).unwrap_or_default();
|
||||||
|
|
||||||
|
let key = format!("{}server_flags", prefix);
|
||||||
|
let server_flags = sql.get_config_int(context, key).unwrap_or_default();
|
||||||
|
|
||||||
|
dc_loginparam_t {
|
||||||
|
addr: addr.to_string(),
|
||||||
|
mail_server,
|
||||||
|
mail_user,
|
||||||
|
mail_pw,
|
||||||
|
mail_port,
|
||||||
|
send_server,
|
||||||
|
send_user,
|
||||||
|
send_pw,
|
||||||
|
send_port,
|
||||||
|
server_flags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_loginparam_write(
|
||||||
|
context: &Context,
|
||||||
|
loginparam: &dc_loginparam_t,
|
||||||
|
sql: &Sql,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
) {
|
||||||
|
let prefix = prefix.as_ref();
|
||||||
|
|
||||||
|
let key = format!("{}addr", prefix);
|
||||||
|
sql.set_config(context, key, Some(&loginparam.addr)).ok();
|
||||||
|
|
||||||
|
let key = format!("{}mail_server", prefix);
|
||||||
|
sql.set_config(context, key, Some(&loginparam.mail_server))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let key = format!("{}mail_port", prefix);
|
||||||
|
sql.set_config_int(context, key, loginparam.mail_port).ok();
|
||||||
|
|
||||||
|
let key = format!("{}mail_user", prefix);
|
||||||
|
sql.set_config(context, key, Some(&loginparam.mail_user))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let key = format!("{}mail_pw", prefix);
|
||||||
|
sql.set_config(context, key, Some(&loginparam.mail_pw)).ok();
|
||||||
|
|
||||||
|
let key = format!("{}send_server", prefix);
|
||||||
|
sql.set_config(context, key, Some(&loginparam.send_server))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let key = format!("{}send_port", prefix);
|
||||||
|
sql.set_config_int(context, key, loginparam.send_port).ok();
|
||||||
|
|
||||||
|
let key = format!("{}send_user", prefix);
|
||||||
|
sql.set_config(context, key, Some(&loginparam.send_user))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let key = format!("{}send_pw", prefix);
|
||||||
|
sql.set_config(context, key, Some(&loginparam.send_pw)).ok();
|
||||||
|
|
||||||
|
let key = format!("{}server_flags", prefix);
|
||||||
|
sql.set_config_int(context, key, loginparam.server_flags)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unset_empty(s: &String) -> Cow<String> {
|
||||||
|
if s.is_empty() {
|
||||||
|
Cow::Owned("unset".to_string())
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dc_loginparam_get_readable(loginparam: &dc_loginparam_t) -> String {
|
||||||
|
let unset = "0";
|
||||||
|
let pw = "***";
|
||||||
|
|
||||||
|
let flags_readable = get_readable_flags(loginparam.server_flags);
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{} {}:{}:{}:{} {}:{}:{}:{} {}",
|
||||||
|
unset_empty(&loginparam.addr),
|
||||||
|
unset_empty(&loginparam.mail_user),
|
||||||
|
if !loginparam.mail_pw.is_empty() {
|
||||||
|
pw
|
||||||
|
} else {
|
||||||
|
unset
|
||||||
|
},
|
||||||
|
unset_empty(&loginparam.mail_server),
|
||||||
|
loginparam.mail_port,
|
||||||
|
unset_empty(&loginparam.send_user),
|
||||||
|
if !loginparam.send_pw.is_empty() {
|
||||||
|
pw
|
||||||
|
} else {
|
||||||
|
unset
|
||||||
|
},
|
||||||
|
unset_empty(&loginparam.send_server),
|
||||||
|
loginparam.send_port,
|
||||||
|
flags_readable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_readable_flags(flags: i32) -> String {
|
||||||
|
let mut res = String::new();
|
||||||
|
for bit in 0..31 {
|
||||||
|
if 0 != flags & 1 << bit {
|
||||||
|
let mut flag_added = 0;
|
||||||
|
if 1 << bit == 0x2 {
|
||||||
|
res += "OAUTH2 ";
|
||||||
|
flag_added = 1;
|
||||||
|
}
|
||||||
|
if 1 << bit == 0x4 {
|
||||||
|
res += "AUTH_NORMAL ";
|
||||||
|
flag_added = 1;
|
||||||
|
}
|
||||||
|
if 1 << bit == 0x100 {
|
||||||
|
res += "IMAP_STARTTLS ";
|
||||||
|
flag_added = 1;
|
||||||
|
}
|
||||||
|
if 1 << bit == 0x200 {
|
||||||
|
res += "IMAP_SSL ";
|
||||||
|
flag_added = 1;
|
||||||
|
}
|
||||||
|
if 1 << bit == 0x400 {
|
||||||
|
res += "IMAP_PLAIN ";
|
||||||
|
flag_added = 1;
|
||||||
|
}
|
||||||
|
if 1 << bit == 0x10000 {
|
||||||
|
res += "SMTP_STARTTLS ";
|
||||||
|
flag_added = 1
|
||||||
|
}
|
||||||
|
if 1 << bit == 0x20000 {
|
||||||
|
res += "SMTP_SSL ";
|
||||||
|
flag_added = 1
|
||||||
|
}
|
||||||
|
if 1 << bit == 0x40000 {
|
||||||
|
res += "SMTP_PLAIN ";
|
||||||
|
flag_added = 1
|
||||||
|
}
|
||||||
|
if 0 == flag_added {
|
||||||
|
res += &format!("{:#0x}", 1 << bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if res.is_empty() {
|
||||||
|
res += "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
185
src/dc_lot.rs
Normal file
185
src/dc_lot.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
use crate::contact::*;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::dc_chat::*;
|
||||||
|
use crate::dc_msg::*;
|
||||||
|
use crate::dc_tools::*;
|
||||||
|
use crate::stock::StockMessage;
|
||||||
|
use crate::types::*;
|
||||||
|
use crate::x::*;
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
/* * Structure behind dc_lot_t */
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct dc_lot_t {
|
||||||
|
pub magic: uint32_t,
|
||||||
|
pub text1_meaning: libc::c_int,
|
||||||
|
pub text1: *mut libc::c_char,
|
||||||
|
pub text2: *mut libc::c_char,
|
||||||
|
pub timestamp: i64,
|
||||||
|
pub state: libc::c_int,
|
||||||
|
pub id: uint32_t,
|
||||||
|
pub fingerprint: *mut libc::c_char,
|
||||||
|
pub invitenumber: *mut libc::c_char,
|
||||||
|
pub auth: *mut libc::c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* *
|
||||||
|
* @class dc_lot_t
|
||||||
|
*
|
||||||
|
* An object containing a set of values.
|
||||||
|
* The meaning of the values is defined by the function returning the object.
|
||||||
|
* Lot objects are created
|
||||||
|
* eg. by chatlist.get_summary() or dc_msg_get_summary().
|
||||||
|
*
|
||||||
|
* NB: _Lot_ is used in the meaning _heap_ here.
|
||||||
|
*/
|
||||||
|
pub unsafe fn dc_lot_new() -> *mut dc_lot_t {
|
||||||
|
let mut lot: *mut dc_lot_t;
|
||||||
|
lot = calloc(1, ::std::mem::size_of::<dc_lot_t>()) as *mut dc_lot_t;
|
||||||
|
assert!(!lot.is_null());
|
||||||
|
|
||||||
|
(*lot).magic = 0x107107i32 as uint32_t;
|
||||||
|
(*lot).text1_meaning = 0i32;
|
||||||
|
|
||||||
|
lot
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_lot_empty(mut lot: *mut dc_lot_t) {
|
||||||
|
if lot.is_null() || (*lot).magic != 0x107107i32 as libc::c_uint {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
free((*lot).text1 as *mut libc::c_void);
|
||||||
|
(*lot).text1 = 0 as *mut libc::c_char;
|
||||||
|
(*lot).text1_meaning = 0i32;
|
||||||
|
free((*lot).text2 as *mut libc::c_void);
|
||||||
|
(*lot).text2 = 0 as *mut libc::c_char;
|
||||||
|
free((*lot).fingerprint as *mut libc::c_void);
|
||||||
|
(*lot).fingerprint = 0 as *mut libc::c_char;
|
||||||
|
free((*lot).invitenumber as *mut libc::c_void);
|
||||||
|
(*lot).invitenumber = 0 as *mut libc::c_char;
|
||||||
|
free((*lot).auth as *mut libc::c_void);
|
||||||
|
(*lot).auth = 0 as *mut libc::c_char;
|
||||||
|
(*lot).timestamp = 0;
|
||||||
|
(*lot).state = 0i32;
|
||||||
|
(*lot).id = 0i32 as uint32_t;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_lot_unref(mut set: *mut dc_lot_t) {
|
||||||
|
if set.is_null() || (*set).magic != 0x107107i32 as libc::c_uint {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dc_lot_empty(set);
|
||||||
|
(*set).magic = 0i32 as uint32_t;
|
||||||
|
free(set as *mut libc::c_void);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_lot_get_text1(lot: *const dc_lot_t) -> *mut libc::c_char {
|
||||||
|
if lot.is_null() || (*lot).magic != 0x107107i32 as libc::c_uint {
|
||||||
|
return 0 as *mut libc::c_char;
|
||||||
|
}
|
||||||
|
|
||||||
|
dc_strdup_keep_null((*lot).text1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_lot_get_text2(lot: *const dc_lot_t) -> *mut libc::c_char {
|
||||||
|
if lot.is_null() || (*lot).magic != 0x107107i32 as libc::c_uint {
|
||||||
|
return 0 as *mut libc::c_char;
|
||||||
|
}
|
||||||
|
|
||||||
|
dc_strdup_keep_null((*lot).text2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_lot_get_text1_meaning(lot: *const dc_lot_t) -> libc::c_int {
|
||||||
|
if lot.is_null() || (*lot).magic != 0x107107i32 as libc::c_uint {
|
||||||
|
return 0i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*lot).text1_meaning
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_lot_get_state(lot: *const dc_lot_t) -> libc::c_int {
|
||||||
|
if lot.is_null() || (*lot).magic != 0x107107i32 as libc::c_uint {
|
||||||
|
return 0i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*lot).state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_lot_get_id(lot: *const dc_lot_t) -> uint32_t {
|
||||||
|
if lot.is_null() || (*lot).magic != 0x107107i32 as libc::c_uint {
|
||||||
|
return 0i32 as uint32_t;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*lot).id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dc_lot_get_timestamp(lot: *const dc_lot_t) -> i64 {
|
||||||
|
if lot.is_null() || (*lot).magic != 0x107107i32 as libc::c_uint {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*lot).timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
/* library-internal */
|
||||||
|
/* in practice, the user additionally cuts the string himself pixel-accurate */
|
||||||
|
pub unsafe fn dc_lot_fill(
|
||||||
|
mut lot: *mut dc_lot_t,
|
||||||
|
msg: *mut dc_msg_t,
|
||||||
|
chat: *const Chat,
|
||||||
|
contact: Option<&Contact>,
|
||||||
|
context: &Context,
|
||||||
|
) {
|
||||||
|
if lot.is_null() || (*lot).magic != 0x107107i32 as libc::c_uint || msg.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (*msg).state == 19i32 {
|
||||||
|
(*lot).text1 = context.stock_str(StockMessage::Draft).strdup();
|
||||||
|
(*lot).text1_meaning = 1i32
|
||||||
|
} else if (*msg).from_id == 1i32 as libc::c_uint {
|
||||||
|
if 0 != dc_msg_is_info(msg) || 0 != dc_chat_is_self_talk(chat) {
|
||||||
|
(*lot).text1 = 0 as *mut libc::c_char;
|
||||||
|
(*lot).text1_meaning = 0i32
|
||||||
|
} else {
|
||||||
|
(*lot).text1 = context.stock_str(StockMessage::SelfMsg).strdup();
|
||||||
|
(*lot).text1_meaning = 3i32
|
||||||
|
}
|
||||||
|
} else if chat.is_null() {
|
||||||
|
(*lot).text1 = 0 as *mut libc::c_char;
|
||||||
|
(*lot).text1_meaning = 0i32
|
||||||
|
} else if (*chat).type_0 == 120i32 || (*chat).type_0 == 130i32 {
|
||||||
|
if 0 != dc_msg_is_info(msg) || contact.is_none() {
|
||||||
|
(*lot).text1 = 0 as *mut libc::c_char;
|
||||||
|
(*lot).text1_meaning = 0i32
|
||||||
|
} else {
|
||||||
|
if !chat.is_null() && (*chat).id == 1i32 as libc::c_uint {
|
||||||
|
if let Some(contact) = contact {
|
||||||
|
(*lot).text1 = contact.get_display_name().strdup();
|
||||||
|
} else {
|
||||||
|
(*lot).text1 = std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(contact) = contact {
|
||||||
|
(*lot).text1 = contact.get_first_name().strdup();
|
||||||
|
} else {
|
||||||
|
(*lot).text1 = std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*lot).text1_meaning = 2i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let msgtext_c = (*msg)
|
||||||
|
.text
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| CString::yolo(String::as_str(s)));
|
||||||
|
let msgtext_ptr = msgtext_c.map_or(ptr::null(), |s| s.as_ptr());
|
||||||
|
|
||||||
|
(*lot).text2 =
|
||||||
|
dc_msg_get_summarytext_by_raw((*msg).type_0, msgtext_ptr, &mut (*msg).param, 160, context);
|
||||||
|
|
||||||
|
(*lot).timestamp = dc_msg_get_timestamp(msg);
|
||||||
|
(*lot).state = (*msg).state;
|
||||||
|
}
|
||||||
1326
src/dc_mimefactory.rs
Normal file
1326
src/dc_mimefactory.rs
Normal file
File diff suppressed because it is too large
Load Diff
1910
src/dc_mimeparser.rs
Normal file
1910
src/dc_mimeparser.rs
Normal file
File diff suppressed because it is too large
Load Diff
40
src/dc_move.rs
Normal file
40
src/dc_move.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use crate::constants::*;
|
||||||
|
use crate::context::*;
|
||||||
|
use crate::dc_job::*;
|
||||||
|
use crate::dc_msg::*;
|
||||||
|
use crate::param::Params;
|
||||||
|
|
||||||
|
pub unsafe fn dc_do_heuristics_moves(context: &Context, folder: &str, msg_id: u32) {
|
||||||
|
if context
|
||||||
|
.sql
|
||||||
|
.get_config_int(context, "mvbox_move")
|
||||||
|
.unwrap_or_else(|| 1)
|
||||||
|
== 0
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dc_is_inbox(context, folder) && !dc_is_sentbox(context, folder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = dc_msg_new_load(context, msg_id);
|
||||||
|
if dc_msg_is_setupmessage(msg) {
|
||||||
|
// do not move setup messages;
|
||||||
|
// there may be a non-delta device that wants to handle it
|
||||||
|
dc_msg_unref(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if dc_is_mvbox(context, folder) {
|
||||||
|
dc_update_msg_move_state(context, (*msg).rfc724_mid, DC_MOVE_STATE_STAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 = dc message, 2 = reply to dc message
|
||||||
|
if 0 != (*msg).is_dc_message {
|
||||||
|
dc_job_add(context, 200, (*msg).id as libc::c_int, Params::new(), 0);
|
||||||
|
dc_update_msg_move_state(context, (*msg).rfc724_mid, DC_MOVE_STATE_MOVING);
|
||||||
|
}
|
||||||
|
|
||||||
|
dc_msg_unref(msg);
|
||||||
|
}
|
||||||
1591
src/dc_msg.rs
Normal file
1591
src/dc_msg.rs
Normal file
File diff suppressed because it is too large
Load Diff
346
src/dc_qr.rs
Normal file
346
src/dc_qr.rs
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
use percent_encoding::percent_decode_str;
|
||||||
|
|
||||||
|
use crate::contact::*;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::dc_chat::*;
|
||||||
|
use crate::dc_lot::*;
|
||||||
|
use crate::dc_strencode::*;
|
||||||
|
use crate::dc_tools::*;
|
||||||
|
use crate::key::*;
|
||||||
|
use crate::param::*;
|
||||||
|
use crate::peerstate::*;
|
||||||
|
use crate::types::*;
|
||||||
|
use crate::x::*;
|
||||||
|
|
||||||
|
// out-of-band verification
|
||||||
|
// id=contact
|
||||||
|
// text1=groupname
|
||||||
|
// id=contact
|
||||||
|
// id=contact
|
||||||
|
// test1=formatted fingerprint
|
||||||
|
// id=contact
|
||||||
|
// text1=text
|
||||||
|
// text1=URL
|
||||||
|
// text1=error string
|
||||||
|
pub unsafe fn dc_check_qr(context: &Context, qr: *const libc::c_char) -> *mut dc_lot_t {
|
||||||
|
let mut current_block: u64;
|
||||||
|
let mut payload: *mut libc::c_char = 0 as *mut libc::c_char;
|
||||||
|
// must be normalized, if set
|
||||||
|
let mut addr: *mut libc::c_char = 0 as *mut libc::c_char;
|
||||||
|
// must be normalized, if set
|
||||||
|
let mut fingerprint: *mut libc::c_char = 0 as *mut libc::c_char;
|
||||||
|
let mut name: *mut libc::c_char = 0 as *mut libc::c_char;
|
||||||
|
let mut invitenumber: *mut libc::c_char = 0 as *mut libc::c_char;
|
||||||
|
let mut auth: *mut libc::c_char = 0 as *mut libc::c_char;
|
||||||
|
let mut qr_parsed: *mut dc_lot_t = dc_lot_new();
|
||||||
|
let mut chat_id: uint32_t = 0i32 as uint32_t;
|
||||||
|
let mut device_msg: *mut libc::c_char = 0 as *mut libc::c_char;
|
||||||
|
let mut grpid: *mut libc::c_char = 0 as *mut libc::c_char;
|
||||||
|
let mut grpname: *mut libc::c_char = 0 as *mut libc::c_char;
|
||||||
|
(*qr_parsed).state = 0i32;
|
||||||
|
if !qr.is_null() {
|
||||||
|
info!(context, 0, "Scanned QR code: {}", as_str(qr),);
|
||||||
|
/* split parameters from the qr code
|
||||||
|
------------------------------------ */
|
||||||
|
if strncasecmp(
|
||||||
|
qr,
|
||||||
|
b"OPENPGP4FPR:\x00" as *const u8 as *const libc::c_char,
|
||||||
|
strlen(b"OPENPGP4FPR:\x00" as *const u8 as *const libc::c_char),
|
||||||
|
) == 0i32
|
||||||
|
{
|
||||||
|
payload =
|
||||||
|
dc_strdup(&*qr.offset(strlen(
|
||||||
|
b"OPENPGP4FPR:\x00" as *const u8 as *const libc::c_char,
|
||||||
|
) as isize));
|
||||||
|
let mut fragment: *mut libc::c_char = strchr(payload, '#' as i32);
|
||||||
|
if !fragment.is_null() {
|
||||||
|
*fragment = 0i32 as libc::c_char;
|
||||||
|
fragment = fragment.offset(1isize);
|
||||||
|
let param: Params = as_str(fragment).parse().expect("invalid params");
|
||||||
|
addr = param
|
||||||
|
.get(Param::Forwarded)
|
||||||
|
.map(|s| s.strdup())
|
||||||
|
.unwrap_or_else(|| std::ptr::null_mut());
|
||||||
|
if !addr.is_null() {
|
||||||
|
if let Some(ref name_enc) = param.get(Param::SetLongitude) {
|
||||||
|
let name_r = percent_decode_str(name_enc)
|
||||||
|
.decode_utf8()
|
||||||
|
.expect("invalid name");
|
||||||
|
name = normalize_name(name_r).strdup();
|
||||||
|
}
|
||||||
|
invitenumber = param
|
||||||
|
.get(Param::ProfileImage)
|
||||||
|
.map(|s| s.strdup())
|
||||||
|
.unwrap_or_else(|| std::ptr::null_mut());
|
||||||
|
auth = param
|
||||||
|
.get(Param::Auth)
|
||||||
|
.map(|s| s.strdup())
|
||||||
|
.unwrap_or_else(|| std::ptr::null_mut());
|
||||||
|
grpid = param
|
||||||
|
.get(Param::GroupId)
|
||||||
|
.map(|s| s.strdup())
|
||||||
|
.unwrap_or_else(|| std::ptr::null_mut());
|
||||||
|
if !grpid.is_null() {
|
||||||
|
if let Some(grpname_enc) = param.get(Param::GroupName) {
|
||||||
|
let grpname_r = percent_decode_str(grpname_enc)
|
||||||
|
.decode_utf8()
|
||||||
|
.expect("invalid groupname");
|
||||||
|
grpname = grpname_r.strdup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fingerprint = dc_normalize_fingerprint_c(payload);
|
||||||
|
current_block = 5023038348526654800;
|
||||||
|
} else if strncasecmp(
|
||||||
|
qr,
|
||||||
|
b"mailto:\x00" as *const u8 as *const libc::c_char,
|
||||||
|
strlen(b"mailto:\x00" as *const u8 as *const libc::c_char),
|
||||||
|
) == 0i32
|
||||||
|
{
|
||||||
|
payload = dc_strdup(
|
||||||
|
&*qr.offset(strlen(b"mailto:\x00" as *const u8 as *const libc::c_char) as isize),
|
||||||
|
);
|
||||||
|
let query: *mut libc::c_char = strchr(payload, '?' as i32);
|
||||||
|
if !query.is_null() {
|
||||||
|
*query = 0i32 as libc::c_char
|
||||||
|
}
|
||||||
|
addr = dc_strdup(payload);
|
||||||
|
current_block = 5023038348526654800;
|
||||||
|
} else if strncasecmp(
|
||||||
|
qr,
|
||||||
|
b"SMTP:\x00" as *const u8 as *const libc::c_char,
|
||||||
|
strlen(b"SMTP:\x00" as *const u8 as *const libc::c_char),
|
||||||
|
) == 0i32
|
||||||
|
{
|
||||||
|
payload = dc_strdup(
|
||||||
|
&*qr.offset(strlen(b"SMTP:\x00" as *const u8 as *const libc::c_char) as isize),
|
||||||
|
);
|
||||||
|
let colon: *mut libc::c_char = strchr(payload, ':' as i32);
|
||||||
|
if !colon.is_null() {
|
||||||
|
*colon = 0i32 as libc::c_char
|
||||||
|
}
|
||||||
|
addr = dc_strdup(payload);
|
||||||
|
current_block = 5023038348526654800;
|
||||||
|
} else if strncasecmp(
|
||||||
|
qr,
|
||||||
|
b"MATMSG:\x00" as *const u8 as *const libc::c_char,
|
||||||
|
strlen(b"MATMSG:\x00" as *const u8 as *const libc::c_char),
|
||||||
|
) == 0i32
|
||||||
|
{
|
||||||
|
/* scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;` - there may or may not be linebreaks after the fields */
|
||||||
|
/* does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field. we ignore this case. */
|
||||||
|
let to: *mut libc::c_char = strstr(qr, b"TO:\x00" as *const u8 as *const libc::c_char);
|
||||||
|
if !to.is_null() {
|
||||||
|
addr = dc_strdup(&mut *to.offset(3isize));
|
||||||
|
let semicolon: *mut libc::c_char = strchr(addr, ';' as i32);
|
||||||
|
if !semicolon.is_null() {
|
||||||
|
*semicolon = 0i32 as libc::c_char
|
||||||
|
}
|
||||||
|
current_block = 5023038348526654800;
|
||||||
|
} else {
|
||||||
|
(*qr_parsed).state = 400i32;
|
||||||
|
(*qr_parsed).text1 =
|
||||||
|
dc_strdup(b"Bad e-mail address.\x00" as *const u8 as *const libc::c_char);
|
||||||
|
current_block = 16562876845594826114;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if strncasecmp(
|
||||||
|
qr,
|
||||||
|
b"BEGIN:VCARD\x00" as *const u8 as *const libc::c_char,
|
||||||
|
strlen(b"BEGIN:VCARD\x00" as *const u8 as *const libc::c_char),
|
||||||
|
) == 0i32
|
||||||
|
{
|
||||||
|
let lines = dc_split_into_lines(qr);
|
||||||
|
for &key in &lines {
|
||||||
|
dc_trim(key);
|
||||||
|
let mut value: *mut libc::c_char = strchr(key, ':' as i32);
|
||||||
|
if !value.is_null() {
|
||||||
|
*value = 0i32 as libc::c_char;
|
||||||
|
value = value.offset(1isize);
|
||||||
|
let mut semicolon_0: *mut libc::c_char = strchr(key, ';' as i32);
|
||||||
|
if !semicolon_0.is_null() {
|
||||||
|
*semicolon_0 = 0i32 as libc::c_char
|
||||||
|
}
|
||||||
|
if strcasecmp(key, b"EMAIL\x00" as *const u8 as *const libc::c_char) == 0i32
|
||||||
|
{
|
||||||
|
semicolon_0 = strchr(value, ';' as i32);
|
||||||
|
if !semicolon_0.is_null() {
|
||||||
|
*semicolon_0 = 0i32 as libc::c_char
|
||||||
|
}
|
||||||
|
addr = dc_strdup(value)
|
||||||
|
} else if strcasecmp(key, b"N\x00" as *const u8 as *const libc::c_char)
|
||||||
|
== 0i32
|
||||||
|
{
|
||||||
|
semicolon_0 = strchr(value, ';' as i32);
|
||||||
|
if !semicolon_0.is_null() {
|
||||||
|
semicolon_0 = strchr(semicolon_0.offset(1isize), ';' as i32);
|
||||||
|
if !semicolon_0.is_null() {
|
||||||
|
*semicolon_0 = 0i32 as libc::c_char
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name = dc_strdup(value);
|
||||||
|
dc_str_replace(
|
||||||
|
&mut name,
|
||||||
|
b";\x00" as *const u8 as *const libc::c_char,
|
||||||
|
b",\x00" as *const u8 as *const libc::c_char,
|
||||||
|
);
|
||||||
|
name = normalize_name(as_str(name)).strdup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dc_free_splitted_lines(lines);
|
||||||
|
}
|
||||||
|
current_block = 5023038348526654800;
|
||||||
|
}
|
||||||
|
match current_block {
|
||||||
|
16562876845594826114 => {}
|
||||||
|
_ => {
|
||||||
|
/* check the parameters
|
||||||
|
---------------------- */
|
||||||
|
if !addr.is_null() {
|
||||||
|
/* urldecoding is needed at least for OPENPGP4FPR but should not hurt in the other cases */
|
||||||
|
let mut temp: *mut libc::c_char = dc_urldecode(addr);
|
||||||
|
free(addr as *mut libc::c_void);
|
||||||
|
addr = temp;
|
||||||
|
temp = addr_normalize(as_str(addr)).strdup();
|
||||||
|
free(addr as *mut libc::c_void);
|
||||||
|
addr = temp;
|
||||||
|
if !may_be_valid_addr(as_str(addr)) {
|
||||||
|
(*qr_parsed).state = 400i32;
|
||||||
|
(*qr_parsed).text1 = dc_strdup(
|
||||||
|
b"Bad e-mail address.\x00" as *const u8 as *const libc::c_char,
|
||||||
|
);
|
||||||
|
current_block = 16562876845594826114;
|
||||||
|
} else {
|
||||||
|
current_block = 14116432890150942211;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current_block = 14116432890150942211;
|
||||||
|
}
|
||||||
|
match current_block {
|
||||||
|
16562876845594826114 => {}
|
||||||
|
_ => {
|
||||||
|
if !fingerprint.is_null() {
|
||||||
|
if strlen(fingerprint) != 40 {
|
||||||
|
(*qr_parsed).state = 400i32;
|
||||||
|
(*qr_parsed).text1 = dc_strdup(
|
||||||
|
b"Bad fingerprint length in QR code.\x00" as *const u8
|
||||||
|
as *const libc::c_char,
|
||||||
|
);
|
||||||
|
current_block = 16562876845594826114;
|
||||||
|
} else {
|
||||||
|
current_block = 5409161009579131794;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current_block = 5409161009579131794;
|
||||||
|
}
|
||||||
|
match current_block {
|
||||||
|
16562876845594826114 => {}
|
||||||
|
_ => {
|
||||||
|
if !fingerprint.is_null() {
|
||||||
|
let peerstate = Peerstate::from_fingerprint(
|
||||||
|
context,
|
||||||
|
&context.sql,
|
||||||
|
as_str(fingerprint),
|
||||||
|
);
|
||||||
|
if addr.is_null() || invitenumber.is_null() || auth.is_null() {
|
||||||
|
if let Some(peerstate) = peerstate {
|
||||||
|
(*qr_parsed).state = 210i32;
|
||||||
|
let addr = peerstate
|
||||||
|
.addr
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or_else(|| "");
|
||||||
|
(*qr_parsed).id = Contact::add_or_lookup(
|
||||||
|
context,
|
||||||
|
"",
|
||||||
|
addr,
|
||||||
|
Origin::UnhandledQrScan,
|
||||||
|
)
|
||||||
|
.map(|(id, _)| id)
|
||||||
|
.unwrap_or_default();
|
||||||
|
dc_create_or_lookup_nchat_by_contact_id(
|
||||||
|
context,
|
||||||
|
(*qr_parsed).id,
|
||||||
|
2i32,
|
||||||
|
&mut chat_id,
|
||||||
|
0 as *mut libc::c_int,
|
||||||
|
);
|
||||||
|
device_msg = dc_mprintf(
|
||||||
|
b"%s verified.\x00" as *const u8
|
||||||
|
as *const libc::c_char,
|
||||||
|
peerstate.addr,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(*qr_parsed).text1 =
|
||||||
|
dc_format_fingerprint_c(fingerprint);
|
||||||
|
(*qr_parsed).state = 230i32
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !grpid.is_null() && !grpname.is_null() {
|
||||||
|
(*qr_parsed).state = 202i32;
|
||||||
|
(*qr_parsed).text1 = dc_strdup(grpname);
|
||||||
|
(*qr_parsed).text2 = dc_strdup(grpid)
|
||||||
|
} else {
|
||||||
|
(*qr_parsed).state = 200i32
|
||||||
|
}
|
||||||
|
(*qr_parsed).id = Contact::add_or_lookup(
|
||||||
|
context,
|
||||||
|
as_str(name),
|
||||||
|
as_str(addr),
|
||||||
|
Origin::UnhandledQrScan,
|
||||||
|
)
|
||||||
|
.map(|(id, _)| id)
|
||||||
|
.unwrap_or_default();
|
||||||
|
(*qr_parsed).fingerprint = dc_strdup(fingerprint);
|
||||||
|
(*qr_parsed).invitenumber = dc_strdup(invitenumber);
|
||||||
|
(*qr_parsed).auth = dc_strdup(auth)
|
||||||
|
}
|
||||||
|
} else if !addr.is_null() {
|
||||||
|
(*qr_parsed).state = 320i32;
|
||||||
|
(*qr_parsed).id = Contact::add_or_lookup(
|
||||||
|
context,
|
||||||
|
as_str(name),
|
||||||
|
as_str(addr),
|
||||||
|
Origin::UnhandledQrScan,
|
||||||
|
)
|
||||||
|
.map(|(id, _)| id)
|
||||||
|
.unwrap_or_default();
|
||||||
|
} else if strstr(
|
||||||
|
qr,
|
||||||
|
b"http://\x00" as *const u8 as *const libc::c_char,
|
||||||
|
) == qr as *mut libc::c_char
|
||||||
|
|| strstr(
|
||||||
|
qr,
|
||||||
|
b"https://\x00" as *const u8 as *const libc::c_char,
|
||||||
|
) == qr as *mut libc::c_char
|
||||||
|
{
|
||||||
|
(*qr_parsed).state = 332i32;
|
||||||
|
(*qr_parsed).text1 = dc_strdup(qr)
|
||||||
|
} else {
|
||||||
|
(*qr_parsed).state = 330i32;
|
||||||
|
(*qr_parsed).text1 = dc_strdup(qr)
|
||||||
|
}
|
||||||
|
if !device_msg.is_null() {
|
||||||
|
dc_add_device_msg(context, chat_id, device_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(addr as *mut libc::c_void);
|
||||||
|
free(fingerprint as *mut libc::c_void);
|
||||||
|
free(payload as *mut libc::c_void);
|
||||||
|
free(name as *mut libc::c_void);
|
||||||
|
free(invitenumber as *mut libc::c_void);
|
||||||
|
free(auth as *mut libc::c_void);
|
||||||
|
free(device_msg as *mut libc::c_void);
|
||||||
|
free(grpname as *mut libc::c_void);
|
||||||
|
free(grpid as *mut libc::c_void);
|
||||||
|
|
||||||
|
qr_parsed
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user