Compare commits

..

1 Commits

Author SHA1 Message Date
holger krekel
601b284ed8 fix #538 -- don't crash on wrong setup codes for ac-message, don't use "expect(), added test 2019-09-18 16:31:20 +02:00
129 changed files with 16487 additions and 20171 deletions

View File

@@ -4,9 +4,6 @@ executors:
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 +12,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-v1-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- repo-source-{{ .Branch }}-{{ .Revision }} - repo-source-{{ .Branch }}-{{ .Revision }}
commands: commands:
@@ -26,9 +23,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:
@@ -36,24 +44,30 @@ jobs:
executor: default executor: default
steps: steps:
- checkout - checkout
- run:
name: Update submodules
command: git submodule update --init --recursive
- run:
name: Calculate dependencies
command: cargo generate-lockfile
- restore_cache: - restore_cache:
keys: keys:
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }} - cargo-v1-{{ 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: rustup component add --toolchain $(cat rust-toolchain) clippy-preview
- 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-v1-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
paths: paths:
- "~/.cargo" - "~/.cargo"
- "~/.rustup" - "~/.rustup"
@@ -84,10 +98,11 @@ jobs:
curl https://sh.rustup.rs -sSf | sh -s -- -y curl https://sh.rustup.rs -sSf | sh -s -- -y
- 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: cargo update
- 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
@@ -108,59 +123,33 @@ 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
@@ -170,7 +159,7 @@ jobs:
at: workspace at: workspace
- run: pyenv global 3.5.2 - 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: clippy:
executor: default executor: default
@@ -179,7 +168,7 @@ jobs:
- *restore-cache - *restore-cache
- run: - run:
name: Run cargo clippy name: Run cargo clippy
command: cargo clippy command: cargo clippy --all
workflows: workflows:
@@ -187,29 +176,23 @@ workflows:
test: test:
jobs: jobs:
# - cargo_fetch - build_test_docs_wheel
- upload_docs_wheels:
requires:
- build_test_docs_wheel
- cargo_fetch
- rustfmt:
requires:
- cargo_fetch
- remote_tests_rust - clippy:
requires:
- remote_tests_python - cargo_fetch
# - upload_docs_wheels:
# requires:
# - build_test_docs_wheel
# - build_doxygen
# - rustfmt:
# requires:
# - 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:

View File

@@ -1,47 +0,0 @@
on: push
name: Code Quality
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2019-11-06
override: true
- uses: actions-rs/cargo@v1
with:
command: check
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2019-11-06
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2019-11-06
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features

3
.gitignore vendored
View File

@@ -16,12 +16,9 @@ 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 # ignore doxgen generated files
deltachat-ffi/html deltachat-ffi/html
deltachat-ffi/xml deltachat-ffi/xml
.rsynclist

View File

@@ -1,276 +0,0 @@
# Changelog
## 1.0.0-beta.18
- #1056 avoid panicking when we couldn't read imap-server's greeting
message
- #1055 avoid panicking when we don't have a selected folder
- #1052 #1049 #1051 improve logging to add thread-id/name and
file/lineno to each info/warn message.
- #1050 allow python bindings to initialize Account with "os_name".
## 1.0.0-beta.17
- #1044 implement avatar recoding to 192x192 in core to keep file sizes small.
- #1024 fix #1021 SQL/injection malformed Chat-Group-Name breakage
- #1036 fix smtp crash by pulling in a fixed async-smtp
- #1039 fix read-receipts appearing as normal messages when you change
MDN settings
- #1040 do not panic on SystemTimeDifference
- #1043 avoid potential crashes in malformed From/Chat-Disposition... headers
- #1045 #1041 #1038 #1035 #1034 #1029 #1025 various cleanups and doc
improvments
## 1.0.0-beta.16
- alleviate login problems with providers which only
support RSA1024 keys by switching back from Rustls
to native-tls, by using the new async-email/async-native-tls
crate from @dignifiedquire. thanks @link2xt.
- introduce per-contact profile images to send out
own profile image heuristically, and fix sending
out of profile images in "in-prepare" groups.
this also extends the Chat-spec that is maintained
in core to specify Chat-Group-Image and Chat-Group-Avatar
headers. thanks @r10s and @hpk42.
- fix merging of protected headers from the encrypted
to the unencrypted parts, now not happening recursively
anymore. thanks @hpk and @r10s
- fix/optimize autocrypt gossip headers to only get
sent when there are more than 2 people in a chat.
thanks @link2xt
- fix displayname to use the authenticated name
when available (displayname as coming from contacts
themselves). thanks @simon-laux
- introduce preliminary support for offline autoconfig
for nauta provider. thanks @hpk42 @r10s
## 1.0.0-beta.15
- fix #994 attachment appeared doubled in chats (and where actually
downloaded after smtp-send). @hpk42
## 1.0.0-beta.14
- fix packaging issue with our rust-email fork, now we are tracking
master again there. hpk42
## 1.0.0-beta.13
- fix #976 -- unicode-issues in display-name of email addresses. @hpk42
- fix #985 group add/remove member bugs resulting in broken groups. @hpk42
- fix hanging IMAP connections -- we now detect with a 15second timeout
if we cannot terminate the IDLE IMAP protocol. @hpk42 @link2xt
- fix incoming multipart/mixed containing html, to show up as
attachments again. Fixes usage for simplebot which sends html
files for users to interact with the bot. @adbenitez @hpk42
- refinements to internal autocrypt-handling code, do not send
prefer-encrypt=nopreference as it is the default if no attribute
is present. @linkxt
- simplify, modularize and rustify several parts
of dc-core (general WIP). @link2xt @flub @hpk42 @r10s
- use async-email/async-smtp to handle SMTP connections, might
fix connection/reconnection issues. @link2xt
- more tests and refinements for dealing with blobstorage @flub @hpk42
- use a dedicated build-server for CI testing of core PRs
## 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

1994
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,27 @@
[package] [package]
name = "deltachat" name = "deltachat"
version = "1.0.0-beta.18" version = "1.0.0-alpha.4"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"] authors = ["dignifiedquire <dignifiedquire@gmail.com>"]
edition = "2018" edition = "2018"
license = "MPL" license = "MPL"
[dependencies] [dependencies]
deltachat_derive = { path = "./deltachat_derive" } deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51" libc = "0.2.51"
pgp = { version = "0.4.0", 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.7.0" rand = "0.6.5"
smallvec = "1.0.0" phf = { git = "https://github.com/sfackler/rust-phf", rev = "0d00821", features = ["macros"] }
reqwest = { version = "0.9.15" } smallvec = "0.6.9"
num-derive = "0.3.0" reqwest = "0.9.15"
num-derive = "0.2.5"
num-traits = "0.2.6" num-traits = "0.2.6"
async-smtp = { git = "https://github.com/async-email/async-smtp", branch = "master" } native-tls = "0.2.3"
email = { git = "https://github.com/deltachat/rust-email", branch = "master" } lettre = "0.9.0"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "native_tls" } imap = { git = "https://github.com/jonhoo/rust-imap", rev = "281d2eb8ab50dc656ceff2ae749ca5045f334e15" }
mmime = { git = "https://github.com/dignifiedquire/mmime", rev = "bccd2c2" }
# XXX newer commits of async-imap lead to import-export tests hanging base64 = "0.10"
async-imap = { git = "https://github.com/async-email/async-imap", branch = "dcc-stable" }
async-native-tls = "0.1.1"
async-std = { version = "1.0", features = ["unstable"] }
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"] }
@@ -34,7 +29,6 @@ serde_json = "1.0"
chrono = "0.4.6" chrono = "0.4.6"
failure = "0.1.5" failure = "0.1.5"
failure_derive = "0.1.5" failure_derive = "0.1.5"
indexmap = "1.3.0"
# TODO: make optional # TODO: make optional
rustyline = "4.1.0" rustyline = "4.1.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
@@ -42,23 +36,18 @@ regex = "1.1.6"
rusqlite = { version = "0.20", features = ["bundled"] } rusqlite = { version = "0.20", features = ["bundled"] }
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" image-meta = "0.1.0"
quick-xml = "0.17.1" quick-xml = "0.15.0"
escaper = "0.1.0" escaper = "0.1.0"
bitflags = "1.1.0" bitflags = "1.1.0"
jetscii = "0.4.4"
debug_stub_derive = "0.3.0" debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.10.1"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = "0.22.3"
[dev-dependencies] [dev-dependencies]
tempfile = "3.0" tempfile = "3.0"

View File

@@ -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,15 +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 ### Expensive tests
Some tests are expensive and marked with `#[ignore]`, to run these Some tests are expensive and marked with `#[ignore]`, to run these

6
Xargo.toml Normal file
View File

@@ -0,0 +1,6 @@
[dependencies.std]
features = ["panic-unwind"]
# if using `cargo test`
[dependencies.test]
stage = 1

View File

@@ -8,11 +8,12 @@ install:
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin - set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -vV - rustc -vV
- cargo -vV - cargo -vV
- cargo update
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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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 DCC_PY_LIVECONFIG -e BRANCH -e TESTS -e DOCS \
--rm -it -v $(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh

View File

@@ -7,30 +7,25 @@ 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} 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

View File

@@ -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

View File

@@ -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

View File

@@ -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-07-10 -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-07-10-x86_64-unknown-linux-gnu/share/

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env bash
set -ex
cd deltachat-ffi
doxygen

View File

@@ -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

View File

@@ -36,25 +36,20 @@ 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 # XXX we don't run liveconfig tests because they hang sometimes
# we run them only for the highest python version we support # see https://github.com/deltachat/deltachat-core-rust/issues/331
# unset DCC_PY_LIVECONFIG
# we split out qr-tests run to minimize likelyness of flaky tests
# (some qr tests are pretty heavy in terms of send/received tox --workdir "$TOXWORKDIR" -e lint,py35,py36,py37,auditwheels -- -k "not qr"
# messages and rust's imap code likely has concurrency problems) tox --workdir "$TOXWORKDIR" -e py35,py36,py37 -- -k "qr"
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

View File

@@ -1,8 +1,8 @@
[package] [package]
name = "deltachat_ffi" name = "deltachat_ffi"
version = "1.0.0-beta.18" version = "1.0.0-alpha.4"
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,11 +16,9 @@ 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"]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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?

View File

@@ -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);
}
}
}

View File

@@ -1,29 +1,32 @@
use std::path::Path; use std::ffi::CString;
use std::ptr;
use std::str::FromStr; use std::str::FromStr;
use deltachat::chat::{self, Chat}; use deltachat::chat::{self, Chat};
use deltachat::chatlist::*; use deltachat::chatlist::*;
use deltachat::config; use deltachat::config;
use deltachat::configure::*;
use deltachat::constants::*; use deltachat::constants::*;
use deltachat::contact::*; use deltachat::contact::*;
use deltachat::context::*; use deltachat::context::*;
use deltachat::dc_imex::*;
use deltachat::dc_receive_imf::*; use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*; use deltachat::dc_tools::*;
use deltachat::error::Error; use deltachat::error::Error;
use deltachat::imex::*;
use deltachat::job::*; use deltachat::job::*;
use deltachat::location; use deltachat::location;
use deltachat::lot::LotState; use deltachat::lot::LotState;
use deltachat::message::{self, Message, MessageState, MsgId}; use deltachat::message::*;
use deltachat::peerstate::*; use deltachat::peerstate::*;
use deltachat::qr::*; use deltachat::qr::*;
use deltachat::sql; use deltachat::sql;
use deltachat::x::*;
use deltachat::Event; use deltachat::Event;
/// Reset database tables. This function is called from Core cmdline. /// Reset database tables. This function is called from Core cmdline.
/// Argument is a bitmask, executing single or multiple actions in one call. /// Argument is a bitmask, executing single or multiple actions in one call.
/// e.g. bitmask 7 triggers actions definded with bits 1, 2 and 4. /// e.g. bitmask 7 triggers actions definded with bits 1, 2 and 4.
pub fn dc_reset_tables(context: &Context, bits: i32) -> i32 { pub unsafe fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
info!(context, "Resetting tables ({})...", bits); info!(context, "Resetting tables ({})...", bits);
if 0 != bits & 1 { if 0 != bits & 1 {
sql::execute(context, &context.sql, "DELETE FROM jobs;", params![]).unwrap(); sql::execute(context, &context.sql, "DELETE FROM jobs;", params![]).unwrap();
@@ -85,153 +88,189 @@ pub fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
context.call_cb(Event::MsgsChanged { context.call_cb(Event::MsgsChanged {
chat_id: 0, chat_id: 0,
msg_id: MsgId::new(0), msg_id: 0,
}); });
1 1
} }
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> { unsafe fn dc_poke_eml_file(context: &Context, filename: *const libc::c_char) -> libc::c_int {
let data = dc_read_file(context, filename)?; /* mainly for testing, may be called by dc_import_spec() */
let mut success: libc::c_int = 0i32;
if let Err(err) = dc_receive_imf(context, &data, "import", 0, 0) { let mut data: *mut libc::c_char = ptr::null_mut();
println!("dc_receive_imf errored: {:?}", err); let mut data_bytes = 0;
if !(dc_read_file(
context,
filename,
&mut data as *mut *mut libc::c_char as *mut *mut libc::c_void,
&mut data_bytes,
) == 0i32)
{
dc_receive_imf(context, data, data_bytes, "import", 0, 0);
success = 1;
} }
Ok(()) free(data as *mut libc::c_void);
success
} }
/// Import a file to the database. /// Import a file to the database.
/// For testing, import a folder with eml-files, a single eml-file, e-mail plus public key and so on. /// For testing, import a folder with eml-files, a single eml-file, e-mail plus public key and so on.
/// For normal importing, use imex(). /// For normal importing, use dc_imex().
/// ///
/// @private @memberof Context /// @private @memberof Context
/// @param context The context as created by dc_context_new(). /// @param context The context as created by dc_context_new().
/// @param spec The file or directory to import. NULL for the last command. /// @param spec The file or directory to import. NULL for the last command.
/// @return 1=success, 0=error. /// @return 1=success, 0=error.
fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int { unsafe fn poke_spec(context: &Context, spec: *const libc::c_char) -> libc::c_int {
if !context.sql.is_open() { if !context.sql.is_open() {
error!(context, "Import: Database not opened."); error!(context, "Import: Database not opened.");
return 0; return 0;
} }
let mut read_cnt = 0; let ok_to_continue;
let mut success: libc::c_int = 0;
let real_spec: String; let real_spec: *mut libc::c_char;
let mut suffix: *mut libc::c_char = ptr::null_mut();
let mut read_cnt: libc::c_int = 0;
/* if `spec` is given, remember it for later usage; if it is not given, try to use the last one */ /* if `spec` is given, remember it for later usage; if it is not given, try to use the last one */
if let Some(spec) = spec { if !spec.is_null() {
real_spec = spec.to_string(); real_spec = dc_strdup(spec);
context context
.sql .sql
.set_raw_config(context, "import_spec", Some(&real_spec)) .set_config(context, "import_spec", Some(as_str(real_spec)))
.unwrap(); .unwrap();
ok_to_continue = true;
} else { } else {
let rs = context.sql.get_raw_config(context, "import_spec"); let rs = context.sql.get_config(context, "import_spec");
if rs.is_none() { if rs.is_none() {
error!(context, "Import: No file or folder given."); error!(context, "Import: No file or folder given.");
return 0; ok_to_continue = false;
}
real_spec = rs.unwrap();
}
if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) {
if suffix == "eml" && dc_poke_eml_file(context, &real_spec).is_ok() {
read_cnt += 1
}
} else {
/* import a directory */
let dir_name = std::path::Path::new(&real_spec);
let dir = std::fs::read_dir(dir_name);
if dir.is_err() {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
return 0;
} else { } else {
let dir = dir.unwrap(); ok_to_continue = true;
for entry in dir { }
if entry.is_err() { real_spec = rs.unwrap_or_default().strdup();
break; }
} if ok_to_continue {
let entry = entry.unwrap(); let ok_to_continue2;
let name_f = entry.file_name(); suffix = dc_get_filesuffix_lc(as_str(real_spec));
let name = name_f.to_string_lossy(); if !suffix.is_null() && strcmp(suffix, b"eml\x00" as *const u8 as *const libc::c_char) == 0
if name.ends_with(".eml") { {
let path_plus_name = format!("{}/{}", &real_spec, name); if 0 != dc_poke_eml_file(context, real_spec) {
info!(context, "Import: {}", path_plus_name); read_cnt += 1
if dc_poke_eml_file(context, path_plus_name).is_ok() { }
read_cnt += 1 ok_to_continue2 = true;
} else {
/* import a directory */
let dir_name = std::path::Path::new(as_str(real_spec));
let dir = std::fs::read_dir(dir_name);
if dir.is_err() {
error!(
context,
"Import: Cannot open directory \"{}\".",
as_str(real_spec),
);
ok_to_continue2 = false;
} else {
let dir = dir.unwrap();
for entry in dir {
if entry.is_err() {
break;
}
let entry = entry.unwrap();
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", as_str(real_spec), name);
info!(context, "Import: {}", path_plus_name);
let path_plus_name_c = CString::yolo(path_plus_name);
if 0 != dc_poke_eml_file(context, path_plus_name_c.as_ptr()) {
read_cnt += 1
}
} }
} }
ok_to_continue2 = true;
} }
} }
if ok_to_continue2 {
info!(
context,
"Import: {} items read from \"{}\".",
read_cnt,
as_str(real_spec)
);
if read_cnt > 0 {
context.call_cb(Event::MsgsChanged {
chat_id: 0,
msg_id: 0,
});
}
success = 1
}
} }
info!(
context, free(real_spec as *mut libc::c_void);
"Import: {} items read from \"{}\".", read_cnt, &real_spec free(suffix as *mut libc::c_void);
); success
if read_cnt > 0 {
context.call_cb(Event::MsgsChanged {
chat_id: 0,
msg_id: MsgId::new(0),
});
}
1
} }
fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) { unsafe fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let contact = Contact::get_by_id(context, msg.get_from_id()).expect("invalid contact"); let contact = Contact::get_by_id(context, dc_msg_get_from_id(msg)).expect("invalid contact");
let contact_name = contact.get_name(); let contact_name = contact.get_name();
let contact_id = contact.get_id(); let contact_id = contact.get_id();
let statestr = match msg.get_state() { let statestr = match dc_msg_get_state(msg) {
MessageState::OutPending => " o", MessageState::OutPending => " o",
MessageState::OutDelivered => "", MessageState::OutDelivered => "",
MessageState::OutMdnRcvd => " √√", MessageState::OutMdnRcvd => " √√",
MessageState::OutFailed => " !!", MessageState::OutFailed => " !!",
_ => "", _ => "",
}; };
let temp2 = dc_timestamp_to_str(msg.get_timestamp()); let temp2 = dc_timestamp_to_str(dc_msg_get_timestamp(msg));
let msgtext = msg.get_text(); let msgtext = dc_msg_get_text(msg);
info!( info!(
context, context,
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]", "{}#{}{}{}: {} (Contact#{}): {} {}{}{}{} [{}]",
prefix.as_ref(), prefix.as_ref(),
msg.get_id(), dc_msg_get_id(msg) as libc::c_int,
if msg.get_showpadlock() { "🔒" } else { "" }, if dc_msg_get_showpadlock(msg) {
if msg.has_location() { "📍" } else { "" }, "🔒"
} else {
""
},
if dc_msg_has_location(msg) { "📍" } else { "" },
&contact_name, &contact_name,
contact_id, contact_id,
msgtext.unwrap_or_default(), as_str(msgtext),
if msg.is_starred() { "" } else { "" }, if dc_msg_is_starred(msg) { "" } else { "" },
if msg.get_from_id() == 1 as libc::c_uint { if dc_msg_get_from_id(msg) == 1 as libc::c_uint {
"" ""
} else if msg.get_state() == MessageState::InSeen { } else if dc_msg_get_state(msg) == MessageState::InSeen {
"[SEEN]" "[SEEN]"
} else if msg.get_state() == MessageState::InNoticed { } else if dc_msg_get_state(msg) == MessageState::InNoticed {
"[NOTICED]" "[NOTICED]"
} else { } else {
"[FRESH]" "[FRESH]"
}, },
if msg.is_info() { "[INFO]" } else { "" }, if dc_msg_is_info(msg) { "[INFO]" } else { "" },
if msg.is_forwarded() {
"[FORWARDED]"
} else {
""
},
statestr, statestr,
&temp2, &temp2,
); );
free(msgtext as *mut libc::c_void);
} }
fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> { unsafe fn log_msglist(context: &Context, msglist: &Vec<u32>) -> Result<(), Error> {
let mut lines_out = 0; let mut lines_out = 0;
for &msg_id in msglist { for &msg_id in msglist {
if msg_id.is_daymarker() { if msg_id == 9 as libc::c_uint {
info!( info!(
context, context,
"--------------------------------------------------------------------------------" "--------------------------------------------------------------------------------"
); );
lines_out += 1 lines_out += 1
} else if !msg_id.is_special() { } else if msg_id > 0 {
if lines_out == 0 { if lines_out == 0 {
info!( info!(
context, context,
@@ -239,8 +278,8 @@ fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
); );
lines_out += 1 lines_out += 1
} }
let msg = Message::load_from_db(context, msg_id)?; let msg = dc_get_msg(context, msg_id)?;
log_msg(context, "", &msg); log_msg(context, "Msg", &msg);
} }
} }
if lines_out > 0 { if lines_out > 0 {
@@ -252,7 +291,7 @@ fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn log_contactlist(context: &Context, contacts: &Vec<u32>) { unsafe fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
let mut contacts = contacts.clone(); let mut contacts = contacts.clone();
if !contacts.contains(&1) { if !contacts.contains(&1) {
contacts.push(1); contacts.push(1);
@@ -304,7 +343,7 @@ fn chat_prefix(chat: &Chat) -> &'static str {
chat.typ.into() chat.typ.into()
} }
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> { pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let chat_id = *context.cmdline_sel_chat_id.read().unwrap(); let chat_id = *context.cmdline_sel_chat_id.read().unwrap();
let mut sel_chat = if chat_id > 0 { let mut sel_chat = if chat_id > 0 {
Chat::load_from_db(context, chat_id).ok() Chat::load_from_db(context, chat_id).ok()
@@ -315,9 +354,18 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let mut args = line.splitn(3, ' '); let mut args = line.splitn(3, ' ');
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() as *const _
};
let arg2 = args.next().unwrap_or_default(); let arg2 = args.next().unwrap_or_default();
let arg2_c = if arg2.is_empty() {
std::ptr::null()
} else {
arg2.strdup() as *const _
};
let blobdir = context.get_blobdir();
match arg0 { match arg0 {
"help" | "?" => match arg1 { "help" | "?" => match arg1 {
// TODO: reuse commands definition in main.rs. // TODO: reuse commands definition in main.rs.
@@ -348,7 +396,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
configure\n\ configure\n\
connect\n\ connect\n\
disconnect\n\ disconnect\n\
interrupt\n\
maybenetwork\n\ maybenetwork\n\
housekeeping\n\ housekeeping\n\
help imex (Import/Export)\n\ help imex (Import/Export)\n\
@@ -374,7 +421,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
sendimage <file> [<text>]\n\ sendimage <file> [<text>]\n\
sendfile <file> [<text>]\n\ sendfile <file> [<text>]\n\
draft [<text>]\n\ draft [<text>]\n\
devicemsg <text>\n\
listmedia\n\ listmedia\n\
archive <chat-id>\n\ archive <chat-id>\n\
unarchive <chat-id>\n\ unarchive <chat-id>\n\
@@ -401,32 +447,37 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
checkqr <qr-content>\n\ checkqr <qr-content>\n\
event <event-id to test>\n\ event <event-id to test>\n\
fileinfo <file>\n\ fileinfo <file>\n\
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
clear -- clear screen\n\ clear -- clear screen\n\
exit or quit\n\ exit or quit\n\
=============================================" ============================================="
), ),
}, },
"initiate-key-transfer" => match initiate_key_transfer(context) { "initiate-key-transfer" => {
Ok(setup_code) => println!( let setup_code = dc_initiate_key_transfer(context);
"Setup code for the transferred setup message: {}", if !setup_code.is_null() {
setup_code, println!(
), "Setup code for the transferred setup message: {}",
Err(err) => bail!("Failed to generate setup code: {}", err), as_str(setup_code),
}, );
free(setup_code as *mut libc::c_void);
} else {
bail!("Failed to generate setup code");
};
}
"get-setupcodebegin" => { "get-setupcodebegin" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let msg_id: MsgId = MsgId::new(arg1.parse()?); let msg_id: u32 = arg1.parse()?;
let msg = Message::load_from_db(context, msg_id)?; let msg = dc_get_msg(context, msg_id)?;
if msg.is_setupmessage() { if dc_msg_is_setupmessage(&msg) {
let setupcodebegin = msg.get_setupcodebegin(context); let setupcodebegin = dc_msg_get_setupcodebegin(context, &msg);
println!( println!(
"The setup code for setup message {} starts with: {}", "The setup code for setup message Msg#{} starts with: {}",
msg_id, msg_id,
setupcodebegin.unwrap_or_default(), as_str(setupcodebegin),
); );
free(setupcodebegin as *mut libc::c_void);
} else { } else {
bail!("{} is no setup message.", msg_id,); bail!("Msg#{} is no setup message.", msg_id,);
} }
} }
"continue-key-transfer" => { "continue-key-transfer" => {
@@ -434,28 +485,33 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
!arg1.is_empty() && !arg2.is_empty(), !arg1.is_empty() && !arg2.is_empty(),
"Arguments <msg-id> <setup-code> expected" "Arguments <msg-id> <setup-code> expected"
); );
continue_key_transfer(context, MsgId::new(arg1.parse()?), &arg2)?; if !dc_continue_key_transfer(context, arg1.parse()?, arg2_c) {
bail!("Continue key transfer failed");
}
} }
"has-backup" => { "has-backup" => {
has_backup(context, blobdir)?; let ret = dc_imex_has_backup(context, context.get_blobdir());
if ret.is_null() {
println!("No backup found.");
}
} }
"export-backup" => { "export-backup" => {
imex(context, ImexMode::ExportBackup, Some(blobdir)); dc_imex(context, 11, Some(context.get_blobdir()), ptr::null());
} }
"import-backup" => { "import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing."); ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(context, ImexMode::ImportBackup, Some(arg1)); dc_imex(context, 12, Some(arg1), ptr::null());
} }
"export-keys" => { "export-keys" => {
imex(context, ImexMode::ExportSelfKeys, Some(blobdir)); dc_imex(context, 1, Some(context.get_blobdir()), ptr::null());
} }
"import-keys" => { "import-keys" => {
imex(context, ImexMode::ImportSelfKeys, Some(blobdir)); dc_imex(context, 2, Some(context.get_blobdir()), ptr::null());
} }
"export-setup" => { "export-setup" => {
let setup_code = create_setup_code(context); let setup_code = dc_create_setup_code(context);
let file_name = blobdir.join("autocrypt-setup-message.html"); let file_name = context.get_blobdir().join("autocrypt-setup-message.html");
let file_content = render_setup_file(context, &setup_code)?; let file_content = dc_render_setup_file(context, &setup_code)?;
std::fs::write(&file_name, file_content)?; std::fs::write(&file_name, file_content)?;
println!( println!(
"Setup message written to: {}\nSetup code: {}", "Setup message written to: {}\nSetup code: {}",
@@ -464,7 +520,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
); );
} }
"poke" => { "poke" => {
ensure!(0 != poke_spec(context, Some(arg1)), "Poke failed"); ensure!(0 != poke_spec(context, arg1_c), "Poke failed");
} }
"reset" => { "reset" => {
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config"); ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
@@ -473,7 +529,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(0 != dc_reset_tables(context, bits), "Reset failed"); ensure!(0 != dc_reset_tables(context, bits), "Reset failed");
} }
"stop" => { "stop" => {
context.stop_ongoing(); dc_stop_ongoing_process(context);
} }
"set" => { "set" => {
ensure!(!arg1.is_empty(), "Argument <key> missing."); ensure!(!arg1.is_empty(), "Argument <key> missing.");
@@ -490,9 +546,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
"info" => { "info" => {
println!("{:#?}", context.get_info()); println!("{:#?}", context.get_info());
} }
"interrupt" => {
interrupt_inbox_idle(context, true);
}
"maybenetwork" => { "maybenetwork" => {
maybe_network(context); maybe_network(context);
} }
@@ -517,12 +570,15 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
for i in (0..cnt).rev() { for i in (0..cnt).rev() {
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?; let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
let temp_subtitle = chat.get_subtitle(context);
let temp_name = chat.get_name();
info!( info!(
context, context,
"{}#{}: {} [{} fresh]", "{}#{}: {} [{}] [{} fresh]",
chat_prefix(&chat), chat_prefix(&chat),
chat.get_id(), chat.get_id(),
chat.get_name(), temp_name,
temp_subtitle,
chat::get_fresh_msg_cnt(context, chat.get_id()), chat::get_fresh_msg_cnt(context, chat.get_id()),
); );
let lot = chatlist.get_summary(context, i, Some(&chat)); let lot = chatlist.get_summary(context, i, Some(&chat));
@@ -579,35 +635,21 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(sel_chat.is_some(), "Failed to select chat"); ensure!(sel_chat.is_some(), "Failed to select chat");
let sel_chat = sel_chat.as_ref().unwrap(); let sel_chat = sel_chat.as_ref().unwrap();
let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, None); let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, 0);
let members = chat::get_chat_contacts(context, sel_chat.id); let temp2 = sel_chat.get_subtitle(context);
let subtitle = if sel_chat.is_device_talk() { let temp_name = sel_chat.get_name();
"device-talk".to_string()
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
let contact = Contact::get_by_id(context, members[0])?;
contact.get_addr().to_string()
} else {
format!("{} member(s)", members.len())
};
info!( info!(
context, context,
"{}#{}: {} [{}]{}{}", "{}#{}: {} [{}]{}",
chat_prefix(sel_chat), chat_prefix(sel_chat),
sel_chat.get_id(), sel_chat.get_id(),
sel_chat.get_name(), temp_name,
subtitle, temp2,
if sel_chat.is_sending_locations() { if sel_chat.is_sending_locations() {
"📍" "📍"
} else { } else {
"" ""
}, },
match sel_chat.get_profile_image(context) {
Some(icon) => match icon.to_str() {
Some(icon) => format!(" Icon: {}", icon),
_ => " Icon: Err".to_string(),
},
_ => "".to_string(),
},
); );
log_msglist(context, &msglist)?; log_msglist(context, &msglist)?;
if let Some(draft) = chat::get_draft(context, sel_chat.get_id())? { if let Some(draft) = chat::get_draft(context, sel_chat.get_id())? {
@@ -629,7 +671,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
} }
"createchatbymsg" => { "createchatbymsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing"); ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?); let msg_id: u32 = arg1.parse()?;
let chat_id = chat::create_by_msg_id(context, msg_id)?; let chat_id = chat::create_by_msg_id(context, msg_id)?;
let chat = Chat::load_from_db(context, chat_id)?; let chat = Chat::load_from_db(context, chat_id)?;
@@ -721,7 +763,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let marker = location.marker.as_ref().unwrap_or(&default_marker); let marker = location.marker.as_ref().unwrap_or(&default_marker);
info!( info!(
context, context,
"Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} {} {}", "Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} Msg#{} {}",
location.location_id, location.location_id,
dc_timestamp_to_str(location.timestamp), dc_timestamp_to_str(location.timestamp),
location.latitude, location.latitude,
@@ -758,7 +800,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let longitude = arg2.parse()?; let longitude = arg2.parse()?;
let continue_streaming = location::set(context, latitude, longitude, 0.); let continue_streaming = location::set(context, latitude, longitude, 0.);
if continue_streaming { if 0 != continue_streaming {
println!("Success, streaming should be continued."); println!("Success, streaming should be continued.");
} else { } else {
println!("Success, streaming can be stoppped."); println!("Success, streaming can be stoppped.");
@@ -783,14 +825,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No file given."); ensure!(!arg1.is_empty(), "No file given.");
let mut msg = Message::new(if arg0 == "sendimage" { let mut msg = dc_msg_new(if arg0 == "sendimage" {
Viewtype::Image Viewtype::Image
} else { } else {
Viewtype::File Viewtype::File
}); });
msg.set_file(arg1, None); dc_msg_set_file(&mut msg, arg1_c, ptr::null());
if !arg2.is_empty() { if !arg2.is_empty() {
msg.set_text(Some(arg2.to_string())); dc_msg_set_text(&mut msg, arg2_c);
} }
chat::send_msg(context, sel_chat.as_ref().unwrap().get_id(), &mut msg)?; chat::send_msg(context, sel_chat.as_ref().unwrap().get_id(), &mut msg)?;
} }
@@ -812,8 +854,8 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
if !arg1.is_empty() { if !arg1.is_empty() {
let mut draft = Message::new(Viewtype::Text); let mut draft = dc_msg_new(Viewtype::Text);
draft.set_text(Some(arg1.to_string())); dc_msg_set_text(&mut draft, arg1_c);
chat::set_draft( chat::set_draft(
context, context,
sel_chat.as_ref().unwrap().get_id(), sel_chat.as_ref().unwrap().get_id(),
@@ -825,18 +867,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
println!("Draft deleted."); println!("Draft deleted.");
} }
} }
"devicemsg" => {
ensure!(
!arg1.is_empty(),
"Please specify text to add as device message."
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(context, None, Some(&mut msg))?;
}
"updatedevicechats" => {
context.update_device_chats()?;
}
"listmedia" => { "listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
@@ -850,9 +880,9 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
println!("{} images or videos: ", images.len()); println!("{} images or videos: ", images.len());
for (i, data) in images.iter().enumerate() { for (i, data) in images.iter().enumerate() {
if 0 == i { if 0 == i {
print!("{}", data); print!("Msg#{}", data);
} else { } else {
print!(", {}", data); print!(", Msg#{}", data);
} }
} }
print!("\n"); print!("\n");
@@ -860,7 +890,11 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
"archive" | "unarchive" => { "archive" | "unarchive" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing."); ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = arg1.parse()?; let chat_id = arg1.parse()?;
chat::archive(context, chat_id, arg0 == "archive")?; chat::archive(
context,
chat_id,
if arg0 == "archive" { true } else { false },
)?;
} }
"delchat" => { "delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing."); ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
@@ -869,9 +903,9 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
} }
"msginfo" => { "msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?); let id = arg1.parse()?;
let res = message::get_msg_info(context, id); let res = dc_get_msg_info(context, id);
println!("{}", res); println!("{}", as_str(res));
} }
"listfresh" => { "listfresh" => {
let msglist = context.get_fresh_msgs(); let msglist = context.get_fresh_msgs();
@@ -881,32 +915,37 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
} }
"forward" => { "forward" => {
ensure!( ensure!(
!arg1.is_empty() && !arg2.is_empty(), !arg1.is_empty() && arg2.is_empty(),
"Arguments <msg-id> <chat-id> expected" "Arguments <msg-id> <chat-id> expected"
); );
let mut msg_ids = [MsgId::new(0); 1]; let mut msg_ids = [0; 1];
let chat_id = arg2.parse()?; let chat_id = arg2.parse()?;
msg_ids[0] = MsgId::new(arg1.parse()?); msg_ids[0] = arg1.parse()?;
chat::forward_msgs(context, &msg_ids, chat_id)?; chat::forward_msgs(context, msg_ids.as_mut_ptr(), 1, chat_id);
} }
"markseen" => { "markseen" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = [MsgId::new(0); 1]; let mut msg_ids = [0; 1];
msg_ids[0] = MsgId::new(arg1.parse()?); msg_ids[0] = arg1.parse()?;
message::markseen_msgs(context, &msg_ids); dc_markseen_msgs(context, msg_ids.as_mut_ptr(), 1);
} }
"star" | "unstar" => { "star" | "unstar" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = [MsgId::new(0); 1]; let mut msg_ids = [0; 1];
msg_ids[0] = MsgId::new(arg1.parse()?); msg_ids[0] = arg1.parse()?;
message::star_msgs(context, &msg_ids, arg0 == "star"); dc_star_msgs(
context,
msg_ids.as_mut_ptr(),
1,
if arg0 == "star" { 1 } else { 0 },
);
} }
"delmsg" => { "delmsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut ids = [MsgId::new(0); 1]; let mut ids = [0; 1];
ids[0] = MsgId::new(arg1.parse()?); ids[0] = arg1.parse()?;
message::delete_msgs(context, &ids); dc_delete_msgs(context, ids.as_mut_ptr(), 1);
} }
"listcontacts" | "contacts" | "listverified" => { "listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all( let contacts = Contact::get_all(
@@ -938,14 +977,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let contact = Contact::get_by_id(context, contact_id)?; let contact = Contact::get_by_id(context, contact_id)?;
let name_n_addr = contact.get_name_n_addr(); let name_n_addr = contact.get_name_n_addr();
let mut res = format!( let mut res = format!("Contact info for: {}:\n\n", name_n_addr);
"Contact info for: {}:\nIcon: {}\n",
name_n_addr,
match contact.get_profile_image(context) {
Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(),
}
);
res += &Contact::get_encrinfo(context, contact_id)?; res += &Contact::get_encrinfo(context, contact_id)?;
@@ -996,21 +1028,19 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
"fileinfo" => { "fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing."); ensure!(!arg1.is_empty(), "Argument <file> missing.");
if let Ok(buf) = dc_read_file(context, &arg1) { if let Some(buf) = dc_read_file_safe(context, &arg1) {
let (width, height) = dc_get_filemeta(&buf)?; let (width, height) = dc_get_filemeta(&buf)?;
println!("width={}, height={}", width, height); println!("width={}, height={}", width, height);
} else { } else {
bail!("Command failed."); bail!("Command failed.");
} }
} }
"emptyserver" => {
ensure!(!arg1.is_empty(), "Argument <flags> missing");
message::dc_empty_server(context, arg1.parse()?);
}
"" => (), "" => (),
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0), _ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
} }
free(arg1_c as *mut _);
free(arg2_c as *mut _);
Ok(()) Ok(())
} }

View File

@@ -23,9 +23,11 @@ use std::sync::{Arc, Mutex, RwLock};
use deltachat::config; use deltachat::config;
use deltachat::configure::*; use deltachat::configure::*;
use deltachat::context::*; use deltachat::context::*;
use deltachat::dc_tools::*;
use deltachat::job::*; use deltachat::job::*;
use deltachat::oauth2::*; use deltachat::oauth2::*;
use deltachat::securejoin::*; use deltachat::securejoin::*;
use deltachat::x::*;
use deltachat::Event; use deltachat::Event;
use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType; use rustyline::config::OutputStreamType;
@@ -43,6 +45,7 @@ use self::cmdline::*;
fn receive_event(_context: &Context, event: Event) -> libc::uintptr_t { fn receive_event(_context: &Context, event: Event) -> libc::uintptr_t {
match event { match event {
Event::GetString { .. } => {}
Event::Info(msg) => { Event::Info(msg) => {
/* do not show the event as this would fill the screen */ /* do not show the event as this would fill the screen */
println!("{}", msg); println!("{}", msg);
@@ -150,11 +153,11 @@ 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()); perform_imap_jobs(&ctx.read().unwrap());
perform_inbox_fetch(&ctx.read().unwrap()); perform_imap_fetch(&ctx.read().unwrap());
while_running!({ while_running!({
let context = ctx.read().unwrap(); let context = ctx.read().unwrap();
perform_inbox_idle(&context); perform_imap_idle(&context);
}); });
}); });
}); });
@@ -202,7 +205,7 @@ 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); interrupt_imap_idle(context);
interrupt_mvbox_idle(context); interrupt_mvbox_idle(context);
interrupt_sentbox_idle(context); interrupt_sentbox_idle(context);
interrupt_smtp_idle(context); interrupt_smtp_idle(context);
@@ -235,7 +238,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 +253,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 +267,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 +293,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 +303,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,7 +311,7 @@ const CONTACT_COMMANDS: [&str; 6] = [
"delcontact", "delcontact",
"cleanupcontacts", "cleanupcontacts",
]; ];
const MISC_COMMANDS: [&str; 9] = [ const MISC_COMMANDS: [&'static str; 9] = [
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help", "getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
]; ];
@@ -334,8 +337,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> {
@@ -403,7 +406,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),
@@ -434,10 +437,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" => {
@@ -455,9 +463,9 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
} }
"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()); perform_imap_jobs(&ctx.read().unwrap());
} }
} }
"configure" => { "configure" => {
@@ -513,6 +521,8 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
_ => dc_cmdline(&ctx.read().unwrap(), line)?, _ => dc_cmdline(&ctx.read().unwrap(), line)?,
} }
free(arg1_c as *mut _);
Ok(ExitResult::Continue) Ok(ExitResult::Continue)
} }

View File

@@ -11,8 +11,7 @@ use deltachat::configure::*;
use deltachat::contact::*; use deltachat::contact::*;
use deltachat::context::*; use deltachat::context::*;
use deltachat::job::{ use deltachat::job::{
perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle, perform_imap_fetch, perform_imap_idle, perform_imap_jobs, perform_smtp_idle, perform_smtp_jobs,
perform_smtp_jobs,
}; };
use deltachat::Event; use deltachat::Event;
@@ -36,80 +35,93 @@ fn cb(_ctx: &Context, event: Event) -> usize {
} }
fn main() { fn main() {
let dir = tempdir().unwrap(); unsafe {
let dbfile = dir.path().join("db.sqlite"); let dir = tempdir().unwrap();
println!("creating database {:?}", dbfile); let dbfile = dir.path().join("db.sqlite");
let ctx = println!("creating database {:?}", dbfile);
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context"); let ctx =
let running = Arc::new(RwLock::new(true)); Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context");
let info = ctx.get_info(); let running = Arc::new(RwLock::new(true));
let duration = time::Duration::from_millis(4000); let info = ctx.get_info();
println!("info: {:#?}", info); 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() {
perform_imap_jobs(&ctx1);
if *r1.read().unwrap() { if *r1.read().unwrap() {
perform_inbox_idle(&ctx1); perform_imap_fetch(&ctx1);
if *r1.read().unwrap() {
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); perform_smtp_jobs(&ctx1);
if *r1.read().unwrap() { if *r1.read().unwrap() {
perform_smtp_idle(&ctx1); perform_smtp_idle(&ctx1);
}
} }
});
println!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 2, "missing password");
let pw = args[1].clone();
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
configure(&ctx);
thread::sleep(duration);
println!("sending a message");
let contact_id =
Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
println!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
for i in 0..chats.len() {
let summary = chats.get_summary(&ctx, 0, None);
let text1 = summary.get_text1();
let text2 = summary.get_text2();
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
} }
});
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::job::interrupt_imap_idle(&ctx);
deltachat::job::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");
} }
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");
} }

View File

@@ -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\'&lt\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"

View File

@@ -5,4 +5,3 @@
# It is recommended to check this file in to source control so that # It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases. # everyone who runs the test benefits from these saved cases.
cc 679506fe9ac59df773f8cfa800fdab5f0a32fe49d2ab370394000a1aa5bc2a72 # shrinks to buf = "%0A" cc 679506fe9ac59df773f8cfa800fdab5f0a32fe49d2ab370394000a1aa5bc2a72 # shrinks to buf = "%0A"
cc e34960438edb2426904b44fb4215154e7e2880f2fd1c3183b98bfcc76fec4882 # shrinks to input = " 0"

View File

@@ -97,7 +97,7 @@ If you want to run "liveconfig" functional tests you can set
chat devs. chat devs.
- or the path of a file that contains two lines, each describing - or the path of a file that contains two lines, each describing
via "addr=... mail_pw=..." a test account login that will via "addr=... mail_pwd=..." a test account login that will
be used for the live tests. be used for the live tests.
With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -6,26 +6,20 @@
import os import os
import subprocess import subprocess
import sys import os
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"
if "DCC_RS_DEV" not in os.environ: if "DCC_RS_DEV" not in os.environ:
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.environ["DCC_RS_DEV"] = dn os.environ["DCC_RS_DEV"] = dn
# 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" os.environ["RUSTFLAGS"] = "-g"
subprocess.check_call([ subprocess.check_call([
"cargo", "build", "-p", "deltachat_ffi", "--" + target "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", "." ])
])

View File

@@ -29,10 +29,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:

View File

@@ -1,7 +1,6 @@
""" 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 os
import re import re
@@ -16,17 +15,15 @@ import deltachat
from . import const from . import const
from .capi import ffi, lib from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
from .chat import Chat from .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, os_name=None, 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
@@ -34,15 +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 os_name: this will be put to the X-Mailer header in outgoing messages
:param debug: turn on debug logging for events.
""" """
self._dc_context = ffi.gc( self._dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)), 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:
@@ -53,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:
@@ -75,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.
@@ -96,12 +78,9 @@ class Account(object):
""" """
self._check_config_key(name) self._check_config_key(name)
name = name.encode("utf8") name = name.encode("utf8")
value = value.encode("utf8")
if name == b"addr" and self.is_configured(): if name == b"addr" and self.is_configured():
raise ValueError("can not change 'addr' after account is configured.") raise ValueError("can not change 'addr' after account is configured.")
if value is not None:
value = value.encode("utf8")
else:
value = ffi.NULL
lib.dc_set_config(self._dc_context, name, value) lib.dc_set_config(self._dc_context, name, value)
def get_config(self, name): def get_config(self, name):
@@ -137,47 +116,16 @@ class Account(object):
""" """
return lib.dc_is_configured(self._dc_context) return lib.dc_is_configured(self._dc_context)
def set_avatar(self, img_path):
"""Set self avatar.
:raises ValueError: if profile image could not be set
:returns: None
"""
if img_path is None:
self.set_config("selfavatar", None)
else:
assert os.path.exists(img_path), img_path
self.set_config("selfavatar", img_path)
def check_is_configured(self): def check_is_configured(self):
""" Raise ValueError if this account is not configured. """ """ Raise ValueError if this account is not configured. """
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.
@@ -187,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)
@@ -201,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)
@@ -227,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.chatting.Contact` objects.
""" """
flags = 0 flags = 0
query = as_dc_charpointer(query) query = as_dc_charpointer(query)
@@ -245,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:
@@ -262,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:
@@ -280,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),
@@ -307,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.
@@ -341,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]
@@ -356,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
@@ -453,7 +353,7 @@ class Account(object):
""" setup contact and return a Chat after contact is established. """ setup contact and return a Chat after contact is established.
Note that this function may block for a long time as messages are exchanged 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 with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
is returned. is returned.
:param qr: valid "setup contact" QR code (all other QR codes will result in an exception) :param qr: valid "setup contact" QR code (all other QR codes will result in an exception)
""" """
@@ -467,7 +367,7 @@ class Account(object):
""" join a chat group through a QR code. """ join a chat group through a QR code.
Note that this function may block for a long time as messages are exchanged 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 with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
is returned which is the chat that we just joined. 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) :param qr: valid "join-group" QR code (all other QR codes will result in an exception)
@@ -478,19 +378,7 @@ class Account(object):
raise ValueError("could not join group") raise ValueError("could not join group")
return Chat(self, chat_id) return Chat(self, chat_id)
def stop_ongoing(self): def start_threads(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.
@@ -498,13 +386,12 @@ 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. """
@@ -515,7 +402,6 @@ class Account(object):
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
@@ -528,26 +414,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:
@@ -560,14 +427,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)
@@ -580,37 +443,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")
@@ -662,11 +505,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

View File

@@ -1,16 +1,58 @@
""" Chat and Location related API. """ """ chatting related objects: Contact, Chat, Message. """
import mimetypes import mimetypes
import calendar
import json
from datetime import datetime
import os import os
from . import props
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 +109,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.
@@ -109,30 +144,6 @@ class Chat(object):
# ------ chat messaging API ------------------------------ # ------ chat messaging API ------------------------------
def send_msg(self, msg):
"""send a message by using a ready Message object.
:param msg: a :class:`deltachat.message.Message` instance
previously returned by
e.g. :meth:`deltachat.message.Message.new_empty` or
:meth:`prepare_file`.
:raises ValueError: if message can not be sent.
:returns: a :class:`deltachat.message.Message` instance as
sent out. This is the same object as was passed in, which
has been modified with the new state of the core.
"""
if msg.is_out_preparing():
assert msg.id != 0
# get a fresh copy of dc_msg, the core needs it
msg = Message.from_db(self.account, msg.id)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
# modify message in place to avoid bad state for the caller
msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg
return msg
def send_text(self, text): def send_text(self, text):
""" send a text message and return the resulting Message instance. """ send a text message and return the resulting Message instance.
@@ -154,12 +165,9 @@ class Chat(object):
:raises ValueError: if message can not be send/chat does not exist. :raises ValueError: if message can not be send/chat does not exist.
:returns: the resulting :class:`deltachat.message.Message` instance :returns: the resulting :class:`deltachat.message.Message` instance
""" """
msg = Message.new_empty(self.account, view_type="file") msg = self.prepare_message_file(path=path, mime_type=mime_type)
msg.set_file(path, mime_type) self.send_prepared(msg)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg) return msg
if sent_id == 0:
raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id)
def send_image(self, path): def send_image(self, path):
""" send an image message and return the resulting Message instance. """ send an image message and return the resulting Message instance.
@@ -169,12 +177,9 @@ class Chat(object):
:returns: the resulting :class:`deltachat.message.Message` instance :returns: the resulting :class:`deltachat.message.Message` instance
""" """
mime_type = mimetypes.guess_type(path)[0] mime_type = mimetypes.guess_type(path)[0]
msg = Message.new_empty(self.account, view_type="image") msg = self.prepare_message_file(path=path, mime_type=mime_type, view_type="image")
msg.set_file(path, mime_type) self.send_prepared(msg)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg) return msg
if sent_id == 0:
raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id)
def prepare_message(self, msg): def prepare_message(self, msg):
""" create a new prepared message. """ create a new prepared message.
@@ -273,12 +278,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):
@@ -306,10 +305,9 @@ 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 :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
@@ -360,80 +358,3 @@ class Chat(object):
if dc_res == ffi.NULL: if dc_res == ffi.NULL:
return None return None
return from_dc_charpointer(dc_res) 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__

View File

@@ -47,39 +47,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_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
@@ -97,67 +77,14 @@ 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_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_QR|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:

View File

@@ -1,59 +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)
def get_profile_image(self):
"""Get contact profile image.
:returns: path to profile image, None if no profile image exists.
"""
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)

View File

@@ -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.
@@ -146,25 +150,25 @@ class Message(object):
if mime_headers: if mime_headers:
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref)) s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
if isinstance(s, bytes): if isinstance(s, bytes):
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)
@@ -174,7 +178,7 @@ class Message(object):
@property @property
def _msgstate(self): def _msgstate(self):
if self.id == 0: if self.id == 0:
dc_msg = self._dc_msg dc_msg = self.message._dc_msg
else: else:
# load message from db to get a fresh/current state # load message from db to get a fresh/current state
dc_msg = ffi.gc( dc_msg = ffi.gc(

View File

@@ -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)

View File

@@ -4,7 +4,6 @@ import pytest
import requests import requests
import time import time
from deltachat import Account from deltachat import Account
from deltachat import const
from deltachat.capi import lib from deltachat.capi import lib
import tempfile import tempfile
@@ -85,21 +84,16 @@ class SessionLiveConfigFromFile:
class SessionLiveConfigFromURL: class SessionLiveConfigFromURL:
def __init__(self, url, create_token): def __init__(self, url, create_token):
self.configlist = [] self.configlist = []
self.url = url for i in range(2):
self.create_token = create_token res = requests.post(url, json={"token_create_user": int(create_token)})
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url, json={"token_create_user": int(self.create_token)})
if res.status_code != 200: if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res)) pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json() d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"]) config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config) self.configlist.append(config)
return config
def get(self, index):
return self.configlist[index]
def exists(self): def exists(self):
return bool(self.configlist) return bool(self.configlist)
@@ -156,41 +150,21 @@ 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:
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
return session_liveconfig.get(self.live_count)
def get_online_config(self):
if not session_liveconfig: if not session_liveconfig:
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig") pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
configdict = session_liveconfig.get(self.live_count) configdict = session_liveconfig.get(self.live_count)
self.live_count += 1 self.live_count += 1
if "e2ee_enabled" not in configdict: if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1" 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 = self.make_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()
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): def get_two_online_accounts(self):
ac1 = self.get_online_configuring_account() ac1 = self.get_online_configuring_account()
ac2 = self.get_online_configuring_account() ac2 = self.get_online_configuring_account()
@@ -232,13 +206,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 break

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,8 +1,6 @@
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
@@ -16,20 +14,11 @@ class TestOfflineAccountBasic:
with pytest.raises(ValueError): with pytest.raises(ValueError):
Account(p.strpath) Account(p.strpath)
def test_os_name(self, tmpdir):
p = tmpdir.join("hello.db")
# we can't easily test if os_name is used in X-Mailer
# outgoing messages without a full Online test
# but we at least check Account accepts the arg
ac1 = Account(p.strpath, os_name="solarpunk")
ac1.get_info()
def test_getinfo(self, acfactory): def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account() ac1 = acfactory.get_unconfigured_account()
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()
@@ -48,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):
@@ -109,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:
@@ -123,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)
@@ -163,39 +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]) @pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified): def test_group_chat_qr(self, acfactory, ac1, verified):
ac2 = acfactory.get_configured_offline_account() ac2 = acfactory.get_configured_offline_account()
@@ -277,9 +221,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
@@ -351,10 +293,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]
@@ -390,258 +332,66 @@ class TestOfflineChat:
assert not res.is_ask_verifygroup() assert not res.is_ask_verifygroup()
assert res.contact_id == 10 assert res.contact_id == 10
def test_group_chat_many_members_add_remove(self, ac1, lp):
lp.sec("ac1: creating group chat with 10 other members")
chat = ac1.create_group_chat(name="title1")
contacts = []
for i in range(10):
contact = ac1.create_contact("some{}@example.org".format(i))
contacts.append(contact)
chat.add_contact(contact)
num_contacts = len(chat.get_contacts())
assert num_contacts == 11
lp.sec("ac1: removing two contacts and checking things are right")
chat.remove_contact(contacts[9])
chat.remove_contact(contacts[3])
assert len(chat.get_contacts()) == 9
class TestOnlineAccount: class TestOnlineAccount:
def get_chat(self, ac1, ac2, both_created=False): def get_chat(self, ac1, ac2):
c2 = ac1.create_contact(email=ac2.get_config("addr")) c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2) chat = ac1.create_chat_by_contact(c2)
assert chat.id > const.DC_CHAT_ID_LAST_SPECIAL assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
if both_created:
ac2.create_chat_by_contact(ac2.create_contact(email=ac1.get_config("addr")))
return chat return chat
def test_configure_canceled(self, acfactory): def test_one_account_send(self, acfactory):
ac1 = acfactory.get_online_configuring_account() ac1 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 200) c2 = ac1.create_contact(email=ac1.get_config("addr"))
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) chat = ac1.create_chat_by_contact(c2)
assert chat.id > const.DC_CHAT_ID_LAST_SPECIAL assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
wait_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")
ac1.set_config("bcc_self", "1")
lp.sec("send out message with bcc to ourselves")
msg_out = chat.send_text("message2") msg_out = chat.send_text("message2")
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED") # wait for own account to receive
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, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2)
msg_out = chat.send_text("message1")
# 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 assert ev[2] == msg_out.id
# wait for send out (BCC) msg_in = ac2.get_message_by_id(msg_out.id)
assert ac1.get_config("bcc_self") == "1" assert msg_in.text == "message1"
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() def test_forward_messages(self, acfactory):
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() ac1, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2) 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)
chat.send_msg(msg1)
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()
ac2 = acfactory.get_online_configuring_account(mvbox=True)
wait_configuration_progress(ac2, 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):
ac1 = acfactory.get_online_configuring_account(mvbox=True)
ac1.set_config("bcc_self", "1")
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac2, 1000)
wait_configuration_progress(ac1, 1000)
chat = self.get_chat(ac1, ac2)
chat.send_text("message1")
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):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2)
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() ac1, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2, both_created=True)
lp.sec("sending message")
msg_out = chat.send_text("message2")
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()
# make DC's life harder wrt to encodings
ac1.set_config("displayname", "ä name")
lp.sec("ac1: create chat with ac2") lp.sec("ac1: create chat with ac2")
chat = self.get_chat(ac1, ac2) chat = self.get_chat(ac1, ac2)
@@ -659,8 +409,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()
assert msg_in.get_sender_contact().display_name == ac1.get_config("displayname")
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
@@ -682,52 +430,11 @@ 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_mdn_asymetric(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")
chat = self.get_chat(ac1, ac2, both_created=True)
# make sure mdns are enabled (usually enabled by default already)
ac1.set_config("mdns_enabled", "1")
ac2.set_config("mdns_enabled", "1")
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
assert len(chat.get_messages()) == 1
lp.sec("disable ac1 MDNs")
ac1.set_config("mdns_enabled", "0")
lp.sec("wait for ac2 to receive message")
msg = ac2.wait_next_incoming_message()
assert len(msg.chat.get_messages()) == 1
lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg])
lp.sec("ac1: waiting for incoming activity")
# wait for MOVED event because even ignored read-receipts should be moved
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
assert len(chat.get_messages()) == 1
assert not msg_out.is_out_mdn_received()
def test_send_and_receive_will_encrypt_decrypt(self, acfactory, lp): def test_send_and_receive_will_encrypt_decrypt(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts() ac1, ac2 = acfactory.get_two_online_accounts()
@@ -736,7 +443,6 @@ class TestOnlineAccount:
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")
assert not msg_out.is_encrypted()
lp.sec("wait for ac2 to receive message") lp.sec("wait for ac2 to receive message")
ev = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED") ev = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
@@ -754,86 +460,6 @@ class TestOnlineAccount:
assert ev[2] > msg_out.id assert ev[2] > msg_out.id
msg_back = ac1.get_message_by_id(ev[2]) msg_back = ac1.get_message_by_id(ev[2])
assert msg_back.text == "message-back" assert msg_back.text == "message-back"
assert msg_back.is_encrypted()
# Test that we do not gossip peer keys in 1-to-1 chat,
# as it makes no sense to gossip to peers their own keys.
# Gossip is only sent in encrypted messages,
# and we sent encrypted msg_back right above.
assert chat2b.get_summary()["gossiped_timestamp"] == 0
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() ac1, ac2 = acfactory.get_two_online_accounts()
@@ -875,30 +501,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):
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") 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]
@@ -908,18 +523,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
@@ -927,46 +531,20 @@ 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] # first try a bad setup code
lp.sec("try a bad setup code")
with pytest.raises(ValueError): with pytest.raises(ValueError):
msg.continue_key_transfer(str(reversed(setup_code))) 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): def test_qr_setup_contact(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts() ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin") lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
@@ -984,93 +562,9 @@ class TestOnlineAccount:
lp.sec("ac2: start QR-code based join-group protocol") lp.sec("ac2: start QR-code based join-group protocol")
ch = ac2.qr_join_chat(qr) ch = ac2.qr_join_chat(qr)
assert ch.id >= 10 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) 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): def test_set_get_profile_image(self, acfactory, data, 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_contact_avatar(self, acfactory, data, lp):
lp.sec("configuring ac1 and ac2")
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: set own profile image")
p = data.get_path("d.png")
ac1.set_avatar(p)
lp.sec("ac1: create 1:1 chat with ac2")
chat = self.get_chat(ac1, ac2, both_created=True)
msg = chat.send_text("hi -- do you see my brand new avatar?")
assert not msg.is_encrypted()
lp.sec("ac2: wait for receiving message and avatar from ac1")
msg1 = ac2.wait_next_incoming_message()
assert not msg1.chat.is_deaddrop()
received_path = msg1.get_sender_contact().get_profile_image()
assert open(received_path, "rb").read() == open(p, "rb").read()
lp.sec("ac2: set own profile image")
p = data.get_path("d.png")
ac2.set_avatar(p)
lp.sec("ac2: send back message")
m = msg1.chat.send_text("yes, i received your avatar -- how do you like mine?")
assert m.is_encrypted()
lp.sec("ac1: wait for receiving message and avatar from ac2")
msg2 = ac1.wait_next_incoming_message()
received_path = msg2.get_sender_contact().get_profile_image()
assert received_path is not None, "did not get avatar through encrypted message"
assert open(received_path, "rb").read() == open(p, "rb").read()
ac2._evlogger.consume_events()
ac1._evlogger.consume_events()
# XXX not sure if the following is correct / possible. you may remove it
lp.sec("ac1: delete profile image from chat, and send message to ac2")
ac1.set_avatar(None)
m = msg2.chat.send_text("i don't like my avatar anymore and removed it")
assert m.is_encrypted()
lp.sec("ac2: wait for message along with avatar deletion of ac1")
msg3 = ac2.wait_next_incoming_message()
assert msg3.get_sender_contact().get_profile_image() is None
def test_set_get_group_image(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_two_online_accounts() ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("create unpromoted group chat") lp.sec("create unpromoted group chat")
@@ -1122,126 +616,3 @@ class TestOnlineAccount:
chat1b = ac1.create_chat_by_message(ev[2]) chat1b = ac1.create_chat_by_message(ev[2])
assert chat1b.get_profile_image() is None assert chat1b.get_profile_image() is None
assert chat.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 TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):
lp.sec("creating and configuring five accounts")
ac1 = acfactory.get_online_configuring_account()
accounts = [acfactory.get_online_configuring_account() for i in range(3)]
wait_configuration_progress(ac1, 1000)
for acc in accounts:
wait_configuration_progress(acc, 1000)
lp.sec("ac1: creating group chat with 3 other members")
chat = ac1.create_group_chat("title1")
contacts = []
chars = list("äöüsr")
for acc in accounts:
contact = ac1.create_contact(acc.get_config("addr"), name=chars.pop())
contacts.append(contact)
chat.add_contact(contact)
# make sure the other side accepts our messages
c1 = acc.create_contact(ac1.get_config("addr"), "ä member")
acc.create_chat_by_contact(c1)
assert not chat.is_promoted()
lp.sec("ac1: send mesage to new group chat")
chat.send_text("hello")
assert chat.is_promoted()
num_contacts = len(chat.get_contacts())
assert num_contacts == 3 + 1
lp.sec("ac2: checking that the chat arrived correctly")
ac2 = accounts[0]
msg = ac2.wait_next_incoming_message()
assert msg.text == "hello"
print("chat is", msg.chat)
assert len(msg.chat.get_contacts()) == 4
lp.sec("ac1: removing one contacts and checking things are right")
to_remove = msg.chat.get_contacts()[-1]
msg.chat.remove_contact(to_remove)
sysmsg = ac1.wait_next_incoming_message()
assert to_remove.addr in sysmsg.text
assert len(sysmsg.chat.get_contacts()) == 3
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)

View File

@@ -1,49 +1,10 @@
from __future__ import print_function from __future__ import print_function
import os.path
import shutil
import pytest
from filecmp import cmp from filecmp import cmp
from conftest import wait_configuration_progress, wait_msgs_changed
from deltachat import const from deltachat import const
from conftest import wait_configuration_progress, wait_msgs_changed
class TestOnlineInCreation: class TestInCreation:
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
lp.sec("Creating in-creation file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join('file.txt').ensure(file=1)
with pytest.raises(Exception):
chat.prepare_message_file(src.strpath)
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
lp.sec("Creating file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join('file.txt')
src.write("hello there\n")
chat.send_file(src.strpath)
blob_src = os.path.join(ac1.get_blobdir(), 'file.txt')
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp): def test_forward_increation(self, acfactory, data, lp):
ac1 = acfactory.get_online_configuring_account() ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account() ac2 = acfactory.get_online_configuring_account()
@@ -56,10 +17,7 @@ class TestOnlineInCreation:
wait_msgs_changed(ac1, 0, 0) # why no chat id? wait_msgs_changed(ac1, 0, 0) # why no chat id?
lp.sec("create a message with a file in creation") lp.sec("create a message with a file in creation")
orig = data.get_path("d.png") path = data.get_path("d.png")
path = os.path.join(ac1.get_blobdir(), 'd.png')
with open(path, "x") as fp:
fp.write("preparing")
prepared_original = chat.prepare_message_file(path) prepared_original = chat.prepare_message_file(path)
assert prepared_original.is_out_preparing() assert prepared_original.is_out_preparing()
wait_msgs_changed(ac1, chat.id, prepared_original.id) wait_msgs_changed(ac1, chat.id, prepared_original.id)
@@ -80,7 +38,6 @@ class TestOnlineInCreation:
lp.sec("finish creating the file and send it") lp.sec("finish creating the file and send it")
assert prepared_original.is_out_preparing() assert prepared_original.is_out_preparing()
shutil.copyfile(orig, path)
chat.send_prepared(prepared_original) chat.send_prepared(prepared_original)
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered() assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
wait_msgs_changed(ac1, chat.id, prepared_original.id) wait_msgs_changed(ac1, chat.id, prepared_original.id)
@@ -100,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, orig, shallow=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, orig, shallow=False) assert cmp(received_copy.filename, path, False)

View File

@@ -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
@@ -41,7 +41,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 +59,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 +83,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

View File

@@ -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")

View File

@@ -1,38 +1,37 @@
[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 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-rerunfailures
pytest-timeout pytest-timeout
pytest-xdist pytest-xdist
auditwheel
pdbpp pdbpp
requests 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,29 +43,19 @@ 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 -ra addopts = -v -rs --reruns 3 --reruns-delay 2
python_files = tests/test_*.py python_files = tests/test_*.py
norecursedirs = .tox norecursedirs = .tox
xfail_strict=true xfail_strict=true
timeout = 60 timeout = 60

View File

@@ -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" && -z "$DCC_PY_LIVECONFIG" ]; then
export DCC_PY_LIVECONFIG=liveconfig export DCC_PY_LIVECONFIG=liveconfig
fi fi
tox "$@" tox "$@"

View File

@@ -1 +1 @@
nightly-2019-11-06 nightly-2019-08-13

View File

@@ -1,65 +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", "check"])
subprocess.call(["git", "add", "-u"])
# subprocess.call(["cargo", "update", "-p", "deltachat"])
print("after commit make sure to: ")
print("")
print(" git tag {}".format(newversion))
print("")

209
spec.md
View File

@@ -1,10 +1,8 @@
# Chat-over-Email specification # Chat-over-Email specification
Version 0.20.0 Version 0.19.0
This document describes how emails can be used This document describes how emails can be used to implement typical messenger functions while staying compatible to existing MUAs.
to implement typical messenger functions
while staying compatible to existing MUAs.
- [Encryption](#encryption) - [Encryption](#encryption)
- [Outgoing messages](#outgoing-messages) - [Outgoing messages](#outgoing-messages)
@@ -22,14 +20,10 @@ while staying compatible to existing MUAs.
# Encryption # Encryption
Messages SHOULD be encrypted by the Messages SHOULD be encrypted by the [Autocrypt](https://autocrypt.org/level1.html) standard; `prefer-encrypt=mutual` MAY be set by default.
[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 Meta data (at least the subject and all chat-headers) SHOULD be encrypted by the [Memoryhole](https://github.com/autocrypt/memoryhole) standard.
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
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. `Chat: Encrypted message` where the part after the colon MAY be localized.
@@ -37,8 +31,7 @@ the subject of encrypted messages SHOULD be replaced by the string
Messengers MUST add a `Chat-Version: 1.0` header to 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, For filtering and smart appearance of the messages in normal MUAs,
the `Subject` header SHOULD start with the characters `Chat:` the `Subject` header SHOULD start with the characters `Chat:` and SHOULD be an excerpt of the message.
and SHOULD be an excerpt of the message.
Replies to messages MAY follow the typical `Re:`-format. Replies to messages MAY follow the typical `Re:`-format.
The body MAY contain text which MUST have the content type `text/plain` The body MAY contain text which MUST have the content type `text/plain`
@@ -48,8 +41,8 @@ The text MAY be divided into a user-text-part and a footer-part using the
line `-- ` (minus, minus, space, lineend). line `-- ` (minus, minus, space, lineend).
The user-text-part MUST contain only user generated content. The user-text-part MUST contain only user generated content.
User generated content are eg. texts a user has actually typed User generated content are eg. texts a user has actually typed or pasted or
or pasted or forwarded from another user. forwarded from another user.
Full quotes, footers or sth. like that MUST NOT go to the user-text-part. Full quotes, footers or sth. like that MUST NOT go to the user-text-part.
From: sender@domain From: sender@domain
@@ -63,19 +56,14 @@ Full quotes, footers or sth. like that MUST NOT go to the user-text-part.
# Incoming messages # Incoming messages
The `Chat-Version` header MAY be used The `Chat-Version` header MAY be used to detect if a messages comes from a compatible messenger.
to detect if a messages comes from a compatible messenger.
The `Subject` header MUST NOT be used The `Subject` header MUST NOT be used to detect compatible messengers, groups or whatever.
to detect compatible messengers, groups or whatever.
Messenger SHOULD show the `Subject` Messenger SHOULD show the `Subject` if the message comes from a normal MUA together with the email-body.
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.
The email-body SHOULD be converted
to plain text, full-quotes and similar regions SHOULD be cut.
Attachments SHOULD be shown where possible. Attachments SHOULD be shown where possible. If an attachment cannot be shown, a non-distracting warning SHOULD be printed.
If an attachment cannot be shown, a non-distracting warning SHOULD be printed.
# Forwarded messages # Forwarded messages
@@ -102,26 +90,21 @@ which SHOULD be anonymized or just a placeholder.
Hello world! Hello world!
Incoming forwarded messages are detected by the header. Incoming forwarded messages are detected by the header.
The messenger SHOULD mark these messages in a way that The messenger SHOULD mark these messages in a way that it becomes obvious
it becomes obvious that the message is not created by the sender. that the message is not created by the sender.
Note that most messengers do not show the original sender of forwarded messages Note that most messengers do not show the original sender with forwarded messages
but MUAs typically expose the sender in the UI. but MUAs typically expose the sender in the UI.
# Groups # Groups
Groups are chats with usually more than one recipient, Groups are chats with usually more than one recipient, each defined by an email-address.
each defined by an email-address.
The sender plus the recipients are the group members. The sender plus the recipients are the group members.
To allow different groups with the same members, To allow different groups with the same members, groups are identified by a group-id.
groups are identified by a group-id. The group-id MUST be created only from the characters `0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`.
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. Groups MUST have a group-name. The group-name is any non-zero-length UTF-8 string.
The group-name is any non-zero-length UTF-8 string.
Groups MAY have a group-image. Groups MAY have a group-image.
@@ -130,77 +113,57 @@ Groups MAY have a group-image.
All group members MUST be added to the `From`/`To` headers. 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-id MUST be written to the `Chat-Group-ID` header.
The group-name MUST be written to `Chat-Group-Name` 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 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 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.
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, To identify the group-id on replies from normal MUAs, the group-id MUST also be added to
the group-id MUST also be added to the message-id of outgoing messages. the message-id of outgoing messages. The message-id MUST have the
The message-id MUST have the format `Gr.<group-id>.<unique data>`. format `Gr.<group-id>.<unique data>`.
From: member1@domain From: member1@domain
To: member2@domain, member3@domain To: member2@domain, member3@domain
Chat-Version: 1.0 Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ Chat-Group-ID: 1234xyZ
Chat-Group-Name: My Group Chat-Group-Name: My Group
Message-ID: Gr.12345uvwxyZ.0001@domain Message-ID: Gr.1234xyZ.0001@domain
Subject: Chat: My Group: Hello group ... Subject: Chat: My Group: Hello group ...
Hello group - this group contains three members Hello group - this group contains three members
Messengers adding the member list in the form `Name <email-address>` Messengers adding the member list in the form `Name <email-address>` MUST take care only to spread the names authorized by the contacts themselves.
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
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). such situations happen more frequently).
## Incoming group messages ## Incoming group messages
The messenger MUST search incoming messages for the group-id The messenger MUST search incoming messages for the group-id in the following headers: `Chat-Group-ID`,
in the following headers: `Chat-Group-ID`,
`Message-ID`, `In-Reply-To` and `References` (in this order). `Message-ID`, `In-Reply-To` and `References` (in this order).
If the messenger finds a valid and existent group-id, If the messenger finds a valid and existent group-id, the message SHOULD be assigned to the given group.
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 the messenger finds a valid but not existent group-id, If no group-id is found, the message MAY be assigned to a normal single-user chat with the email-address given in `From`.
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 ## Add and remove members
Messenger clients MUST construct the member list Messenger clients MUST construct the member list from the `From`/`To` headers only on the first group message or if they see a `Chat-Group-Member-Added` or `Chat-Group-Member-Removed` action header.
from the `From`/`To` headers only on the first group message Both headers MUST have the email-address of the added or removed member as the value.
or if they see a `Chat-Group-Member-Added` 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
or `Chat-Group-Member-Removed` action header. does not expect adding a user to a _message_ will also add him to the _group_ "forever").
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 messenger SHOULD send an explicit mail for each added or removed member.
The body of the message SHOULD contain The body of the message SHOULD contain a localized description about what happened
a localized description about what happened
and the message SHOULD appear as a message or action from the sender. and the message SHOULD appear as a message or action from the sender.
From: member1@domain From: member1@domain
To: member2@domain, member3@domain, member4@domain To: member2@domain, member3@domain, member4@domain
Chat-Version: 1.0 Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ Chat-Group-ID: 1234xyZ
Chat-Group-Name: My Group Chat-Group-Name: My Group
Chat-Group-Member-Added: member4@domain Chat-Group-Member-Added: member4@domain
Message-ID: Gr.12345uvwxyZ.0002@domain Message-ID: Gr.1234xyZ.0002@domain
Subject: Chat: My Group: Hello, ... Subject: Chat: My Group: Hello, ...
Hello, I've added member4@domain to our group. Now we have 4 members. Hello, I've added member4@domain to our group. Now we have 4 members.
@@ -210,10 +173,10 @@ To remove a member:
From: member1@domain From: member1@domain
To: member2@domain, member3@domain To: member2@domain, member3@domain
Chat-Version: 1.0 Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ Chat-Group-ID: 1234xyZ
Chat-Group-Name: My Group Chat-Group-Name: My Group
Chat-Group-Member-Removed: member4@domain Chat-Group-Member-Removed: member4@domain
Message-ID: Gr.12345uvwxyZ.0003@domain Message-ID: Gr.1234xyZ.0003@domain
Subject: Chat: My Group: Hello, ... Subject: Chat: My Group: Hello, ...
Hello, I've removed member4@domain from our group. Now we have 3 members. Hello, I've removed member4@domain from our group. Now we have 3 members.
@@ -227,17 +190,16 @@ 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 new group name goes to the header `Chat-Group-Name`.
The messenger SHOULD send an explicit mail for each name change. The messenger SHOULD send an explicit mail for each name change.
The body of the message SHOULD contain The body of the message SHOULD contain a localized description about what happened
a localized description about what happened
and the message SHOULD appear as a message or action from the sender. and the message SHOULD appear as a message or action from the sender.
From: member1@domain From: member1@domain
To: member2@domain, member3@domain To: member2@domain, member3@domain
Chat-Version: 1.0 Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ Chat-Group-ID: 1234xyZ
Chat-Group-Name: Our Group Chat-Group-Name: Our Group
Chat-Group-Name-Changed: My Group Chat-Group-Name-Changed: My Group
Message-ID: Gr.12345uvwxyZ.0004@domain Message-ID: Gr.1234xyZ.0004@domain
Subject: Chat: Our Group: Hello, ... Subject: Chat: Our Group: Hello, ...
Hello, I've changed the group name from "My Group" to "Our Group". Hello, I've changed the group name from "My Group" to "Our Group".
@@ -246,27 +208,23 @@ and the message SHOULD appear as a message or action from the sender.
## Set group image ## Set group image
A group MAY have a group-image. A group MAY have a group-image.
To change or set the 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
the messenger MUST attach an image file to a message value set to the image name.
and MUST add the header `Chat-Group-Avatar`
with the value set to the image name.
To remove the group-image, To remove the group-image, the messenger MUST add the header `Chat-Group-Image: 0`.
the messenger MUST add the header `Chat-Group-Avatar: 0`.
The messenger SHOULD send an explicit mail for each group image change. The messenger SHOULD send an explicit mail for each group image change.
The body of the message SHOULD contain The body of the message SHOULD contain a localized description about what happened
a localized description about what happened
and the message SHOULD appear as a message or action from the sender. and the message SHOULD appear as a message or action from the sender.
From: member1@domain From: member1@domain
To: member2@domain, member3@domain To: member2@domain, member3@domain
Chat-Version: 1.0 Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ Chat-Group-ID: 1234xyZ
Chat-Group-Name: Our Group Chat-Group-Name: Our Group
Chat-Group-Avatar: image.jpg Chat-Group-Image: image.jpg
Message-ID: Gr.12345uvwxyZ.0005@domain Message-ID: Gr.1234xyZ.0005@domain
Subject: Chat: Our Group: Hello, ... Subject: Chat: Our Group: Hello, ...
Content-Type: multipart/mixed; boundary="==break==" Content-Type: multipart/mixed; boundary="==break=="
@@ -281,35 +239,26 @@ and the message SHOULD appear as a message or action from the sender.
/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBw ... /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBw ...
--==break==-- --==break==--
The image format SHOULD be image/jpeg or image/png. 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.
To save data, it is RECOMMENDED
to add a `Chat-Group-Avatar` only on image changes.
# Set profile image # Set profile image
A user MAY have a profile-image that MAY be spread to their contacts. A user MAY have a profile-image that MAY be spread to his contacts.
To change or set the profile-image, 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
the messenger MUST attach an image file to a message value set to the image name.
and MUST add the header `Chat-User-Avatar`
with the value set to the image name.
To remove the profile-image, To remove the profile-image, the messenger MUST add the header `Chat-Profile-Image: 0`.
the messenger MUST add the header `Chat-User-Avatar: 0`.
To spread the image, To spread the image, the messenger MAY send the profile image together with the next mail to a given contact
the messenger MAY send the profile image (to do this only once, the messenger has to keep a `profile_image_update_state` somewhere).
together with the next mail to a given contact Alternatively, the messenger MAY send an explicit mail for each profile-image change to all contacts using a compatible messenger.
(to do this only once,
the messenger has to keep a `user_avatar_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. The messenger SHOULD NOT send an explicit mail to normal MUAs.
From: sender@domain From: sender@domain
To: rcpt@domain To: rcpt@domain
Chat-Version: 1.0 Chat-Version: 1.0
Chat-User-Avatar: photo.jpg Chat-Profile-Image: photo.jpg
Subject: Chat: Hello, ... Subject: Chat: Hello, ...
Content-Type: multipart/mixed; boundary="==break==" Content-Type: multipart/mixed; boundary="==break=="
@@ -324,47 +273,35 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQYFBA ... AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQYFBA ...
--==break==-- --==break==--
The image format SHOULD be image/jpeg or image/png. 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
Note that `Chat-User-Avatar` may appear together with all other headers, `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.
eg. there may be a `Chat-User-Avatar` and a `Chat-Group-Avatar` header
in the same message.
To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
only on image changes.
# Miscellaneous # Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual. Messengers SHOULD use the header `Chat-Predecessor` instead of `In-Reply-To` as
the latter one results in infinite threads on typical MUAs.
Messengers SHOULD add a `Chat-Voice-message: 1` header Messengers SHOULD add a `Chat-Voice-message: 1` header if an attached audio file is a voice message.
if an attached audio file is a voice message.
Messengers MAY add a `Chat-Duration` header Messengers MAY add a `Chat-Duration` header to specify the duration of attached audio or video files.
to specify the duration of attached audio or video files.
The value MUST be the duration in milliseconds. The value MUST be the duration in milliseconds.
This allows the receiver to show the time without knowing the file format. This allows the receiver to show the time without knowing the file format.
In-Reply-To: Gr.12345uvwxyZ.0005@domain Chat-Predecessor: foo123@domain
Chat-Voice-Message: 1 Chat-Voice-Message: 1
Chat-Duration: 10000 Chat-Duration: 10000
Messengers MAY send and receive Message Disposition Notifications 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))
(MDNs, [RFC 8098](https://tools.ietf.org/html/rfc8098), 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
[RFC 3503](https://tools.ietf.org/html/rfc3503)) standard).
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 ## Sync messages
If some action is required by a message header, 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.
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 We define the effective date of a message as the sending time of the message as indicated by its Date header,
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. or the time of first receipt if that date is in the future or unavailable.

View File

@@ -1,14 +1,12 @@
//! # 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::key::*; use crate::key::*;
/// Possible values for encryption preference /// Possible values for encryption preference
@@ -42,13 +40,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 +55,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 +63,66 @@ impl Aheader {
} }
} }
pub fn from_headers( pub fn from_imffields(wanted_from: &str, header: *const mailimf_fields) -> Option<Self> {
context: &Context, if header.is_null() {
wanted_from: &str, return None;
headers: &[mailparse::MailHeader<'_>],
) -> Option<Self> {
use mailparse::MailHeaderMap;
if let Ok(Some(value)) = headers.get_first_value("Autocrypt") {
match Self::from_str(&value) {
Ok(header) => {
if addr_cmp(&header.addr, wanted_from) {
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, 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 +131,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 +168,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 +194,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 +211,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 +224,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 +262,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"));
}
} }

View File

@@ -1,634 +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 self::image::GenericImageView;
use crate::constants::AVATAR_SIZE;
use crate::context::Context;
use crate::events::Event;
extern crate image;
/// 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 new_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
}
pub fn recode_to_avatar_size(&self, context: &Context) -> Result<(), BlobError> {
let blob_abs = self.to_abs_path();
let img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
return Ok(());
}
let img = img.thumbnail(AVATAR_SIZE, AVATAR_SIZE);
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
Ok(())
}
}
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,
},
RecodeFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
cause: image::ImageError,
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::RecodeFailure {
blobdir, blobname, ..
} => write!(f, "Failed to recode {} in {}", 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::new_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::new_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::new_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"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,10 @@
//! # Chat list module
use crate::chat::*; use crate::chat::*;
use crate::constants::*; use crate::constants::*;
use crate::contact::*; use crate::contact::*;
use crate::context::*; use crate::context::*;
use crate::error::Result; use crate::error::Result;
use crate::lot::Lot; use crate::lot::Lot;
use crate::message::{Message, MessageState, MsgId}; use crate::message::*;
use crate::stock::StockMessage; use crate::stock::StockMessage;
/// An object representing a single chatlist in memory. /// An object representing a single chatlist in memory.
@@ -36,7 +34,7 @@ use crate::stock::StockMessage;
#[derive(Debug)] #[derive(Debug)]
pub struct Chatlist { pub struct Chatlist {
/// 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 Chatlist {
@@ -60,7 +58,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 +69,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
@@ -88,12 +86,25 @@ impl Chatlist {
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 +112,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,59 +152,48 @@ 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, 0));
} }
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0))); ids.push((DC_CHAT_ID_ARCHIVED_LINK, 0));
} }
Ok(Chatlist { ids }) Ok(Chatlist { ids })
@@ -227,7 +204,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 +221,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.
@@ -279,24 +258,30 @@ impl Chatlist {
let chat_loaded: Chat; let chat_loaded: Chat;
let chat = if let Some(chat) = chat { let chat = if let Some(chat) = chat {
chat chat
} else if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0) {
chat_loaded = chat;
&chat_loaded
} else { } else {
return ret; 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) { let lastmsg = if 0 != lastmsg_id {
if lastmsg.from_id != DC_CONTACT_ID_SELF if let Ok(lastmsg) = dc_msg_load_from_db(context, lastmsg_id) {
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup) if lastmsg.from_id != 1 as libc::c_uint
{ && (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok(); {
} lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok();
}
Some(lastmsg) Some(lastmsg)
} else {
None
}
} else { } else {
None None
}; };
@@ -314,7 +299,6 @@ impl Chatlist {
} }
} }
/// Returns 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
@@ -326,62 +310,19 @@ pub fn dc_get_archived_cnt(context: &Context) -> u32 {
.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_get_value(
"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;" )
), .unwrap_or_default()
params![],
)
}
#[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);
}
} }

View File

@@ -1,15 +1,12 @@
//! # Key-value configuration management
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_tools::*; use crate::dc_tools::*;
use crate::error::Error;
use crate::job::*; use crate::job::*;
use crate::stock::StockMessage; use crate::stock::StockMessage;
use rusqlite::NO_PARAMS;
/// The available configuration keys. /// The available configuration keys.
#[derive( #[derive(
@@ -22,45 +19,30 @@ 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"))] #[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,
@@ -68,23 +50,19 @@ pub enum Config {
ConfiguredMailPw, ConfiguredMailPw,
ConfiguredMailPort, ConfiguredMailPort,
ConfiguredMailSecurity, ConfiguredMailSecurity,
ConfiguredImapCertificateChecks,
ConfiguredSendServer, ConfiguredSendServer,
ConfiguredSendUser, ConfiguredSendUser,
ConfiguredSendPw, ConfiguredSendPw,
ConfiguredSendPort, ConfiguredSendPort,
ConfiguredSmtpCertificateChecks,
ConfiguredServerFlags, ConfiguredServerFlags,
ConfiguredSendSecurity, ConfiguredSendSecurity,
ConfiguredE2EEEnabled, 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,
} }
@@ -94,13 +72,13 @@ impl Context {
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(self, &p).to_str().unwrap().to_string())
} }
Config::SysVersion => Some((&*DC_VERSION_STR).clone()), Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
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() {
@@ -114,46 +92,27 @@ 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 => { Config::Selfavatar if value.is_some() => {
let rel_path = std::fs::canonicalize(value.unwrap())?;
self.sql self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", NO_PARAMS)?; .set_config(self, key, Some(&rel_path.to_string_lossy()))
self.sql
.set_raw_config_bool(self, "attach_selfavatar", true)?;
match value {
Some(value) => {
let blob = BlobObject::new_from_path(&self, value)?;
blob.recode_to_avatar_size(self)?;
self.sql.set_raw_config(self, key, Some(blob.as_name()))
}
None => self.sql.set_raw_config(self, key, None),
}
} }
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); 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); 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); interrupt_mvbox_idle(self);
ret ret
} }
@@ -165,9 +124,9 @@ impl Context {
value value
}; };
self.sql.set_raw_config(self, key, val) self.sql.set_config(self, key, val)
} }
_ => self.sql.set_raw_config(self, key, value), _ => self.sql.set_config(self, key, value),
} }
} }
} }
@@ -190,10 +149,6 @@ mod tests {
use std::str::FromStr; use std::str::FromStr;
use std::string::ToString; use std::string::ToString;
use crate::test_utils::*;
use std::fs::File;
use std::io::Write;
#[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");
@@ -210,34 +165,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_outside_blobdir() -> failure::Fallible<()> {
let t = dummy_context();
let avatar_src = t.dir.path().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src)?.write_all(avatar_bytes)?;
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!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
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");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src)?.write_all(avatar_bytes)?;
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(())
}
} }

View File

@@ -1,110 +1,81 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
use quick_xml; use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText}; use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::constants::*; use crate::constants::*;
use crate::context::Context; use crate::context::Context;
use crate::dc_tools::*;
use crate::login_param::LoginParam; use crate::login_param::LoginParam;
use crate::x::*;
use super::read_url::read_url; use super::read_autoconf_file;
/* ******************************************************************************
#[derive(Debug, Fail)] * Thunderbird's Autoconfigure
pub enum Error { ******************************************************************************/
#[fail(display = "Invalid email address: {:?}", _0)] /* documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
InvalidEmailAddress(String), #[repr(C)]
struct moz_autoconfigure_t<'a> {
#[fail(display = "XML error at position {}", position)] pub in_0: &'a LoginParam,
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_emaildomain: &'a str,
pub in_emaillocalpart: &'a str, pub in_emaillocalpart: &'a str,
pub out: LoginParam, pub out: LoginParam,
pub out_imap_set: bool, pub out_imap_set: libc::c_int,
pub out_smtp_set: bool, pub out_smtp_set: libc::c_int,
pub tag_server: MozServer, pub tag_server: libc::c_int,
pub tag_config: MozConfigTag, pub tag_config: libc::c_int,
} }
#[derive(Debug, PartialEq)] pub unsafe fn moz_autoconfigure(
enum MozServer { context: &Context,
Undefined, url: &str,
Imap, param_in: &LoginParam,
Smtp, ) -> Option<LoginParam> {
} let xml_raw = read_autoconf_file(context, url);
if xml_raw.is_null() {
#[derive(Debug)] return None;
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. // Split address into local part and domain part.
let p = in_emailaddr let p = param_in.addr.find("@");
.find('@') if p.is_none() {
.ok_or_else(|| Error::InvalidEmailAddress(in_emailaddr.to_string()))?; free(xml_raw as *mut libc::c_void);
let (in_emaillocalpart, in_emaildomain) = in_emailaddr.split_at(p); return None;
}
let (in_emaillocalpart, in_emaildomain) = param_in.addr.split_at(p.unwrap());
let in_emaildomain = &in_emaildomain[1..]; let in_emaildomain = &in_emaildomain[1..];
let mut moz_ac = MozAutoconfigure { let mut reader = quick_xml::Reader::from_str(as_str(xml_raw));
in_emailaddr, reader.trim_text(true);
let mut buf = Vec::new();
let mut moz_ac = moz_autoconfigure_t {
in_0: param_in,
in_emaildomain, in_emaildomain,
in_emaillocalpart, in_emaillocalpart,
out: LoginParam::new(), out: LoginParam::new(),
out_imap_set: false, out_imap_set: 0,
out_smtp_set: false, out_smtp_set: 0,
tag_server: MozServer::Undefined, tag_server: 0,
tag_config: MozConfigTag::Undefined, tag_config: 0,
}; };
let mut buf = Vec::new();
loop { loop {
let event = reader match reader.read_event(&mut buf) {
.read_event(&mut buf) Ok(quick_xml::events::Event::Start(ref e)) => {
.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) moz_autoconfigure_starttag_cb(e, &mut moz_ac, &reader)
} }
quick_xml::events::Event::End(ref e) => moz_autoconfigure_endtag_cb(e, &mut moz_ac), Ok(quick_xml::events::Event::End(ref e)) => moz_autoconfigure_endtag_cb(e, &mut moz_ac),
quick_xml::events::Event::Text(ref e) => { Ok(quick_xml::events::Event::Text(ref e)) => {
moz_autoconfigure_text_cb(e, &mut moz_ac, &reader) moz_autoconfigure_text_cb(e, &mut moz_ac, &reader)
} }
quick_xml::events::Event::Eof => break, Err(e) => {
error!(
context,
"Configure xml: Error at position {}: {:?}",
reader.buffer_position(),
e
);
}
Ok(quick_xml::events::Event::Eof) => break,
_ => (), _ => (),
} }
buf.clear(); buf.clear();
@@ -115,37 +86,24 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
|| moz_ac.out.send_server.is_empty() || moz_ac.out.send_server.is_empty()
|| moz_ac.out.send_port == 0 || moz_ac.out.send_port == 0
{ {
Err(Error::IncompleteAutoconfig(moz_ac.out)) let r = moz_ac.out.to_string();
} else { warn!(context, "Bad or incomplete autoconfig: {}", r,);
Ok(moz_ac.out) free(xml_raw as *mut libc::c_void);
return None;
} }
}
pub fn moz_autoconfigure( free(xml_raw as *mut libc::c_void);
context: &Context, Some(moz_ac.out)
url: &str,
param_in: &LoginParam,
) -> Result<LoginParam> {
let xml_raw = read_url(context, url)?;
let res = parse_xml(&param_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>( fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
event: &BytesText, event: &BytesText,
moz_ac: &mut MozAutoconfigure, moz_ac: &mut moz_autoconfigure_t,
reader: &quick_xml::Reader<B>, reader: &quick_xml::Reader<B>,
) { ) {
let val = event.unescape_and_decode(reader).unwrap_or_default(); let val = event.unescape_and_decode(reader).unwrap_or_default();
let addr = moz_ac.in_emailaddr; let addr = &moz_ac.in_0.addr;
let email_local = moz_ac.in_emaillocalpart; let email_local = moz_ac.in_emaillocalpart;
let email_domain = moz_ac.in_emaildomain; let email_domain = moz_ac.in_emaildomain;
@@ -155,12 +113,12 @@ fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
.replace("%EMAILLOCALPART%", email_local) .replace("%EMAILLOCALPART%", email_local)
.replace("%EMAILDOMAIN%", email_domain); .replace("%EMAILDOMAIN%", email_domain);
match moz_ac.tag_server { if moz_ac.tag_server == 1 {
MozServer::Imap => match moz_ac.tag_config { match moz_ac.tag_config {
MozConfigTag::Hostname => moz_ac.out.mail_server = val, 10 => moz_ac.out.mail_server = val,
MozConfigTag::Port => moz_ac.out.mail_port = val.parse().unwrap_or_default(), 11 => moz_ac.out.mail_port = val.parse().unwrap_or_default(),
MozConfigTag::Username => moz_ac.out.mail_user = val, 12 => moz_ac.out.mail_user = val,
MozConfigTag::Sockettype => { 13 => {
let val_lower = val.to_lowercase(); let val_lower = val.to_lowercase();
if val_lower == "ssl" { if val_lower == "ssl" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32 moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
@@ -173,12 +131,13 @@ fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
} }
} }
_ => {} _ => {}
}, }
MozServer::Smtp => match moz_ac.tag_config { } else if moz_ac.tag_server == 2 {
MozConfigTag::Hostname => moz_ac.out.send_server = val, match moz_ac.tag_config {
MozConfigTag::Port => moz_ac.out.send_port = val.parse().unwrap_or_default(), 10 => moz_ac.out.send_server = val,
MozConfigTag::Username => moz_ac.out.send_user = val, 11 => moz_ac.out.send_port = val.parse().unwrap_or_default(),
MozConfigTag::Sockettype => { 12 => moz_ac.out.send_user = val,
13 => {
let val_lower = val.to_lowercase(); let val_lower = val.to_lowercase();
if val_lower == "ssl" { if val_lower == "ssl" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32 moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
@@ -191,34 +150,29 @@ fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
} }
} }
_ => {} _ => {}
}, }
MozServer::Undefined => {}
} }
} }
fn moz_autoconfigure_endtag_cb(event: &BytesEnd, moz_ac: &mut MozAutoconfigure) { fn moz_autoconfigure_endtag_cb(event: &BytesEnd, moz_ac: &mut moz_autoconfigure_t) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "incomingserver" { if tag == "incomingserver" {
if moz_ac.tag_server == MozServer::Imap { moz_ac.tag_server = 0;
moz_ac.out_imap_set = true; moz_ac.tag_config = 0;
} moz_ac.out_imap_set = 1;
moz_ac.tag_server = MozServer::Undefined;
moz_ac.tag_config = MozConfigTag::Undefined;
} else if tag == "outgoingserver" { } else if tag == "outgoingserver" {
if moz_ac.tag_server == MozServer::Smtp { moz_ac.tag_server = 0;
moz_ac.out_smtp_set = true; moz_ac.tag_config = 0;
} moz_ac.out_smtp_set = 1;
moz_ac.tag_server = MozServer::Undefined;
moz_ac.tag_config = MozConfigTag::Undefined;
} else { } else {
moz_ac.tag_config = MozConfigTag::Undefined; moz_ac.tag_config = 0;
} }
} }
fn moz_autoconfigure_starttag_cb<B: std::io::BufRead>( fn moz_autoconfigure_starttag_cb<B: std::io::BufRead>(
event: &BytesStart, event: &BytesStart,
moz_ac: &mut MozAutoconfigure, moz_ac: &mut moz_autoconfigure_t,
reader: &quick_xml::Reader<B>, reader: &quick_xml::Reader<B>,
) { ) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
@@ -235,115 +189,25 @@ fn moz_autoconfigure_starttag_cb<B: std::io::BufRead>(
.unwrap_or_default() .unwrap_or_default()
.to_lowercase(); .to_lowercase();
if typ == "imap" && !moz_ac.out_imap_set { if typ == "imap" && moz_ac.out_imap_set == 0 {
MozServer::Imap 1
} else { } else {
MozServer::Undefined 0
} }
} else { } else {
MozServer::Undefined 0
}; };
moz_ac.tag_config = MozConfigTag::Undefined; moz_ac.tag_config = 0;
} else if tag == "outgoingserver" { } else if tag == "outgoingserver" {
moz_ac.tag_server = if !moz_ac.out_smtp_set { moz_ac.tag_server = if moz_ac.out_smtp_set == 0 { 2 } else { 0 };
MozServer::Smtp moz_ac.tag_config = 0;
} else {
MozServer::Undefined
};
moz_ac.tag_config = MozConfigTag::Undefined;
} else if tag == "hostname" { } else if tag == "hostname" {
moz_ac.tag_config = MozConfigTag::Hostname; moz_ac.tag_config = 10;
} else if tag == "port" { } else if tag == "port" {
moz_ac.tag_config = MozConfigTag::Port; moz_ac.tag_config = 11;
} else if tag == "sockettype" { } else if tag == "sockettype" {
moz_ac.tag_config = MozConfigTag::Sockettype; moz_ac.tag_config = 13;
} else if tag == "username" { } else if tag == "username" {
moz_ac.tag_config = MozConfigTag::Username; moz_ac.tag_config = 12;
}
}
#[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);
} }
} }

View File

@@ -1,269 +1,213 @@
//! Outlook's Autodiscover
use quick_xml; use quick_xml;
use quick_xml::events::BytesEnd; use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::constants::*; use crate::constants::*;
use crate::context::Context; use crate::context::Context;
use crate::dc_tools::*;
use crate::login_param::LoginParam; use crate::login_param::LoginParam;
use crate::x::*;
use std::ptr;
use super::read_url::read_url; use super::read_autoconf_file;
/* ******************************************************************************
#[derive(Debug, Fail)] * Outlook's Autodiscover
pub enum Error { ******************************************************************************/
#[fail(display = "XML error at position {}", position)] #[repr(C)]
InvalidXml { struct outlk_autodiscover_t<'a> {
position: usize, pub in_0: &'a LoginParam,
#[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: LoginParam,
pub out_imap_set: bool, pub out_imap_set: libc::c_int,
pub out_smtp_set: bool, pub out_smtp_set: libc::c_int,
pub config_type: Option<String>, pub tag_config: libc::c_int,
pub config_server: String, pub config: [*mut libc::c_char; 6],
pub config_port: i32, pub redirect: *mut libc::c_char,
pub config_ssl: String,
pub config_redirecturl: Option<String>,
} }
enum ParsingResult { pub unsafe fn outlk_autodiscover(
LoginParam(LoginParam), context: &Context,
RedirectUrl(String), url__: &str,
} param_in: &LoginParam,
) -> Option<LoginParam> {
fn parse_xml(xml_raw: &str) -> Result<ParsingResult> { let mut xml_raw: *mut libc::c_char = ptr::null_mut();
let mut outlk_ad = OutlookAutodiscover { let mut url = url__.strdup();
let mut outlk_ad = outlk_autodiscover_t {
in_0: param_in,
out: LoginParam::new(), out: LoginParam::new(),
out_imap_set: false, out_imap_set: 0,
out_smtp_set: false, out_smtp_set: 0,
config_type: None, tag_config: 0,
config_server: String::new(), config: [ptr::null_mut(); 6],
config_port: 0, redirect: ptr::null_mut(),
config_ssl: String::new(),
config_redirecturl: None,
}; };
let ok_to_continue;
let mut reader = quick_xml::Reader::from_str(&xml_raw); let mut i = 0;
reader.trim_text(true);
let mut buf = Vec::new();
let mut current_tag: Option<String> = None;
loop { loop {
let event = reader if !(i < 10) {
.read_event(&mut buf) ok_to_continue = true;
.map_err(|error| Error::InvalidXml { break;
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(); memset(
&mut outlk_ad as *mut outlk_autodiscover_t as *mut libc::c_void,
0,
::std::mem::size_of::<outlk_autodiscover_t>(),
);
xml_raw = read_autoconf_file(context, as_str(url));
if xml_raw.is_null() {
ok_to_continue = false;
break;
}
let mut reader = quick_xml::Reader::from_str(as_str(xml_raw));
reader.trim_text(true);
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf) {
Ok(quick_xml::events::Event::Start(ref e)) => {
outlk_autodiscover_starttag_cb(e, &mut outlk_ad)
}
Ok(quick_xml::events::Event::End(ref e)) => {
outlk_autodiscover_endtag_cb(e, &mut outlk_ad)
}
Ok(quick_xml::events::Event::Text(ref e)) => {
outlk_autodiscover_text_cb(e, &mut outlk_ad, &reader)
}
Err(e) => {
error!(
context,
"Configure xml: Error at position {}: {:?}",
reader.buffer_position(),
e
);
}
Ok(quick_xml::events::Event::Eof) => break,
_ => (),
}
buf.clear();
}
if !(!outlk_ad.config[5].is_null()
&& 0 != *outlk_ad.config[5usize].offset(0isize) as libc::c_int)
{
ok_to_continue = true;
break;
}
free(url as *mut libc::c_void);
url = dc_strdup(outlk_ad.config[5usize]);
outlk_clean_config(&mut outlk_ad);
free(xml_raw as *mut libc::c_void);
xml_raw = ptr::null_mut();
i += 1;
} }
// XML redirect via redirecturl if ok_to_continue {
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() if outlk_ad.out.mail_server.is_empty()
|| outlk_ad.out.mail_port == 0 || outlk_ad.out.mail_port == 0
|| outlk_ad.out.send_server.is_empty() || outlk_ad.out.send_server.is_empty()
|| outlk_ad.out.send_port == 0 || outlk_ad.out.send_port == 0
{ {
return Err(Error::IncompleteAutoconfig(outlk_ad.out)); let r = outlk_ad.out.to_string();
} warn!(context, "Bad or incomplete autoconfig: {}", r,);
ParsingResult::LoginParam(outlk_ad.out) free(url as *mut libc::c_void);
} else { free(xml_raw as *mut libc::c_void);
ParsingResult::RedirectUrl(outlk_ad.config_redirecturl.unwrap()) outlk_clean_config(&mut outlk_ad);
};
Ok(res)
}
pub fn outlk_autodiscover( return None;
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) free(url as *mut libc::c_void);
free(xml_raw as *mut libc::c_void);
outlk_clean_config(&mut outlk_ad);
Some(outlk_ad.out)
} }
fn outlk_autodiscover_endtag_cb(event: &BytesEnd, outlk_ad: &mut OutlookAutodiscover) { unsafe fn outlk_clean_config(mut outlk_ad: *mut outlk_autodiscover_t) {
for i in 0..6 {
free((*outlk_ad).config[i] as *mut libc::c_void);
(*outlk_ad).config[i] = ptr::null_mut();
}
}
fn outlk_autodiscover_text_cb<B: std::io::BufRead>(
event: &BytesText,
outlk_ad: &mut outlk_autodiscover_t,
reader: &quick_xml::Reader<B>,
) {
let val = event.unescape_and_decode(reader).unwrap_or_default();
unsafe {
free(outlk_ad.config[outlk_ad.tag_config as usize].cast());
outlk_ad.config[outlk_ad.tag_config as usize] = val.trim().strdup();
}
}
unsafe fn outlk_autodiscover_endtag_cb(event: &BytesEnd, outlk_ad: &mut outlk_autodiscover_t) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "protocol" { if tag == "protocol" {
if let Some(type_) = &outlk_ad.config_type { if !outlk_ad.config[1].is_null() {
let port = outlk_ad.config_port; let port = dc_atoi_null_is_0(outlk_ad.config[3]);
let ssl_on = outlk_ad.config_ssl == "on"; let ssl_on = (!outlk_ad.config[4].is_null()
let ssl_off = outlk_ad.config_ssl == "off"; && strcasecmp(
if type_ == "imap" && !outlk_ad.out_imap_set { outlk_ad.config[4],
outlk_ad.out.mail_server = b"on\x00" as *const u8 as *const libc::c_char,
std::mem::replace(&mut outlk_ad.config_server, String::new()); ) == 0) as libc::c_int;
let ssl_off = (!outlk_ad.config[4].is_null()
&& strcasecmp(
outlk_ad.config[4],
b"off\x00" as *const u8 as *const libc::c_char,
) == 0) as libc::c_int;
if strcasecmp(
outlk_ad.config[1],
b"imap\x00" as *const u8 as *const libc::c_char,
) == 0
&& outlk_ad.out_imap_set == 0
{
outlk_ad.out.mail_server = to_string(outlk_ad.config[2]);
outlk_ad.out.mail_port = port; outlk_ad.out.mail_port = port;
if ssl_on { if 0 != ssl_on {
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32 outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
} else if ssl_off { } else if 0 != ssl_off {
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32 outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
} }
outlk_ad.out_imap_set = true outlk_ad.out_imap_set = 1
} else if type_ == "smtp" && !outlk_ad.out_smtp_set { } else if strcasecmp(
outlk_ad.out.send_server = outlk_ad.config[1usize],
std::mem::replace(&mut outlk_ad.config_server, String::new()); b"smtp\x00" as *const u8 as *const libc::c_char,
outlk_ad.out.send_port = outlk_ad.config_port; ) == 0
if ssl_on { && outlk_ad.out_smtp_set == 0
{
outlk_ad.out.send_server = to_string(outlk_ad.config[2]);
outlk_ad.out.send_port = port;
if 0 != ssl_on {
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32 outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
} else if ssl_off { } else if 0 != ssl_off {
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32 outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
} }
outlk_ad.out_smtp_set = true outlk_ad.out_smtp_set = 1
} }
} }
outlk_clean_config(outlk_ad);
} }
outlk_ad.tag_config = 0;
} }
#[cfg(test)] fn outlk_autodiscover_starttag_cb(event: &BytesStart, outlk_ad: &mut outlk_autodiscover_t) {
mod tests { let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
use super::*;
#[test] if tag == "protocol" {
fn test_parse_redirect() { unsafe { outlk_clean_config(outlk_ad) };
let res = parse_xml(" } else if tag == "type" {
<?xml version=\"1.0\" encoding=\"utf-8\"?> outlk_ad.tag_config = 1
<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006\"> } else if tag == "server" {
<Response xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a\"> outlk_ad.tag_config = 2
<Account> } else if tag == "port" {
<AccountType>email</AccountType> outlk_ad.tag_config = 3
<Action>redirectUrl</Action> } else if tag == "ssl" {
<RedirectUrl>https://mail.example.com/autodiscover/autodiscover.xml</RedirectUrl> outlk_ad.tag_config = 4
</Account> } else if tag == "redirecturl" {
</Response> outlk_ad.tag_config = 5
</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");
}
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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))
}
}
}

View File

@@ -1,4 +1,4 @@
//! # Constants //! Constants
#![allow(non_camel_case_types, dead_code)] #![allow(non_camel_case_types, dead_code)]
use deltachat_derive::*; use deltachat_derive::*;
@@ -8,8 +8,24 @@ lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string(); pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
} }
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
pub enum MoveState {
Undefined = 0,
Pending = 1,
Stay = 2,
Moving = 3,
}
impl Default for MoveState {
fn default() -> Self {
MoveState::Undefined
}
}
// some defaults // some defaults
const DC_E2EE_DEFAULT_ENABLED: i32 = 1; const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
pub const DC_MDNS_DEFAULT_ENABLED: i32 = 1;
const DC_INBOX_WATCH_DEFAULT: i32 = 1; const DC_INBOX_WATCH_DEFAULT: i32 = 1;
const DC_SENTBOX_WATCH_DEFAULT: i32 = 1; const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_WATCH_DEFAULT: i32 = 1; const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
@@ -29,20 +45,6 @@ impl Default for Blocked {
} }
} }
#[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_IMAP_SEEN: u32 = 0x1;
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01; pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
@@ -53,17 +55,19 @@ 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; 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;
// unchanged user avatars are resent to the recipients every some days /// param1 is a directory where the keys are written to
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14; const DC_IMEX_EXPORT_SELF_KEYS: usize = 1;
/// param1 is a directory where the keys are searched in and read from
// values for DC_PARAM_FORCE_PLAINTEXT const DC_IMEX_IMPORT_SELF_KEYS: usize = 2;
pub(crate) const DC_FP_NO_AUTOCRYPT_HEADER: i32 = 2; /// param1 is a directory where the backup is written to
pub(crate) const DC_FP_ADD_AUTOCRYPT_HEADER: i32 = 1; const DC_IMEX_EXPORT_BACKUP: usize = 11;
/// param1 is the file with the backup to import
const DC_IMEX_IMPORT_BACKUP: usize = 12;
/// 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(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
@@ -108,7 +112,7 @@ impl Default for Chattype {
} }
pub const DC_MSG_ID_MARKER1: u32 = 1; pub const DC_MSG_ID_MARKER1: u32 = 1;
pub const DC_MSG_ID_DAYMARKER: u32 = 9; const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9; pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// approx. max. length returned by dc_msg_get_text() /// approx. max. length returned by dc_msg_get_text()
@@ -118,18 +122,10 @@ const DC_MAX_GET_INFO_LEN: usize = 100000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0; pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1; pub const DC_CONTACT_ID_SELF: u32 = 1;
pub const DC_CONTACT_ID_INFO: u32 = 2; const DC_CONTACT_ID_DEVICE: u32 = 2;
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9; pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
// decorative address that is used for DC_CONTACT_ID_DEVICE pub const DC_CREATE_MVBOX: usize = 1;
// when an api that returns an email is called.
pub const DC_CONTACT_ID_DEVICE_ADDR: &str = "device@localhost";
// Flags for empty server job
pub const DC_EMPTY_MVBOX: u32 = 0x01;
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
@@ -139,23 +135,23 @@ pub const DC_EMPTY_INBOX: u32 = 0x02;
/// 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 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.
@@ -170,9 +166,9 @@ 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 =
@@ -184,9 +180,6 @@ pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
pub const DC_BOB_ERROR: i32 = 0; pub const DC_BOB_ERROR: i32 = 0;
pub const DC_BOB_SUCCESS: i32 = 1; pub const DC_BOB_SUCCESS: i32 = 1;
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)] #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(i32)] #[repr(i32)]
pub enum Viewtype { pub enum Viewtype {
@@ -207,11 +200,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().
@@ -264,8 +252,13 @@ const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated; const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
const DC_STR_SELFNOTINGRP: usize = 21; // deprecated; const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
/// Values for dc_get|set_config("show_emails")
const DC_SHOW_EMAILS_OFF: usize = 0;
const DC_SHOW_EMAILS_ACCEPTED_CONTACTS: usize = 1;
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; const DC_STR_NOMESSAGES: usize = 1;
const DC_STR_SELF: usize = 2; const DC_STR_SELF: usize = 2;
@@ -311,8 +304,7 @@ const DC_STR_MSGACTIONBYME: usize = 63;
const DC_STR_MSGLOCATIONENABLED: usize = 64; const DC_STR_MSGLOCATIONENABLED: usize = 64;
const DC_STR_MSGLOCATIONDISABLED: usize = 65; const DC_STR_MSGLOCATIONDISABLED: usize = 65;
const DC_STR_LOCATION: usize = 66; const DC_STR_LOCATION: usize = 66;
const DC_STR_STICKER: usize = 67; 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;

View File

@@ -1,5 +1,3 @@
//! Contacts module
use std::path::PathBuf; use std::path::PathBuf;
use deltachat_derive::*; use deltachat_derive::*;
@@ -12,13 +10,11 @@ use crate::constants::*;
use crate::context::Context; use crate::context::Context;
use crate::dc_tools::*; use crate::dc_tools::*;
use crate::e2ee; use crate::e2ee;
use crate::error::{Error, Result}; use crate::error::Result;
use crate::events::Event; use crate::events::Event;
use crate::key::*; use crate::key::*;
use crate::login_param::LoginParam; use crate::login_param::LoginParam;
use crate::message::{MessageState, MsgId}; use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::*;
use crate::peerstate::*; use crate::peerstate::*;
use crate::sql; use crate::sql;
use crate::stock::StockMessage; use crate::stock::StockMessage;
@@ -27,17 +23,15 @@ use crate::stock::StockMessage;
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.
#[derive(Debug)] #[derive(Debug)]
pub struct Contact { pub struct Contact {
/// The contact ID. /// The contact ID.
@@ -53,8 +47,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,
@@ -62,8 +56,6 @@ pub struct Contact {
blocked: bool, blocked: bool,
/// The origin/source of the contact. /// The origin/source of the contact.
pub origin: Origin, pub origin: Origin,
/// Parameters as Param::ProfileImage
pub param: Params,
} }
/// Possible origins of a contact. /// Possible origins of a contact.
@@ -114,6 +106,11 @@ impl Default for Origin {
} }
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
@@ -144,11 +141,24 @@ pub enum VerifiedStatus {
} }
impl Contact { impl Contact {
pub fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> { pub fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
let mut res = context.sql.query_row( if contact_id == DC_CONTACT_ID_SELF {
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param let contact = Contact {
FROM contacts c id: contact_id,
WHERE c.id=?;", name: context.stock_str(StockMessage::SelfMsg).into(),
authname: "".into(),
addr: context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default(),
blocked: false,
origin: Origin::Unknown,
};
return Ok(contact);
}
context.sql.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname FROM contacts c WHERE c.id=?;",
params![contact_id as i32], params![contact_id as i32],
|row| { |row| {
let contact = Self { let contact = Self {
@@ -158,21 +168,10 @@ impl Contact {
addr: row.get::<_, String>(1)?, addr: row.get::<_, String>(1)?,
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0, blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
origin: row.get(2)?, origin: row.get(2)?,
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
}; };
Ok(contact) Ok(contact)
}, }
)?; )
if contact_id == DC_CONTACT_ID_SELF {
res.name = context.stock_str(StockMessage::SelfMsg).to_string();
res.addr = context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
} else if contact_id == DC_CONTACT_ID_DEVICE {
res.name = context.stock_str(StockMessage::DeviceMessages).to_string();
res.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
}
Ok(res)
} }
/// Returns `true` if this contact is blocked. /// Returns `true` if this contact is blocked.
@@ -200,7 +199,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.
@@ -230,7 +229,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) {
@@ -244,7 +243,7 @@ impl Contact {
{ {
context.call_cb(Event::MsgsChanged { context.call_cb(Event::MsgsChanged {
chat_id: 0, chat_id: 0,
msg_id: MsgId::new(0), msg_id: 0,
}); });
} }
} }
@@ -264,8 +263,8 @@ 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_get_value(
@@ -301,8 +300,8 @@ 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) {
@@ -391,18 +390,20 @@ impl Contact {
} }
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, "Cannot add contact.");
}
} }
Ok((row_id, sth_modified)) Ok((row_id, sth_modified))
@@ -420,7 +421,7 @@ 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 `addr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
/// ///
@@ -577,12 +578,7 @@ impl Contact {
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 {
@@ -602,25 +598,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,
); );
@@ -683,7 +679,7 @@ impl Contact {
} }
Err(err) => { Err(err) => {
error!(context, "delete_contact {} failed ({})", contact_id, err); error!(context, "delete_contact {} failed ({})", contact_id, err);
return Err(err.into()); return Err(err);
} }
} }
} }
@@ -701,17 +697,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)
}
pub fn update_param(&mut self, context: &Context) -> Result<()> {
sql::execute(
context,
&context.sql,
"UPDATE contacts SET param=? WHERE id=?",
params![self.param.to_string(), self.id as i32],
)?;
Ok(())
} }
/// Get the ID of the contact. /// Get the ID of the contact.
@@ -724,7 +710,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
} }
@@ -747,9 +732,6 @@ impl Contact {
if !self.name.is_empty() { if !self.name.is_empty() {
return &self.name; return &self.name;
} }
if !self.authname.is_empty() {
return &self.authname;
}
&self.addr &self.addr
} }
@@ -785,11 +767,8 @@ impl Contact {
if let Some(p) = context.get_config(Config::Selfavatar) { if let Some(p) = context.get_config(Config::Selfavatar) {
return Some(PathBuf::from(p)); return Some(PathBuf::from(p));
} }
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Some(dc_get_abs_path(context, image_rel));
}
} }
// TODO: else get image_abs from contact param
None None
} }
@@ -825,14 +804,14 @@ impl Contact {
} }
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(context, &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;
} }
} }
@@ -848,7 +827,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;
} }
} }
@@ -872,14 +851,14 @@ impl Contact {
.unwrap_or_default() as usize .unwrap_or_default() as usize
} }
pub fn get_origin_by_id(context: &Context, contact_id: u32, ret_blocked: &mut bool) -> Origin { pub fn get_origin_by_id(context: &Context, contact_id: u32, ret_blocked: &mut i32) -> Origin {
let mut ret = Origin::Unknown; let mut ret = Origin::Unknown;
*ret_blocked = false; *ret_blocked = 0;
if let Ok(contact) = Contact::load_from_db(context, contact_id) { if let Ok(contact) = Contact::load_from_db(context, contact_id) {
/* we could optimize this by loading only the needed fields */ /* we could optimize this by loading only the needed fields */
if contact.blocked { if contact.blocked {
*ret_blocked = true; *ret_blocked = 1;
} else { } else {
ret = contact.origin; ret = contact.origin;
} }
@@ -889,7 +868,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;
} }
@@ -913,8 +892,7 @@ impl Contact {
} }
} }
/// Extracts first name from full name. 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()
} }
@@ -924,7 +902,6 @@ pub fn may_be_valid_addr(addr: &str) -> bool {
res.is_ok() res.is_ok()
} }
/// 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();
@@ -936,26 +913,26 @@ 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=?);",
@@ -964,36 +941,11 @@ fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
Contact::mark_noticed(context, contact_id); Contact::mark_noticed(context, contact_id);
context.call_cb(Event::ContactsChanged(None)); context.call_cb(Event::ContactsChanged(None));
} }
}
} }
} }
} }
pub fn set_profile_image(
context: &Context,
contact_id: u32,
profile_image: &AvatarAction,
) -> Result<()> {
// the given profile image is expected to be already in the blob directory
// as profile images can be set only by receiving messages, this should be always the case, however.
let mut contact = Contact::load_from_db(context, contact_id)?;
let changed = match profile_image {
AvatarAction::Change(profile_image) => {
contact.param.set(Param::ProfileImage, profile_image);
true
}
AvatarAction::Delete => {
contact.param.remove(Param::ProfileImage);
true
}
AvatarAction::None => false,
};
if changed {
contact.update_param(context)?;
context.call_cb(Event::ContactsChanged(Some(contact_id)));
}
Ok(())
}
/// Normalize a name. /// Normalize a name.
/// ///
/// - Remove quotes (come from some bad MUA implementations) /// - Remove quotes (come from some bad MUA implementations)
@@ -1011,9 +963,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];
} }
@@ -1058,25 +1010,23 @@ fn cat_fingerprint(
} }
} }
impl Context {
/// determine whether the specified addr maps to the/a self addr
pub fn is_self_addr(&self, addr: &str) -> Result<bool> {
let self_addr = match self.get_config(Config::ConfiguredAddr) {
Some(s) => s,
None => return Err(Error::NotConfigured),
};
Ok(addr_cmp(self_addr, addr))
}
}
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 {
if !addr.as_ref().is_empty() {
let normalized_addr = addr_normalize(addr.as_ref());
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr) {
return normalized_addr == self_addr;
}
}
false
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> { fn split_address_book(book: &str) -> Vec<(&str, &str)> {
book.lines() book.lines()
.chunks(2) .chunks(2)
@@ -1096,8 +1046,6 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
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);
@@ -1123,10 +1071,6 @@ 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]
@@ -1143,128 +1087,4 @@ mod tests {
vec![("Name one", "Address one"), ("Name two", "Address two")] 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_is_self_addr() -> Result<()> {
let t = test_context(None);
assert!(t.ctx.is_self_addr("me@me.org").is_err());
let addr = configure_alice_keypair(&t.ctx);
assert_eq!(t.ctx.is_self_addr("me@me.org")?, false);
assert_eq!(t.ctx.is_self_addr(&addr)?, true);
Ok(())
}
#[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"));
}
} }

View File

@@ -1,5 +1,3 @@
//! Context module
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -8,7 +6,6 @@ use std::sync::{Arc, Condvar, Mutex, RwLock};
use libc::uintptr_t; use libc::uintptr_t;
use crate::chat::*; use crate::chat::*;
use crate::config::Config;
use crate::constants::*; use crate::constants::*;
use crate::contact::*; use crate::contact::*;
use crate::error::*; use crate::error::*;
@@ -19,9 +16,9 @@ use crate::job_thread::JobThread;
use crate::key::*; use crate::key::*;
use crate::login_param::LoginParam; use crate::login_param::LoginParam;
use crate::lot::Lot; use crate::lot::Lot;
use crate::message::{self, Message, MsgId}; use crate::message::*;
use crate::param::Params; use crate::param::Params;
use crate::smtp::Smtp; use crate::smtp::*;
use crate::sql::Sql; use crate::sql::Sql;
/// Callback function type for [Context] /// Callback function type for [Context]
@@ -41,14 +38,12 @@ pub type ContextCallback = dyn Fn(&Context, Event) -> uintptr_t + Send + Sync;
#[derive(DebugStub)] #[derive(DebugStub)]
pub struct Context { pub struct Context {
/// Database file path
dbfile: PathBuf, dbfile: PathBuf,
/// Blob directory path
blobdir: PathBuf, blobdir: PathBuf,
pub sql: Sql, pub sql: Sql,
pub inbox: Arc<RwLock<Imap>>,
pub perform_inbox_jobs_needed: Arc<RwLock<bool>>, pub perform_inbox_jobs_needed: Arc<RwLock<bool>>,
pub probe_imap_network: Arc<RwLock<bool>>, pub probe_imap_network: Arc<RwLock<bool>>,
pub inbox_thread: Arc<RwLock<JobThread>>,
pub sentbox_thread: Arc<RwLock<JobThread>>, pub sentbox_thread: Arc<RwLock<JobThread>>,
pub mvbox_thread: Arc<RwLock<JobThread>>, pub mvbox_thread: Arc<RwLock<JobThread>>,
pub smtp: Arc<Mutex<Smtp>>, pub smtp: Arc<Mutex<Smtp>>,
@@ -59,45 +54,19 @@ pub struct Context {
pub os_name: Option<String>, pub os_name: Option<String>,
pub cmdline_sel_chat_id: Arc<RwLock<u32>>, pub cmdline_sel_chat_id: Arc<RwLock<u32>>,
pub bob: Arc<RwLock<BobStatus>>, pub bob: Arc<RwLock<BobStatus>>,
pub last_smeared_timestamp: RwLock<i64>, pub last_smeared_timestamp: Arc<RwLock<i64>>,
pub running_state: Arc<RwLock<RunningState>>, pub running_state: Arc<RwLock<RunningState>>,
/// Mutex to avoid generating the key for the user more than once. /// Mutex to avoid generating the key for the user more than once.
pub generating_key_mutex: Mutex<()>, pub generating_key_mutex: Mutex<()>,
pub translated_stockstrings: RwLock<HashMap<usize, String>>,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct RunningState { pub struct RunningState {
pub ongoing_running: bool, pub ongoing_running: bool,
shall_stop_ongoing: bool, pub shall_stop_ongoing: bool,
}
/// Return some info about deltachat-core
///
/// This contains information mostly about the library itself, the
/// actual keys and their values which will be present are not
/// guaranteed. Calling [Context::get_info] also includes information
/// about the context on top of the information here.
pub fn get_info() -> HashMap<&'static str, String> {
let mut res = HashMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert(
"sqlite_thread_safe",
unsafe { rusqlite::ffi::sqlite3_threadsafe() }.to_string(),
);
res.insert(
"arch",
(std::mem::size_of::<*mut libc::c_void>())
.wrapping_mul(8)
.to_string(),
);
res.insert("level", "awesome".into());
res
} }
impl Context { impl Context {
/// Creates new context.
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> { pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
let mut blob_fname = OsString::new(); let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default()); blob_fname.push(dbfile.file_name().unwrap_or_default());
@@ -123,6 +92,7 @@ impl Context {
let ctx = Context { let ctx = Context {
blobdir, blobdir,
dbfile, dbfile,
inbox: Arc::new(RwLock::new(Imap::new())),
cb, cb,
os_name: Some(os_name), os_name: Some(os_name),
running_state: Arc::new(RwLock::new(Default::default())), running_state: Arc::new(RwLock::new(Default::default())),
@@ -131,13 +101,8 @@ impl Context {
smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())), smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
oauth2_critical: Arc::new(Mutex::new(())), oauth2_critical: Arc::new(Mutex::new(())),
bob: Arc::new(RwLock::new(Default::default())), bob: Arc::new(RwLock::new(Default::default())),
last_smeared_timestamp: RwLock::new(0), last_smeared_timestamp: Arc::new(RwLock::new(0)),
cmdline_sel_chat_id: Arc::new(RwLock::new(0)), cmdline_sel_chat_id: Arc::new(RwLock::new(0)),
inbox_thread: Arc::new(RwLock::new(JobThread::new(
"INBOX",
"configured_inbox_folder",
Imap::new(),
))),
sentbox_thread: Arc::new(RwLock::new(JobThread::new( sentbox_thread: Arc::new(RwLock::new(JobThread::new(
"SENTBOX", "SENTBOX",
"configured_sentbox_folder", "configured_sentbox_folder",
@@ -151,23 +116,20 @@ impl Context {
probe_imap_network: Arc::new(RwLock::new(false)), probe_imap_network: Arc::new(RwLock::new(false)),
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)), perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()), generating_key_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
}; };
ensure!( ensure!(
ctx.sql.open(&ctx, &ctx.dbfile, false), ctx.sql.open(&ctx, &ctx.dbfile, 0),
"Failed opening sqlite database" "Failed opening sqlite database"
); );
Ok(ctx) Ok(ctx)
} }
/// Returns database file path.
pub fn get_dbfile(&self) -> &Path { pub fn get_dbfile(&self) -> &Path {
self.dbfile.as_path() self.dbfile.as_path()
} }
/// Returns blob directory path.
pub fn get_blobdir(&self) -> &Path { pub fn get_blobdir(&self) -> &Path {
self.blobdir.as_path() self.blobdir.as_path()
} }
@@ -176,84 +138,31 @@ impl Context {
(*self.cb)(self, event) (*self.cb)(self, event)
} }
/*******************************************************************************
* Ongoing process allocation/free/check
******************************************************************************/
pub fn alloc_ongoing(&self) -> bool {
if self.has_ongoing() {
warn!(self, "There is already another ongoing process running.",);
false
} else {
let s_a = self.running_state.clone();
let mut s = s_a.write().unwrap();
s.ongoing_running = true;
s.shall_stop_ongoing = false;
true
}
}
pub fn free_ongoing(&self) {
let s_a = self.running_state.clone();
let mut s = s_a.write().unwrap();
s.ongoing_running = false;
s.shall_stop_ongoing = true;
}
pub fn has_ongoing(&self) -> bool {
let s_a = self.running_state.clone();
let s = s_a.read().unwrap();
s.ongoing_running || !s.shall_stop_ongoing
}
/// Signal an ongoing process to stop.
pub fn stop_ongoing(&self) {
let s_a = self.running_state.clone();
let mut s = s_a.write().unwrap();
if s.ongoing_running && !s.shall_stop_ongoing {
info!(self, "Signaling the ongoing process to stop ASAP.",);
s.shall_stop_ongoing = true;
} else {
info!(self, "No ongoing process to stop.",);
};
}
pub fn shall_stop_ongoing(&self) -> bool {
self.running_state
.clone()
.read()
.unwrap()
.shall_stop_ongoing
}
/*******************************************************************************
* UI chat/message related API
******************************************************************************/
pub fn get_info(&self) -> HashMap<&'static str, String> { pub fn get_info(&self) -> HashMap<&'static str, String> {
let unset = "0"; let unset = "0";
let l = LoginParam::from_database(self, ""); let l = LoginParam::from_database(self, "");
let l2 = LoginParam::from_database(self, "configured_"); let l2 = LoginParam::from_database(self, "configured_");
let displayname = self.get_config(Config::Displayname); let displayname = self.sql.get_config(self, "displayname");
let chats = get_chat_cnt(self) as usize; let chats = get_chat_cnt(self) as usize;
let real_msgs = message::get_real_msg_cnt(self) as usize; let real_msgs = dc_get_real_msg_cnt(self) as usize;
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self) as usize; let deaddrop_msgs = dc_get_deaddrop_msg_cnt(self) as usize;
let contacts = Contact::get_real_cnt(self) as usize; let contacts = Contact::get_real_cnt(self) as usize;
let is_configured = self.get_config_int(Config::Configured); let is_configured = self
.sql
.get_config_int(self, "configured")
.unwrap_or_default();
let dbversion = self let dbversion = self
.sql .sql
.get_raw_config_int(self, "dbversion") .get_config_int(self, "dbversion")
.unwrap_or_default(); .unwrap_or_default();
let e2ee_enabled = self
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled); .sql
let mdns_enabled = self.get_config_int(Config::MdnsEnabled); .get_config_int(self, "e2ee_enabled")
let bcc_self = self.get_config_int(Config::BccSelf); .unwrap_or_else(|| 1);
let mdns_enabled = self
.sql
.get_config_int(self, "mdns_enabled")
.unwrap_or_else(|| 1);
let prv_key_cnt: Option<isize> = let prv_key_cnt: Option<isize> =
self.sql self.sql
@@ -271,25 +180,48 @@ impl Context {
"<Not yet calculated>".into() "<Not yet calculated>".into()
}; };
let inbox_watch = self.get_config_int(Config::InboxWatch); let inbox_watch = self
let sentbox_watch = self.get_config_int(Config::SentboxWatch); .sql
let mvbox_watch = self.get_config_int(Config::MvboxWatch); .get_config_int(self, "inbox_watch")
let mvbox_move = self.get_config_int(Config::MvboxMove); .unwrap_or_else(|| 1);
let sentbox_watch = self
.sql
.get_config_int(self, "sentbox_watch")
.unwrap_or_else(|| 1);
let mvbox_watch = self
.sql
.get_config_int(self, "mvbox_watch")
.unwrap_or_else(|| 1);
let mvbox_move = self
.sql
.get_config_int(self, "mvbox_move")
.unwrap_or_else(|| 1);
let folders_configured = self let folders_configured = self
.sql .sql
.get_raw_config_int(self, "folders_configured") .get_config_int(self, "folders_configured")
.unwrap_or_default(); .unwrap_or_default();
let configured_sentbox_folder = self let configured_sentbox_folder = self
.sql .sql
.get_raw_config(self, "configured_sentbox_folder") .get_config(self, "configured_sentbox_folder")
.unwrap_or_else(|| "<unset>".to_string()); .unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self let configured_mvbox_folder = self
.sql .sql
.get_raw_config(self, "configured_mvbox_folder") .get_config(self, "configured_mvbox_folder")
.unwrap_or_else(|| "<unset>".to_string()); .unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info(); let mut res = HashMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert(
"sqlite_thread_safe",
unsafe { rusqlite::ffi::sqlite3_threadsafe() }.to_string(),
);
res.insert(
"arch",
(::std::mem::size_of::<*mut libc::c_void>())
.wrapping_mul(8)
.to_string(),
);
res.insert("number_of_chats", chats.to_string()); res.insert("number_of_chats", chats.to_string());
res.insert("number_of_chat_messages", real_msgs.to_string()); res.insert("number_of_chat_messages", real_msgs.to_string());
res.insert("messages_in_contact_requests", deaddrop_msgs.to_string()); res.insert("messages_in_contact_requests", deaddrop_msgs.to_string());
@@ -298,11 +230,6 @@ impl Context {
res.insert("database_version", dbversion.to_string()); res.insert("database_version", dbversion.to_string());
res.insert("blobdir", self.get_blobdir().display().to_string()); res.insert("blobdir", self.get_blobdir().display().to_string());
res.insert("display_name", displayname.unwrap_or_else(|| unset.into())); res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
res.insert(
"selfavatar",
self.get_config(Config::Selfavatar)
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string()); res.insert("is_configured", is_configured.to_string());
res.insert("entered_account_settings", l.to_string()); res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string()); res.insert("used_account_settings", l2.to_string());
@@ -315,7 +242,6 @@ impl Context {
res.insert("configured_mvbox_folder", configured_mvbox_folder); res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("mdns_enabled", mdns_enabled.to_string()); res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("e2ee_enabled", e2ee_enabled.to_string()); res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert("bcc_self", bcc_self.to_string());
res.insert( res.insert(
"private_key_count", "private_key_count",
prv_key_cnt.unwrap_or_default().to_string(), prv_key_cnt.unwrap_or_default().to_string(),
@@ -325,43 +251,38 @@ impl Context {
pub_key_cnt.unwrap_or_default().to_string(), pub_key_cnt.unwrap_or_default().to_string(),
); );
res.insert("fingerprint", fingerprint_str); res.insert("fingerprint", fingerprint_str);
res.insert("level", "awesome".into());
res res
} }
pub fn get_fresh_msgs(&self) -> Vec<MsgId> { pub fn get_fresh_msgs(&self) -> Vec<u32> {
let show_deaddrop = 0; let show_deaddrop = 0;
self.sql self.sql
.query_map( .query_map(
concat!( "SELECT m.id FROM msgs m LEFT JOIN contacts ct \
"SELECT m.id", ON m.from_id=ct.id LEFT JOIN chats c ON m.chat_id=c.id WHERE m.state=? \
" FROM msgs m", AND m.hidden=0 \
" LEFT JOIN contacts ct", AND m.chat_id>? \
" ON m.from_id=ct.id", AND ct.blocked=0 \
" LEFT JOIN chats c", AND (c.blocked=0 OR c.blocked=?) ORDER BY m.timestamp DESC,m.id DESC;",
" ON m.chat_id=c.id",
" WHERE m.state=?",
" AND m.hidden=0",
" AND m.chat_id>?",
" AND ct.blocked=0",
" AND (c.blocked=0 OR c.blocked=?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
),
&[10, 9, if 0 != show_deaddrop { 2 } else { 0 }], &[10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|row| row.get::<_, MsgId>(0), |row| row.get(0),
|rows| { |rows| {
let mut ret = Vec::new(); let mut ret = Vec::new();
for row in rows { for row in rows {
ret.push(row?); let id: u32 = row?;
ret.push(id);
} }
Ok(ret) Ok(ret)
}, },
) )
.unwrap_or_default() .unwrap()
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn search_msgs(&self, chat_id: u32, query: impl AsRef<str>) -> Vec<MsgId> { pub fn search_msgs(&self, chat_id: u32, query: impl AsRef<str>) -> Vec<u32> {
let real_query = query.as_ref().trim(); let real_query = query.as_ref().trim();
if real_query.is_empty() { if real_query.is_empty() {
return Vec::new(); return Vec::new();
@@ -370,43 +291,25 @@ impl Context {
let strLikeBeg = format!("{}%", real_query); let strLikeBeg = format!("{}%", real_query);
let query = if 0 != chat_id { let query = if 0 != chat_id {
concat!( "SELECT m.id, m.timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id WHERE m.chat_id=? \
"SELECT m.id AS id, m.timestamp AS timestamp", AND m.hidden=0 \
" FROM msgs m", AND ct.blocked=0 AND (txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp,m.id;"
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" WHERE m.chat_id=?",
" AND m.hidden=0",
" AND ct.blocked=0",
" AND (txt LIKE ? OR ct.name LIKE ?)",
" ORDER BY m.timestamp,m.id;"
)
} else { } else {
concat!( "SELECT m.id, m.timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id \
"SELECT m.id AS id, m.timestamp AS timestamp", LEFT JOIN chats c ON m.chat_id=c.id WHERE m.chat_id>9 AND m.hidden=0 \
" FROM msgs m", AND (c.blocked=0 OR c.blocked=?) \
" LEFT JOIN contacts ct", AND ct.blocked=0 AND (m.txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp DESC,m.id DESC;"
" ON m.from_id=ct.id",
" LEFT JOIN chats c",
" ON m.chat_id=c.id",
" WHERE m.chat_id>9",
" AND m.hidden=0",
" AND (c.blocked=0 OR c.blocked=?)",
" AND ct.blocked=0",
" AND (m.txt LIKE ? OR ct.name LIKE ?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
)
}; };
self.sql self.sql
.query_map( .query_map(
query, query,
params![chat_id as i32, &strLikeInText, &strLikeBeg], params![chat_id as i32, &strLikeInText, &strLikeBeg],
|row| row.get::<_, MsgId>("id"), |row| row.get::<_, i32>(0),
|rows| { |rows| {
let mut ret = Vec::new(); let mut ret = Vec::new();
for id in rows { for id in rows {
ret.push(id?); ret.push(id? as u32);
} }
Ok(ret) Ok(ret)
}, },
@@ -419,7 +322,7 @@ impl Context {
} }
pub fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool { pub fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
let sentbox_name = self.sql.get_raw_config(self, "configured_sentbox_folder"); let sentbox_name = self.sql.get_config(self, "configured_sentbox_folder");
if let Some(name) = sentbox_name { if let Some(name) = sentbox_name {
name == folder_name.as_ref() name == folder_name.as_ref()
} else { } else {
@@ -428,7 +331,7 @@ impl Context {
} }
pub fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool { pub fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
let mvbox_name = self.sql.get_raw_config(self, "configured_mvbox_folder"); let mvbox_name = self.sql.get_config(self, "configured_mvbox_folder");
if let Some(name) = mvbox_name { if let Some(name) = mvbox_name {
name == folder_name.as_ref() name == folder_name.as_ref()
@@ -437,30 +340,41 @@ impl Context {
} }
} }
pub fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) { pub fn do_heuristics_moves(&self, folder: &str, msg_id: u32) {
if !self.get_config_bool(Config::MvboxMove) { if self
.sql
.get_config_int(self, "mvbox_move")
.unwrap_or_else(|| 1)
== 0
{
return; return;
} }
if self.is_mvbox(folder) { if !self.is_inbox(folder) && !self.is_sentbox(folder) {
return; return;
} }
if let Ok(msg) = Message::load_from_db(self, msg_id) {
if msg.is_setupmessage() { if let Ok(msg) = dc_msg_new_load(self, msg_id) {
if dc_msg_is_setupmessage(&msg) {
// do not move setup messages; // do not move setup messages;
// there may be a non-delta device that wants to handle it // there may be a non-delta device that wants to handle it
return; return;
} }
if self.is_mvbox(folder) {
dc_update_msg_move_state(self, msg.rfc724_mid, MoveState::Stay);
}
// 1 = dc message, 2 = reply to dc message // 1 = dc message, 2 = reply to dc message
if 0 != msg.is_dc_message { if 0 != msg.is_dc_message {
job_add( job_add(
self, self,
Action::MoveMsg, Action::MoveMsg,
msg.id.to_u32() as i32, msg.id as libc::c_int,
Params::new(), Params::new(),
0, 0,
); );
dc_update_msg_move_state(self, msg.rfc724_mid, MoveState::Moving);
} }
} }
} }
@@ -468,8 +382,8 @@ impl Context {
impl Drop for Context { impl Drop for Context {
fn drop(&mut self) { fn drop(&mut self) {
info!(self, "disconnecting inbox-thread",); info!(self, "disconnecting INBOX-watch",);
self.inbox_thread.read().unwrap().imap.disconnect(self); self.inbox.read().unwrap().disconnect(self);
info!(self, "disconnecting sentbox-thread",); info!(self, "disconnecting sentbox-thread",);
self.sentbox_thread.read().unwrap().imap.disconnect(self); self.sentbox_thread.read().unwrap().imap.disconnect(self);
info!(self, "disconnecting mvbox-thread",); info!(self, "disconnecting mvbox-thread",);
@@ -496,25 +410,12 @@ pub struct BobStatus {
pub qr_scan: Option<Lot>, pub qr_scan: Option<Lot>,
} }
#[derive(Debug, PartialEq)]
pub enum PerformJobsNeeded {
Not,
AtOnce,
AvoidDos,
}
impl Default for PerformJobsNeeded {
fn default() -> Self {
Self::Not
}
}
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct SmtpState { pub struct SmtpState {
pub idle: bool, pub idle: bool,
pub suspended: bool, pub suspended: bool,
pub doing_jobs: bool, pub doing_jobs: bool,
pub perform_jobs_needed: PerformJobsNeeded, pub perform_jobs_needed: i32,
pub probe_network: bool, pub probe_network: bool,
} }
@@ -574,15 +475,6 @@ mod tests {
assert!(dbfile2.is_file()); assert!(dbfile2.is_file());
} }
#[test]
fn test_with_empty_blobdir() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}
#[test] #[test]
fn test_with_blobdir_not_exists() { fn test_with_blobdir_not_exists() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@@ -603,14 +495,6 @@ mod tests {
let t = dummy_context(); let t = dummy_context();
let info = t.ctx.get_info(); let info = t.ctx.get_info();
assert!(info.get("database_dir").is_some());
}
#[test]
fn test_get_info_no_context() {
let info = get_info();
assert!(info.get("deltachat_core_version").is_some());
assert!(info.get("database_dir").is_none());
assert_eq!(info.get("level").unwrap(), "awesome"); assert_eq!(info.get("level").unwrap(), "awesome");
} }
} }

View File

@@ -1,7 +1,3 @@
//! De-HTML
//!
//! A module to remove HTML tags from the email text
use lazy_static::lazy_static; use lazy_static::lazy_static;
use quick_xml; use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText}; use quick_xml::events::{BytesEnd, BytesStart, BytesText};
@@ -23,18 +19,22 @@ enum AddText {
YesPreserveLineEnds, YesPreserveLineEnds,
} }
// dehtml() returns way too many newlines; however, an optimisation on this issue is not needed as // dc_dehtml() returns way too many lineends; however, an optimisation on this issue is not needed as
// the newlines are typically removed in further processing by the caller // the lineends are typically remove in further processing by the caller
pub fn dehtml(buf: &str) -> String { pub fn dc_dehtml(buf_terminated: &str) -> String {
let buf = buf.trim(); let buf_terminated = buf_terminated.trim();
if buf_terminated.is_empty() {
return "".into();
}
let mut dehtml = Dehtml { let mut dehtml = Dehtml {
strbuilder: String::with_capacity(buf.len()), strbuilder: String::with_capacity(buf_terminated.len()),
add_text: AddText::YesRemoveLineEnds, add_text: AddText::YesRemoveLineEnds,
last_href: None, last_href: None,
}; };
let mut reader = quick_xml::Reader::from_str(buf); let mut reader = quick_xml::Reader::from_str(buf_terminated);
let mut buf = Vec::new(); let mut buf = Vec::new();
@@ -171,7 +171,7 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_dehtml() { fn test_dc_dehtml() {
let cases = vec![ let cases = vec![
( (
"<a href='https://example.com'> Foo </a>", "<a href='https://example.com'> Foo </a>",
@@ -186,7 +186,7 @@ mod tests {
("", ""), ("", ""),
]; ];
for (input, output) in cases { for (input, output) in cases {
assert_eq!(dehtml(input), output); assert_eq!(dc_dehtml(input), output);
} }
} }
} }

1089
src/dc_imex.rs Normal file

File diff suppressed because it is too large Load Diff

1294
src/dc_mimefactory.rs Normal file

File diff suppressed because it is too large Load Diff

1665
src/dc_mimeparser.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,169 +1,179 @@
use crate::dehtml::*; use crate::dc_dehtml::*;
#[derive(Copy, Clone)]
pub struct Simplify {
pub is_forwarded: bool,
}
/// Return index of footer line in vector of message lines, or vector length if
/// no footer is found.
///
/// Also return whether not-standard (rfc3676, §4.3) footer is found.
fn find_message_footer(lines: &[&str]) -> (usize, bool) {
for ix in 0..lines.len() {
let line = lines[ix];
/// Remove standard (RFC 3676, §4.3) footer if it is found.
fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
for (ix, &line) in lines.iter().enumerate() {
// quoted-printable may encode `-- ` to `-- =20` which is converted // quoted-printable may encode `-- ` to `-- =20` which is converted
// back to `-- ` // back to `-- `
match line { match line.as_ref() {
"-- " | "-- " => return &lines[..ix], "-- " | "-- " => return (ix, false),
"--" | "---" | "----" => return (ix, true),
_ => (), _ => (),
} }
} }
lines return (lines.len(), false);
} }
/// Remove nonstandard footer and a boolean indicating whether such impl Simplify {
/// footer was removed. pub fn new() -> Self {
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) { Simplify {
for (ix, &line) in lines.iter().enumerate() { is_forwarded: false,
if line == "--"
|| line == "---"
|| line == "----"
|| line.starts_with("-----")
|| line.starts_with("_____")
|| line.starts_with("=====")
|| line.starts_with("*****")
|| line.starts_with("~~~~~")
{
return (&lines[..ix], true);
} }
} }
(lines, false)
}
fn split_lines(buf: &str) -> Vec<&str> { /// Simplify and normalise text: Remove quotes, signatures, unnecessary
buf.split('\n').collect() /// lineends etc.
} /// The data returned from simplify() must be free()'d when no longer used.
pub fn simplify(&mut self, input: &str, is_html: bool, is_msgrmsg: bool) -> String {
let mut out = if is_html {
dc_dehtml(input)
} else {
input.to_string()
};
/// Simplify message text for chat display. out.retain(|c| c != '\r');
/// Remove quotes, signatures, trailing empty lines etc. out = self.simplify_plain_text(&out, is_msgrmsg);
pub fn simplify(input: &str, is_html: bool, is_chat_message: bool) -> (String, bool) { out.retain(|c| c != '\r');
let mut out = if is_html {
dehtml(input)
} else {
input.to_string()
};
out.retain(|c| c != '\r'); out
let lines = split_lines(&out);
let (lines, is_forwarded) = skip_forward_header(&lines);
let lines = remove_message_footer(lines);
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
let (lines, has_bottom_quote) = if !is_chat_message {
remove_bottom_quote(lines)
} else {
(lines, false)
};
let (lines, has_top_quote) = if !is_chat_message {
remove_top_quote(lines)
} else {
(lines, false)
};
// re-create buffer from the remaining lines
let text = render_message(
lines,
has_top_quote,
has_nonstandard_footer || has_bottom_quote,
);
(text, is_forwarded)
}
/// Skips "forwarded message" header.
/// Returns message body lines and a boolean indicating whether
/// a message is forwarded or not.
fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
if lines.len() >= 3
&& lines[0] == "---------- Forwarded message ----------"
&& lines[1].starts_with("From: ")
&& lines[2].is_empty()
{
(&lines[3..], true)
} else {
(lines, false)
} }
}
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) { /**
let mut last_quoted_line = None; * Simplify Plain Text
for (l, line) in lines.iter().enumerate().rev() { */
if is_plain_quote(line) { #[allow(non_snake_case)]
last_quoted_line = Some(l) fn simplify_plain_text(&mut self, buf_terminated: &str, is_msgrmsg: bool) -> String {
} else if !is_empty_line(line) { /* This function ...
break; ... removes all text after the line `-- ` (footer mark)
} ... removes full quotes at the beginning and at the end of the text -
} these are all lines starting with the character `>`
if let Some(mut l_last) = last_quoted_line { ... remove a non-empty line before the removed quote (contains sth. like "On 2.9.2016, Bjoern wrote:" in different formats and lanugages) */
if l_last > 1 && is_empty_line(lines[l_last - 1]) { /* split the given buffer into lines */
l_last -= 1 let lines: Vec<_> = buf_terminated.split('\n').collect();
} let mut l_first: usize = 0;
if l_last > 1 { let mut is_cut_at_begin = false;
let line = lines[l_last - 1]; let (mut l_last, mut is_cut_at_end) = find_message_footer(&lines);
if is_quoted_headline(line) {
l_last -= 1 if l_last > l_first + 2 {
let line0 = lines[l_first];
let line1 = lines[l_first + 1];
let line2 = lines[l_first + 2];
if line0 == "---------- Forwarded message ----------"
&& line1.starts_with("From: ")
&& line2.is_empty()
{
self.is_forwarded = true;
l_first += 3
} }
} }
(&lines[..l_last], true) for l in l_first..l_last {
} else { let line = lines[l];
(lines, false) if line == "-----"
} || line == "_____"
} || line == "====="
|| line == "*****"
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) { || line == "~~~~~"
let mut last_quoted_line = None; {
let mut has_quoted_headline = false; l_last = l;
for (l, line) in lines.iter().enumerate() { is_cut_at_end = true;
if is_plain_quote(line) { /* done */
last_quoted_line = Some(l)
} else if !is_empty_line(line) {
if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() {
has_quoted_headline = true
} else {
/* non-quoting line found */
break; break;
} }
} }
} if !is_msgrmsg {
if let Some(last_quoted_line) = last_quoted_line { let mut l_lastQuotedLine = None;
(&lines[last_quoted_line + 1..], true) for l in (l_first..l_last).rev() {
} else { let line = lines[l];
(lines, false) if is_plain_quote(line) {
} l_lastQuotedLine = Some(l)
} } else if !is_empty_line(line) {
break;
fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) -> String { }
let mut ret = String::new(); }
if is_cut_at_begin { if let Some(last_quoted_line) = l_lastQuotedLine {
ret += "[...]"; l_last = last_quoted_line;
} is_cut_at_end = true;
/* we write empty lines only in case and non-empty line follows */ if l_last > 1 {
let mut pending_linebreaks = 0; if is_empty_line(lines[l_last - 1]) {
let mut empty_body = true; l_last -= 1
for line in lines { }
if is_empty_line(line) { }
pending_linebreaks += 1 if l_last > 1 {
} else { let line = lines[l_last - 1];
if !empty_body { if is_quoted_headline(line) {
if pending_linebreaks > 2 { l_last -= 1
pending_linebreaks = 2 }
}
while 0 != pending_linebreaks {
ret += "\n";
pending_linebreaks -= 1
} }
} }
// the incoming message might contain invalid UTF8
ret += line;
empty_body = false;
pending_linebreaks = 1
} }
if !is_msgrmsg {
let mut l_lastQuotedLine_0 = None;
let mut hasQuotedHeadline = 0;
for l in l_first..l_last {
let line = lines[l];
if is_plain_quote(line) {
l_lastQuotedLine_0 = Some(l)
} else if !is_empty_line(line) {
if is_quoted_headline(line)
&& 0 == hasQuotedHeadline
&& l_lastQuotedLine_0.is_none()
{
hasQuotedHeadline = 1i32
} else {
/* non-quoting line found */
break;
}
}
}
if let Some(last_quoted_line) = l_lastQuotedLine_0 {
l_first = last_quoted_line + 1;
is_cut_at_begin = true
}
}
/* re-create buffer from the remaining lines */
let mut ret = String::new();
if is_cut_at_begin {
ret += "[...]";
}
/* we write empty lines only in case and non-empty line follows */
let mut pending_linebreaks: libc::c_int = 0i32;
let mut content_lines_added: libc::c_int = 0i32;
for l in l_first..l_last {
let line = lines[l];
if is_empty_line(line) {
pending_linebreaks += 1
} else {
if 0 != content_lines_added {
if pending_linebreaks > 2i32 {
pending_linebreaks = 2i32
}
while 0 != pending_linebreaks {
ret += "\n";
pending_linebreaks -= 1
}
}
// the incoming message might contain invalid UTF8
ret += line;
content_lines_added += 1;
pending_linebreaks = 1i32
}
}
if is_cut_at_end && (!is_cut_at_begin || 0 != content_lines_added) {
ret += " [...]";
}
ret
} }
if is_cut_at_end && (!is_cut_at_begin || !empty_body) {
ret += " [...]";
}
ret
} }
/** /**
@@ -195,7 +205,7 @@ fn is_quoted_headline(buf: &str) -> bool {
} }
fn is_plain_quote(buf: &str) -> bool { fn is_plain_quote(buf: &str) -> bool {
buf.starts_with('>') buf.starts_with(">")
} }
#[cfg(test)] #[cfg(test)]
@@ -207,59 +217,50 @@ mod tests {
#[test] #[test]
// proptest does not support [[:graphical:][:space:]] regex. // proptest does not support [[:graphical:][:space:]] regex.
fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") { fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") {
let (output, _is_forwarded) = simplify(&input, false, true); let output = Simplify::new().simplify_plain_text(&input, true);
assert!(output.split('\n').all(|s| s != "-- ")); assert!(output.split('\n').all(|s| s != "-- "));
} }
} }
#[test] #[test]
fn test_simplify_trim() { fn test_simplify_trim() {
let mut simplify = Simplify::new();
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r"; let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
let (plain, is_forwarded) = simplify(html, true, false); let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "line1\nline2"); assert_eq!(plain, "line1\nline2");
assert!(!is_forwarded);
} }
#[test] #[test]
fn test_simplify_parse_href() { fn test_simplify_parse_href() {
let mut simplify = Simplify::new();
let html = "<a href=url>text</a"; let html = "<a href=url>text</a";
let (plain, is_forwarded) = simplify(html, true, false); let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "[text](url)"); assert_eq!(plain, "[text](url)");
assert!(!is_forwarded);
} }
#[test] #[test]
fn test_simplify_bold_text() { fn test_simplify_bold_text() {
let mut simplify = Simplify::new();
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>"; let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let (plain, is_forwarded) = simplify(html, true, false); let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "text *bold*<>"); assert_eq!(plain, "text *bold*<>");
assert!(!is_forwarded);
}
#[test]
fn test_simplify_forwarded_message() {
let text = "---------- Forwarded message ----------\r\nFrom: test@example.com\r\n\r\nForwarded message\r\n-- \r\nSignature goes here";
let (plain, is_forwarded) = simplify(text, false, false);
assert_eq!(plain, "Forwarded message");
assert!(is_forwarded);
} }
#[test] #[test]
fn test_simplify_html_encoded() { fn test_simplify_html_encoded() {
let mut simplify = Simplify::new();
let html = let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;"; "&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let (plain, is_forwarded) = simplify(html, true, false); let plain = simplify.simplify(html, true, false);
assert_eq!( assert_eq!(
plain, plain,
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}" "<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
); );
assert!(!is_forwarded);
} }
#[test] #[test]
@@ -273,19 +274,4 @@ mod tests {
assert!(!is_plain_quote("Life is pain")); assert!(!is_plain_quote("Life is pain"));
assert!(!is_plain_quote("")); assert!(!is_plain_quote(""));
} }
#[test]
fn test_remove_top_quote() {
let (lines, has_top_quote) = remove_top_quote(&["> first", "> second"]);
assert!(lines.is_empty());
assert!(has_top_quote);
let (lines, has_top_quote) = remove_top_quote(&["> first", "> second", "not a quote"]);
assert_eq!(lines, &["not a quote"]);
assert!(has_top_quote);
let (lines, has_top_quote) = remove_top_quote(&["not a quote", "> first", "> second"]);
assert_eq!(lines, &["not a quote", "> first", "> second"]);
assert!(!has_top_quote);
}
} }

469
src/dc_strencode.rs Normal file
View File

@@ -0,0 +1,469 @@
use std::borrow::Cow;
use std::ffi::CString;
use std::ptr;
use charset::Charset;
use mmime::mailmime_decode::*;
use mmime::mmapstring::*;
use mmime::other::*;
use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, CONTROLS};
use crate::dc_tools::*;
use crate::x::*;
/**
* Encode non-ascii-strings as `=?UTF-8?Q?Bj=c3=b6rn_Petersen?=`.
* Belongs to RFC 2047: https://tools.ietf.org/html/rfc2047
*
* We do not fold at position 72; this would result in empty words as `=?utf-8?Q??=` which are correct,
* but cannot be displayed by some mail programs (eg. Android Stock Mail).
* however, this is not needed, as long as _one_ word is not longer than 72 characters.
* _if_ it is, the display may get weird. This affects the subject only.
* the best solution wor all this would be if libetpan encodes the line as only libetpan knowns when a header line is full.
*
* @param to_encode Null-terminated UTF-8-string to encode.
* @return Returns the encoded string which must be free()'d when no longed needed.
* On errors, NULL is returned.
*/
pub unsafe fn dc_encode_header_words(to_encode_r: impl AsRef<str>) -> *mut libc::c_char {
let to_encode =
CString::new(to_encode_r.as_ref().as_bytes()).expect("invalid cstring to_encode");
let mut ok_to_continue = true;
let mut ret_str: *mut libc::c_char = ptr::null_mut();
let mut cur: *const libc::c_char = to_encode.as_ptr();
let mmapstr: *mut MMAPString = mmap_string_new(b"\x00" as *const u8 as *const libc::c_char);
if mmapstr.is_null() {
ok_to_continue = false;
}
loop {
if !ok_to_continue {
if !mmapstr.is_null() {
mmap_string_free(mmapstr);
}
break;
} else {
if *cur as libc::c_int != '\u{0}' as i32 {
let begin: *const libc::c_char;
let mut end: *const libc::c_char;
let mut do_quote: bool;
let mut quote_words: libc::c_int;
begin = cur;
end = begin;
quote_words = 0i32;
do_quote = true;
while *cur as libc::c_int != '\u{0}' as i32 {
get_word(cur, &mut cur, &mut do_quote);
if !do_quote {
break;
}
quote_words = 1i32;
end = cur;
if *cur as libc::c_int != '\u{0}' as i32 {
cur = cur.offset(1isize)
}
}
if 0 != quote_words {
if !quote_word(
b"utf-8\x00" as *const u8 as *const libc::c_char,
mmapstr,
begin,
end.wrapping_offset_from(begin) as libc::size_t,
) {
ok_to_continue = false;
continue;
}
if *end as libc::c_int == ' ' as i32 || *end as libc::c_int == '\t' as i32 {
if mmap_string_append_c(mmapstr, *end).is_null() {
ok_to_continue = false;
continue;
}
end = end.offset(1isize)
}
if *end as libc::c_int != '\u{0}' as i32 {
if mmap_string_append_len(
mmapstr,
end,
cur.wrapping_offset_from(end) as libc::size_t,
)
.is_null()
{
ok_to_continue = false;
continue;
}
}
} else if mmap_string_append_len(
mmapstr,
begin,
cur.wrapping_offset_from(begin) as libc::size_t,
)
.is_null()
{
ok_to_continue = false;
continue;
}
if !(*cur as libc::c_int == ' ' as i32 || *cur as libc::c_int == '\t' as i32) {
continue;
}
if mmap_string_append_c(mmapstr, *cur).is_null() {
ok_to_continue = false;
continue;
}
cur = cur.offset(1isize);
} else {
ret_str = strdup((*mmapstr).str_0);
ok_to_continue = false;
}
}
}
ret_str
}
unsafe fn quote_word(
display_charset: *const libc::c_char,
mmapstr: *mut MMAPString,
word: *const libc::c_char,
size: libc::size_t,
) -> bool {
let mut cur: *const libc::c_char;
let mut i = 0;
let mut hex: [libc::c_char; 4] = [0; 4];
// let mut col: libc::c_int = 0i32;
if mmap_string_append(mmapstr, b"=?\x00" as *const u8 as *const libc::c_char).is_null() {
return false;
}
if mmap_string_append(mmapstr, display_charset).is_null() {
return false;
}
if mmap_string_append(mmapstr, b"?Q?\x00" as *const u8 as *const libc::c_char).is_null() {
return false;
}
// col = (*mmapstr).len as libc::c_int;
cur = word;
while i < size {
let mut do_quote_char = false;
match *cur as u8 as char {
',' | ':' | '!' | '"' | '#' | '$' | '@' | '[' | '\\' | ']' | '^' | '`' | '{' | '|'
| '}' | '~' | '=' | '?' | '_' => do_quote_char = true,
_ => {
if *cur as u8 >= 128 {
do_quote_char = true;
}
}
}
if do_quote_char {
print_hex(hex.as_mut_ptr(), cur);
if mmap_string_append(mmapstr, hex.as_mut_ptr()).is_null() {
return false;
}
// col += 3i32
} else {
if *cur as libc::c_int == ' ' as i32 {
if mmap_string_append_c(mmapstr, '_' as i32 as libc::c_char).is_null() {
return false;
}
} else if mmap_string_append_c(mmapstr, *cur).is_null() {
return false;
}
// col += 3i32
}
cur = cur.offset(1isize);
i = i.wrapping_add(1)
}
if mmap_string_append(mmapstr, b"?=\x00" as *const u8 as *const libc::c_char).is_null() {
return false;
}
true
}
unsafe fn get_word(
begin: *const libc::c_char,
pend: *mut *const libc::c_char,
pto_be_quoted: *mut bool,
) {
let mut cur: *const libc::c_char = begin;
while *cur as libc::c_int != ' ' as i32
&& *cur as libc::c_int != '\t' as i32
&& *cur as libc::c_int != '\u{0}' as i32
{
cur = cur.offset(1isize)
}
*pto_be_quoted = to_be_quoted(begin, cur.wrapping_offset_from(begin) as libc::size_t);
*pend = cur;
}
/* ******************************************************************************
* Encode/decode header words, RFC 2047
******************************************************************************/
/* see comment below */
unsafe fn to_be_quoted(word: *const libc::c_char, size: libc::size_t) -> bool {
let mut cur: *const libc::c_char = word;
let mut i = 0;
while i < size {
match *cur as libc::c_int {
44 | 58 | 33 | 34 | 35 | 36 | 64 | 91 | 92 | 93 | 94 | 96 | 123 | 124 | 125 | 126
| 61 | 63 | 95 => return true,
_ => {
if *cur as libc::c_uchar as libc::c_int >= 128i32 {
return true;
}
}
}
cur = cur.offset(1isize);
i = i.wrapping_add(1)
}
false
}
pub unsafe fn dc_decode_header_words(in_0: *const libc::c_char) -> *mut libc::c_char {
if in_0.is_null() {
return ptr::null_mut();
}
let mut out: *mut libc::c_char = ptr::null_mut();
let mut cur_token = 0;
let r: libc::c_int = mailmime_encoded_phrase_parse(
b"iso-8859-1\x00" as *const u8 as *const libc::c_char,
in_0,
strlen(in_0),
&mut cur_token,
b"utf-8\x00" as *const u8 as *const libc::c_char,
&mut out,
);
if r != MAILIMF_NO_ERROR as libc::c_int || out.is_null() {
out = dc_strdup(in_0)
}
out
}
pub fn dc_decode_header_words_safe(input: &str) -> String {
static FROM_ENCODING: &[u8] = b"iso-8859-1\x00";
static TO_ENCODING: &[u8] = b"utf-8\x00";
let mut out = ptr::null_mut();
let mut cur_token = 0;
let input_c = CString::yolo(input);
unsafe {
let r = mailmime_encoded_phrase_parse(
FROM_ENCODING.as_ptr().cast(),
input_c.as_ptr(),
input.len(),
&mut cur_token,
TO_ENCODING.as_ptr().cast(),
&mut out,
);
if r as u32 != MAILIMF_NO_ERROR || out.is_null() {
input.to_string()
} else {
let res = to_string(out);
free(out.cast());
res
}
}
}
pub fn dc_needs_ext_header(to_check: impl AsRef<str>) -> bool {
let to_check = to_check.as_ref();
if to_check.is_empty() {
return false;
}
to_check.chars().any(|c| {
!(c.is_ascii_alphanumeric()
|| c == '-'
|| c == '_'
|| c == '_'
|| c == '.'
|| c == '~'
|| c == '%')
})
}
const EXT_ASCII_ST: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'-')
.add(b'_')
.add(b'.')
.add(b'~')
.add(b'%');
/// Encode an UTF-8 string to the extended header format.
pub fn dc_encode_ext_header(to_encode: impl AsRef<str>) -> String {
let encoded = utf8_percent_encode(to_encode.as_ref(), &EXT_ASCII_ST);
format!("utf-8''{}", encoded)
}
/// Decode an extended-header-format strings to UTF-8.
pub fn dc_decode_ext_header(to_decode: &[u8]) -> Cow<str> {
if let Some(index) = bytes!(b'\'').find(to_decode) {
let (charset, rest) = to_decode.split_at(index);
if !charset.is_empty() {
// skip language
if let Some(index2) = bytes!(b'\'').find(&rest[1..]) {
let decoded = percent_decode(&rest[index2 + 2..]);
if charset != b"utf-8" && charset != b"UTF-8" {
if let Some(encoding) = Charset::for_label(charset) {
let bytes = decoded.collect::<Vec<u8>>();
let (res, _, _) = encoding.decode(&bytes);
return Cow::Owned(res.into_owned());
} else {
return decoded.decode_utf8_lossy();
}
} else {
return decoded.decode_utf8_lossy();
}
}
}
}
String::from_utf8_lossy(to_decode)
}
unsafe fn print_hex(target: *mut libc::c_char, cur: *const libc::c_char) {
assert!(!target.is_null());
assert!(!cur.is_null());
let bytes = std::slice::from_raw_parts(cur as *const _, strlen(cur));
let raw = CString::yolo(format!("={}", &hex::encode_upper(bytes)[..2]));
libc::memcpy(target as *mut _, raw.as_ptr() as *const _, 4);
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CStr;
#[test]
fn test_dc_decode_header_words() {
unsafe {
let mut buf1: *mut libc::c_char = dc_decode_header_words(
b"=?utf-8?B?dGVzdMOkw7bDvC50eHQ=?=\x00" as *const u8 as *const libc::c_char,
);
assert_eq!(
strcmp(
buf1,
b"test\xc3\xa4\xc3\xb6\xc3\xbc.txt\x00" as *const u8 as *const libc::c_char
),
0
);
free(buf1 as *mut libc::c_void);
buf1 =
dc_decode_header_words(b"just ascii test\x00" as *const u8 as *const libc::c_char);
assert_eq!(CStr::from_ptr(buf1).to_str().unwrap(), "just ascii test");
free(buf1 as *mut libc::c_void);
buf1 = dc_encode_header_words("abcdef");
assert_eq!(CStr::from_ptr(buf1).to_str().unwrap(), "abcdef");
free(buf1 as *mut libc::c_void);
buf1 = dc_encode_header_words(
std::string::String::from_utf8(b"test\xc3\xa4\xc3\xb6\xc3\xbc.txt".to_vec())
.unwrap(),
);
assert_eq!(
strncmp(buf1, b"=?utf-8\x00" as *const u8 as *const libc::c_char, 7),
0
);
let buf2: *mut libc::c_char = dc_decode_header_words(buf1);
assert_eq!(
strcmp(
buf2,
b"test\xc3\xa4\xc3\xb6\xc3\xbc.txt\x00" as *const u8 as *const libc::c_char
),
0
);
free(buf1 as *mut libc::c_void);
free(buf2 as *mut libc::c_void);
buf1 = dc_decode_header_words(
b"=?ISO-8859-1?Q?attachment=3B=0D=0A_filename=3D?= =?ISO-8859-1?Q?=22test=E4=F6=FC=2Etxt=22=3B=0D=0A_size=3D39?=\x00" as *const u8 as *const libc::c_char
);
assert_eq!(
strcmp(
buf1,
b"attachment;\r\n filename=\"test\xc3\xa4\xc3\xb6\xc3\xbc.txt\";\r\n size=39\x00" as *const u8 as *const libc::c_char,
),
0
);
free(buf1 as *mut libc::c_void);
}
}
#[test]
fn test_dc_encode_ext_header() {
let buf1 = dc_encode_ext_header("Björn Petersen");
assert_eq!(&buf1, "utf-8\'\'Bj%C3%B6rn%20Petersen");
let buf2 = dc_decode_ext_header(buf1.as_bytes());
assert_eq!(&buf2, "Björn Petersen",);
let buf1 = dc_decode_ext_header(b"iso-8859-1\'en\'%A3%20rates");
assert_eq!(buf1, "£ rates",);
let buf1 = dc_decode_ext_header(b"wrong\'format");
assert_eq!(buf1, "wrong\'format",);
let buf1 = dc_decode_ext_header(b"\'\'");
assert_eq!(buf1, "\'\'");
let buf1 = dc_decode_ext_header(b"x\'\'");
assert_eq!(buf1, "");
let buf1 = dc_decode_ext_header(b"\'");
assert_eq!(buf1, "\'");
let buf1 = dc_decode_ext_header(b"");
assert_eq!(buf1, "");
// regressions
assert_eq!(
dc_decode_ext_header(dc_encode_ext_header("%0A").as_bytes()),
"%0A"
);
}
#[test]
fn test_dc_needs_ext_header() {
assert_eq!(dc_needs_ext_header("Björn"), true);
assert_eq!(dc_needs_ext_header("Bjoern"), false);
assert_eq!(dc_needs_ext_header(""), false);
assert_eq!(dc_needs_ext_header(" "), true);
assert_eq!(dc_needs_ext_header("a b"), true);
}
#[test]
fn test_print_hex() {
let mut hex: [libc::c_char; 4] = [0; 4];
let cur = b"helloworld" as *const u8 as *const libc::c_char;
unsafe { print_hex(hex.as_mut_ptr(), cur) };
assert_eq!(to_string(hex.as_ptr() as *const _), "=68");
let cur = b":" as *const u8 as *const libc::c_char;
unsafe { print_hex(hex.as_mut_ptr(), cur) };
assert_eq!(to_string(hex.as_ptr() as *const _), "=3A");
}
use proptest::prelude::*;
proptest! {
#[test]
fn test_ext_header_roundtrip(buf: String) {
let encoded = dc_encode_ext_header(&buf);
let decoded = dc_decode_ext_header(encoded.as_bytes());
assert_eq!(buf, decoded);
}
#[test]
fn test_ext_header_decode_anything(buf: Vec<u8>) {
// make sure this never panics
let _decoded = dc_decode_ext_header(&buf);
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,19 @@
//! # Error handling use failure::Fail;
use lettre_email::mime;
#[derive(Debug, Fail)] #[derive(Debug, Fail)]
pub enum Error { pub enum Error {
#[fail(display = "Sqlite Error: {:?}", _0)]
Sql(rusqlite::Error),
#[fail(display = "Sqlite Connection Pool Error: {:?}", _0)]
ConnectionPool(r2d2::Error),
#[fail(display = "{:?}", _0)] #[fail(display = "{:?}", _0)]
Failure(failure::Error), Failure(failure::Error),
#[fail(display = "SQL error: {:?}", _0)] #[fail(display = "Sqlite: Connection closed")]
SqlError(#[cause] crate::sql::Error), SqlNoConnection,
#[fail(display = "Sqlite: Already open")]
SqlAlreadyOpen,
#[fail(display = "Sqlite: Failed to open")]
SqlFailedToOpen,
#[fail(display = "{:?}", _0)] #[fail(display = "{:?}", _0)]
Io(std::io::Error), Io(std::io::Error),
#[fail(display = "{:?}", _0)] #[fail(display = "{:?}", _0)]
@@ -16,39 +22,15 @@ pub enum Error {
Image(image_meta::ImageError), Image(image_meta::ImageError),
#[fail(display = "{:?}", _0)] #[fail(display = "{:?}", _0)]
Utf8(std::str::Utf8Error), Utf8(std::str::Utf8Error),
#[fail(display = "PGP: {:?}", _0)]
Pgp(pgp::errors::Error),
#[fail(display = "Base64Decode: {:?}", _0)]
Base64Decode(base64::DecodeError),
#[fail(display = "{:?}", _0)] #[fail(display = "{:?}", _0)]
FromUtf8(std::string::FromUtf8Error), CStringError(crate::dc_tools::CStringError),
#[fail(display = "{}", _0)]
BlobError(#[cause] crate::blob::BlobError),
#[fail(display = "Invalid Message ID.")]
InvalidMsgId,
#[fail(display = "Watch folder not found {:?}", _0)]
WatchFolderNotFound(String),
#[fail(display = "Invalid Email: {:?}", _0)]
MailParseError(#[cause] mailparse::MailParseError),
#[fail(display = "Building invalid Email: {:?}", _0)]
LettreError(#[cause] lettre_email::error::Error),
#[fail(display = "FromStr error: {:?}", _0)]
FromStr(#[cause] mime::FromStrError),
#[fail(display = "Not Configured")]
NotConfigured,
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
impl From<crate::sql::Error> for Error { impl From<rusqlite::Error> for Error {
fn from(err: crate::sql::Error) -> Error { fn from(err: rusqlite::Error) -> Error {
Error::SqlError(err) Error::Sql(err)
}
}
impl From<base64::DecodeError> for Error {
fn from(err: base64::DecodeError) -> Error {
Error::Base64Decode(err)
} }
} }
@@ -58,6 +40,12 @@ impl From<failure::Error> for Error {
} }
} }
impl From<r2d2::Error> for Error {
fn from(err: r2d2::Error) -> Error {
Error::ConnectionPool(err)
}
}
impl From<std::io::Error> for Error { impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error { fn from(err: std::io::Error) -> Error {
Error::Io(err) Error::Io(err)
@@ -76,45 +64,9 @@ impl From<image_meta::ImageError> for Error {
} }
} }
impl From<pgp::errors::Error> for Error { impl From<crate::dc_tools::CStringError> for Error {
fn from(err: pgp::errors::Error) -> Error { fn from(err: crate::dc_tools::CStringError) -> Error {
Error::Pgp(err) Error::CStringError(err)
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(err: std::string::FromUtf8Error) -> Error {
Error::FromUtf8(err)
}
}
impl From<crate::blob::BlobError> for Error {
fn from(err: crate::blob::BlobError) -> Error {
Error::BlobError(err)
}
}
impl From<crate::message::InvalidMsgId> for Error {
fn from(_err: crate::message::InvalidMsgId) -> Error {
Error::InvalidMsgId
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)
}
}
impl From<lettre_email::error::Error> for Error {
fn from(err: lettre_email::error::Error) -> Error {
Error::LettreError(err)
}
}
impl From<mime::FromStrError> for Error {
fn from(err: mime::FromStrError) -> Error {
Error::FromStr(err)
} }
} }

View File

@@ -1,10 +1,8 @@
//! # Events specification
use std::path::PathBuf; use std::path::PathBuf;
use strum::EnumProperty; use strum::EnumProperty;
use crate::message::MsgId; use crate::stock::StockMessage;
impl Event { impl Event {
/// Returns the corresponding Event id. /// Returns the corresponding Event id.
@@ -44,36 +42,6 @@ pub enum Event {
#[strum(props(id = "103"))] #[strum(props(id = "103"))]
SmtpMessageSent(String), SmtpMessageSent(String),
/// Emitted when an IMAP message has been marked as deleted
///
/// @return 0
#[strum(props(id = "104"))]
ImapMessageDeleted(String),
/// Emitted when an IMAP message has been moved
///
/// @return 0
#[strum(props(id = "105"))]
ImapMessageMoved(String),
/// Emitted when an IMAP folder was emptied
///
/// @return 0
#[strum(props(id = "106"))]
ImapFolderEmptied(String),
/// Emitted when an new file in the $BLOBDIR was created
///
/// @return 0
#[strum(props(id = "150"))]
NewBlobFile(String),
/// Emitted when an new file in the $BLOBDIR was created
///
/// @return 0
#[strum(props(id = "151"))]
DeletedBlobFile(String),
/// The library-user should write a warning string to the log. /// The library-user should write a warning string to the log.
/// Passed to the callback given to dc_context_new(). /// Passed to the callback given to dc_context_new().
/// ///
@@ -92,7 +60,7 @@ pub enum Event {
/// However, for ongoing processes (eg. configure()) /// However, for ongoing processes (eg. configure())
/// or for functions that are expected to fail (eg. dc_continue_key_transfer()) /// 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 /// 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 /// failed (returned false). It should be sufficient to report only the _last_ error
/// in a messasge box then. /// in a messasge box then.
/// ///
/// @return /// @return
@@ -135,7 +103,7 @@ pub enum Event {
/// ///
/// @return 0 /// @return 0
#[strum(props(id = "2000"))] #[strum(props(id = "2000"))]
MsgsChanged { chat_id: u32, msg_id: MsgId }, MsgsChanged { chat_id: u32, msg_id: u32 },
/// There is a fresh message. Typically, the user will show an notification /// There is a fresh message. Typically, the user will show an notification
/// when receiving this message. /// when receiving this message.
@@ -144,28 +112,28 @@ pub enum Event {
/// ///
/// @return 0 /// @return 0
#[strum(props(id = "2005"))] #[strum(props(id = "2005"))]
IncomingMsg { chat_id: u32, msg_id: MsgId }, IncomingMsg { chat_id: u32, msg_id: u32 },
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to /// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state(). /// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
/// ///
/// @return 0 /// @return 0
#[strum(props(id = "2010"))] #[strum(props(id = "2010"))]
MsgDelivered { chat_id: u32, msg_id: MsgId }, MsgDelivered { chat_id: u32, msg_id: u32 },
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to /// 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(). /// DC_STATE_OUT_FAILED, see dc_msg_get_state().
/// ///
/// @return 0 /// @return 0
#[strum(props(id = "2012"))] #[strum(props(id = "2012"))]
MsgFailed { chat_id: u32, msg_id: MsgId }, MsgFailed { chat_id: u32, msg_id: u32 },
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to /// 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(). /// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
/// ///
/// @return 0 /// @return 0
#[strum(props(id = "2015"))] #[strum(props(id = "2015"))]
MsgRead { chat_id: u32, msg_id: MsgId }, MsgRead { chat_id: u32, msg_id: u32 },
/// Chat changed. The name or the image of a chat group was changed or members were added or removed. /// 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. /// Or the verify state of a chat has changed.
@@ -199,7 +167,7 @@ pub enum Event {
#[strum(props(id = "2041"))] #[strum(props(id = "2041"))]
ConfigureProgress(usize), ConfigureProgress(usize),
/// Inform about the import/export progress started by imex(). /// Inform about the import/export progress started by dc_imex().
/// ///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done /// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0 /// @param data2 0
@@ -207,8 +175,8 @@ pub enum Event {
#[strum(props(id = "2051"))] #[strum(props(id = "2051"))]
ImexProgress(usize), ImexProgress(usize),
/// A file has been exported. A file has been written by imex(). /// 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 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 /// A typical purpose for a handler of this event may be to make the file public to some system
/// services. /// services.
@@ -246,10 +214,16 @@ pub enum Event {
#[strum(props(id = "2061"))] #[strum(props(id = "2061"))]
SecurejoinJoinerProgress { contact_id: u32, progress: usize }, SecurejoinJoinerProgress { contact_id: u32, progress: usize },
/// This event is sent out to the inviter when a joiner successfully joined a group. // the following events are functions that should be provided by the frontends
/// @param data1 (int) chat_id /// Requeste a localized string from the frontend.
/// @param data2 (int) contact_id /// @param data1 (int) ID of the string to request, one of the DC_STR_/// constants.
/// @return 0 /// @param data2 (int) The count. If the requested string contains a placeholder for a numeric value,
#[strum(props(id = "2062"))] /// the ui may use this value to return different strings on different plural forms.
SecurejoinMemberAdded { chat_id: u32, contact_id: u32 }, /// @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.
#[strum(props(id = "2091"))]
GetString { id: StockMessage, count: usize },
} }

View File

@@ -1,58 +0,0 @@
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames)]
#[strum(serialize_all = "kebab_case")]
#[allow(dead_code)]
pub enum HeaderDef {
MessageId,
Subject,
Date,
From_,
To,
Cc,
Disposition,
OriginalMessageId,
ListId,
References,
InReplyTo,
Precedence,
ChatVersion,
ChatGroupId,
ChatGroupName,
ChatGroupNameChanged,
ChatVerified,
ChatGroupImage, // deprecated
ChatGroupAvatar,
ChatUserAvatar,
ChatVoiceMessage,
ChatGroupMemberRemoved,
ChatGroupMemberAdded,
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
AutocryptSetupMessage,
SecureJoin,
SecureJoinGroup,
SecureJoinFingerprint,
SecureJoinInvitenumber,
SecureJoinAuth,
_TestHeader,
}
impl HeaderDef {
/// Returns the corresponding Event id.
pub fn get_headername(&self) -> String {
self.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
/// Test that kebab_case serialization works as expected
fn kebab_test() {
assert_eq!(HeaderDef::From_.to_string(), "from");
assert_eq!(HeaderDef::_TestHeader.to_string(), "test-header");
}
}

1697
src/imap.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,273 +0,0 @@
use super::Imap;
use async_imap::extensions::idle::IdleResponse;
use async_std::prelude::*;
use async_std::task;
use std::sync::atomic::Ordering;
use std::time::{Duration, SystemTime};
use crate::context::Context;
use crate::imap_client::*;
use super::select_folder;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
IdleProtocolFailed(#[cause] async_imap::error::Error),
#[fail(display = "IMAP IDLE protocol timed out")]
IdleTimeout(#[cause] async_std::future::TimeoutError),
#[fail(display = "IMAP server does not have IDLE capability")]
IdleAbilityMissing,
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[fail(display = "IMAP error")]
ImapError(#[cause] async_imap::error::Error),
#[fail(display = "Setup handle error")]
SetupHandleError(#[cause] super::Error),
}
impl From<select_folder::Error> for Error {
fn from(err: select_folder::Error) -> Error {
Error::SelectFolderError(err)
}
}
impl Imap {
pub fn can_idle(&self) -> bool {
task::block_on(async move { self.config.read().await.can_idle })
}
pub fn idle(&self, context: &Context, watch_folder: Option<String>) -> Result<()> {
task::block_on(async move {
if !self.can_idle() {
return Err(Error::IdleAbilityMissing);
}
self.setup_handle_if_needed(context)
.await
.map_err(Error::SetupHandleError)?;
self.select_folder(context, watch_folder.clone()).await?;
let session = self.session.lock().await.take();
let timeout = Duration::from_secs(23 * 60);
if let Some(session) = session {
match session.idle() {
// BEWARE: If you change the Secure branch you
// typically also need to change the Insecure branch.
IdleHandle::Secure(mut handle) => {
if let Err(err) = handle.init().await {
return Err(Error::IdleProtocolFailed(err));
}
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
*self.interrupt.lock().await = Some(interrupt);
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
std::mem::drop(idle_wait);
info!(context, "Idle wait was skipped");
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
IdleResponse::NewData(_) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
IdleResponse::Timeout => {
info!(context, "Idle-wait timeout or interruption");
}
IdleResponse::ManualInterrupt => {
info!(context, "Idle wait was interrupted");
}
}
}
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
*self.session.lock().await = Some(Session::Secure(session));
}
Err(err) => {
// if we cannot terminate IDLE it probably
// means that we waited long (with idle_wait)
// but the network went away/changed
self.trigger_reconnect();
return Err(Error::IdleProtocolFailed(err));
}
}
}
IdleHandle::Insecure(mut handle) => {
if let Err(err) = handle.init().await {
return Err(Error::IdleProtocolFailed(err));
}
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
*self.interrupt.lock().await = Some(interrupt);
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
std::mem::drop(idle_wait);
info!(context, "Idle wait was skipped");
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
IdleResponse::NewData(_) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
IdleResponse::Timeout => {
info!(context, "Idle-wait timeout or interruption");
}
IdleResponse::ManualInterrupt => {
info!(context, "Idle wait was interrupted");
}
}
}
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
*self.session.lock().await = Some(Session::Insecure(session));
}
Err(err) => {
// if we cannot terminate IDLE it probably
// means that we waited long (with idle_wait)
// but the network went away/changed
self.trigger_reconnect();
return Err(Error::IdleProtocolFailed(err));
}
}
}
}
}
Ok(())
})
}
pub(crate) fn fake_idle(&self, context: &Context, watch_folder: Option<String>) {
// Idle using polling. This is also needed if we're not yet configured -
// in this case, we're waiting for a configure job (and an interrupt).
task::block_on(async move {
let fake_idle_start_time = SystemTime::now();
info!(context, "IMAP-fake-IDLEing...");
let interrupt = stop_token::StopSource::new();
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let interval = async_std::stream::interval(Duration::from_secs(60));
let mut interrupt_interval = interrupt.stop_token().stop_stream(interval);
*self.interrupt.lock().await = Some(interrupt);
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
info!(context, "fake-idle wait was skipped");
} else {
// loop until we are interrupted or if we fetched something
while let Some(_) = interrupt_interval.next().await {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.connect_configured(context) {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if self.config.read().await.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break;
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
if let Some(ref watch_folder) = watch_folder {
match self.fetch_new_messages(context, watch_folder).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break;
}
}
Err(err) => {
error!(context, "could not fetch from folder: {}", err);
self.trigger_reconnect()
}
}
}
}
}
self.interrupt.lock().await.take();
info!(
context,
"IMAP-fake-IDLE done after {:.4}s",
SystemTime::now()
.duration_since(fake_idle_start_time)
.unwrap_or_default()
.as_millis() as f64
/ 1000.,
);
})
}
pub fn interrupt_idle(&self, context: &Context) {
task::block_on(async move {
let mut interrupt: Option<stop_token::StopSource> = self.interrupt.lock().await.take();
if interrupt.is_none() {
// idle wait is not running, signal it needs to skip
self.skip_next_idle_wait.store(true, Ordering::SeqCst);
// meanwhile idle-wait may have produced the StopSource
interrupt = self.interrupt.lock().await.take();
}
// let's manually drop the StopSource
if interrupt.is_some() {
// the imap thread provided us a stop token but might
// not have entered idle_wait yet, give it some time
// for that to happen. XXX handle this without extra wait
// https://github.com/deltachat/deltachat-core-rust/issues/925
std::thread::sleep(Duration::from_millis(200));
info!(context, "low-level: dropping stop-source to interrupt idle");
std::mem::drop(interrupt)
}
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,113 +0,0 @@
use super::Imap;
use crate::context::Context;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "IMAP Could not obtain imap-session object.")]
NoSession,
#[fail(display = "IMAP Connection Lost or no connection established")]
ConnectionLost,
#[fail(display = "IMAP Folder name invalid: {:?}", _0)]
BadFolderName(String),
#[fail(display = "IMAP close/expunge failed: {}", _0)]
CloseExpungeFailed(#[cause] async_imap::error::Error),
#[fail(display = "IMAP other error: {:?}", _0)]
Other(String),
}
impl Imap {
/// select a folder, possibly update uid_validity and, if needed,
/// expunge the folder to remove delete-marked messages.
pub(super) async fn select_folder<S: AsRef<str>>(
&self,
context: &Context,
folder: Option<S>,
) -> Result<()> {
if self.session.lock().await.is_none() {
let mut cfg = self.config.write().await;
cfg.selected_folder = None;
cfg.selected_folder_needs_expunge = false;
self.trigger_reconnect();
return Err(Error::NoSession);
}
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(ref folder) = folder {
if let Some(ref selected_folder) = self.config.read().await.selected_folder {
if folder.as_ref() == selected_folder {
return Ok(());
}
}
}
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
let needs_expunge = { self.config.read().await.selected_folder_needs_expunge };
if needs_expunge {
if let Some(ref folder) = self.config.read().await.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
// A CLOSE-SELECT is considerably faster than an EXPUNGE-SELECT, see
// https://tools.ietf.org/html/rfc3501#section-6.4.2
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.close().await {
Ok(_) => {
info!(context, "close/expunge succeeded");
}
Err(err) => {
self.trigger_reconnect();
return Err(Error::CloseExpungeFailed(err));
}
}
} else {
return Err(Error::NoSession);
}
}
self.config.write().await.selected_folder_needs_expunge = false;
}
// select new folder
if let Some(ref folder) = folder {
if let Some(ref mut session) = &mut *self.session.lock().await {
let res = session.select(folder).await;
// https://tools.ietf.org/html/rfc3501#section-6.3.1
// says that if the server reports select failure we are in
// authenticated (not-select) state.
match res {
Ok(mailbox) => {
let mut config = self.config.write().await;
config.selected_folder = Some(folder.as_ref().to_string());
config.selected_mailbox = Some(mailbox);
Ok(())
}
Err(async_imap::error::Error::ConnectionLost) => {
self.trigger_reconnect();
self.config.write().await.selected_folder = None;
Err(Error::ConnectionLost)
}
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.as_ref().to_string()))
}
Err(err) => {
self.config.write().await.selected_folder = None;
self.trigger_reconnect();
Err(Error::Other(err.to_string()))
}
}
} else {
Err(Error::NoSession)
}
} else {
Ok(())
}
}
}

View File

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

View File

@@ -1,849 +0,0 @@
//! # Import/export module
use core::cmp::{max, min};
use std::path::Path;
use num_traits::FromPrimitive;
use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
use crate::chat;
use crate::config::Config;
use crate::configure::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::error::*;
use crate::events::Event;
use crate::job::*;
use crate::key::*;
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
use crate::pgp;
use crate::sql::{self, Sql};
use crate::stock::StockMessage;
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i32)]
pub enum ImexMode {
/// Export all private keys and all public keys of the user to the
/// directory given as `param1`. The default key is written to the files `public-key-default.asc`
/// and `private-key-default.asc`, if there are more keys, they are written to files as
/// `public-key-<id>.asc` and `private-key-<id>.asc`
ExportSelfKeys = 1,
/// Import private keys found in the directory given as `param1`.
/// The last imported key is made the default keys unless its name contains the string `legacy`.
/// Public keys are not imported.
ImportSelfKeys = 2,
/// Export a backup to the directory given as `param1`.
/// The backup contains all contacts, chats, images and other data and device independent settings.
/// The backup does not contain device dependent settings as ringtones or LED notification settings.
/// The name of the backup is typically `delta-chat.<day>.bak`, if more than one backup is create on a day,
/// the format is `delta-chat.<day>-<number>.bak`
ExportBackup = 11,
/// `param1` is the file (not: directory) to import. The file is normally
/// created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
/// is only possible as long as the context is not configured or used in another way.
ImportBackup = 12,
}
/// Import/export things.
/// For this purpose, the function creates a job that is executed in the IMAP-thread then;
/// this requires to call dc_perform_inbox_jobs() regularly.
///
/// What to do is defined by the *what* parameter.
///
/// While dc_imex() returns immediately, the started job may take a while,
/// you can stop it using dc_stop_ongoing_process(). During execution of the job,
/// some events are sent out:
///
/// - A number of #DC_EVENT_IMEX_PROGRESS events are sent and may be used to create
/// a progress bar or stuff like that. Moreover, you'll be informed when the imex-job is done.
///
/// - For each file written on export, the function sends #DC_EVENT_IMEX_FILE_WRITTEN
///
/// Only one import-/export-progress can run at the same time.
/// To cancel an import-/export-progress, use dc_stop_ongoing_process().
pub fn imex(context: &Context, what: ImexMode, param1: Option<impl AsRef<Path>>) {
let mut param = Params::new();
param.set_int(Param::Cmd, what as i32);
if let Some(param1) = param1 {
param.set(Param::Arg, param1.as_ref().to_string_lossy());
}
job_kill_action(context, Action::ImexImap);
job_add(context, Action::ImexImap, 0, param, 0);
}
/// Returns the filename of the backup found (otherwise an error)
pub fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
let dir_name = dir_name.as_ref();
let dir_iter = std::fs::read_dir(dir_name)?;
let mut newest_backup_time = 0;
let mut newest_backup_path: Option<std::path::PathBuf> = None;
for dirent in dir_iter {
if let Ok(dirent) = dirent {
let path = dirent.path();
let name = dirent.file_name();
let name = name.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
let sql = Sql::new();
if sql.open(context, &path, true) {
let curr_backup_time = sql
.get_raw_config_int(context, "backup_time")
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
newest_backup_time = curr_backup_time;
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close(&context);
}
}
}
}
match newest_backup_path {
Some(path) => Ok(path.to_string_lossy().into_owned()),
None => bail!("no backup found in {}", dir_name.display()),
}
}
pub fn initiate_key_transfer(context: &Context) -> Result<String> {
ensure!(context.alloc_ongoing(), "could not allocate ongoing");
let res = do_initiate_key_transfer(context);
context.free_ongoing();
res
}
fn do_initiate_key_transfer(context: &Context) -> Result<String> {
let mut msg: Message;
let setup_code = create_setup_code(context);
/* this may require a keypair to be created. this may take a second ... */
ensure!(!context.shall_stop_ongoing(), "canceled");
let setup_file_content = render_setup_file(context, &setup_code)?;
/* encrypting may also take a while ... */
ensure!(!context.shall_stop_ongoing(), "canceled");
let setup_file_blob = BlobObject::create(
context,
"autocrypt-setup-message.html",
setup_file_content.as_bytes(),
)?;
let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF)?;
msg = Message::default();
msg.type_0 = Viewtype::File;
msg.param.set(Param::File, setup_file_blob.as_name());
msg.param
.set(Param::MimeType, "application/autocrypt-setup");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
msg.param
.set_int(Param::ForcePlaintext, DC_FP_NO_AUTOCRYPT_HEADER);
ensure!(!context.shall_stop_ongoing(), "canceled");
let msg_id = chat::send_msg(context, chat_id, &mut msg)?;
info!(context, "Wait for setup message being sent ...",);
while !context.shall_stop_ongoing() {
std::thread::sleep(std::time::Duration::from_secs(1));
if let Ok(msg) = Message::load_from_db(context, msg_id) {
if msg.is_sent() {
info!(context, "... setup message sent.",);
break;
}
}
}
// no maybe_add_bcc_self_device_msg() here.
// the ui shows the dialog with the setup code on this device,
// it would be too much noise to have two things popping up at the same time.
// maybe_add_bcc_self_device_msg() is called on the other device
// once the transfer is completed.
Ok(setup_code)
}
/// Renders HTML body of a setup file message.
///
/// The `passphrase` must be at least 2 characters long.
pub fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
ensure!(
passphrase.len() >= 2,
"Passphrase must be at least 2 chars long."
);
let self_addr = e2ee::ensure_secret_key_exists(context)?;
let private_key = Key::from_self_private(context, self_addr, &context.sql)
.ok_or_else(|| format_err!("Failed to get private key."))?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled) {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
};
let private_key_asc = private_key.to_asc(ac_headers);
let encr = pgp::symm_encrypt(&passphrase, private_key_asc.as_bytes())?;
let replacement = format!(
concat!(
"-----BEGIN PGP MESSAGE-----\r\n",
"Passphrase-Format: numeric9x4\r\n",
"Passphrase-Begin: {}"
),
&passphrase[..2]
);
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject);
let msg_body = context.stock_str(StockMessage::AcSetupMsgBody);
let msg_body_html = msg_body.replace("\r", "").replace("\n", "<br>");
Ok(format!(
concat!(
"<!DOCTYPE html>\r\n",
"<html>\r\n",
" <head>\r\n",
" <title>{}</title>\r\n",
" </head>\r\n",
" <body>\r\n",
" <h1>{}</h1>\r\n",
" <p>{}</p>\r\n",
" <pre>\r\n{}\r\n</pre>\r\n",
" </body>\r\n",
"</html>\r\n"
),
msg_subj, msg_subj, msg_body_html, pgp_msg
))
}
pub fn create_setup_code(_context: &Context) -> String {
let mut random_val: u16;
let mut rng = thread_rng();
let mut ret = String::new();
for i in 0..9 {
loop {
random_val = rng.gen();
if random_val as usize <= 60000 {
break;
}
}
random_val = (random_val as usize % 10000) as u16;
ret += &format!(
"{}{:04}",
if 0 != i { "-" } else { "" },
random_val as usize
);
}
ret
}
fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool(context, "bcc_self") {
let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled.
msg.text = Some(
"It seems you are using multiple devices with Delta Chat. Great!\n\n\
If you also want to synchronize outgoing messages accross all devices, \
go to the settings and enable \"Send copy to self\"."
.to_string(),
);
chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg))?;
}
Ok(())
}
pub fn continue_key_transfer(context: &Context, msg_id: MsgId, setup_code: &str) -> Result<()> {
ensure!(!msg_id.is_special(), "wrong id");
let msg = Message::load_from_db(context, msg_id)?;
ensure!(
msg.is_setupmessage(),
"Message is no Autocrypt Setup Message."
);
if let Some(filename) = msg.get_file(context) {
let file = dc_open_file(context, filename)?;
let sc = normalize_setup_code(setup_code);
let armored_key = decrypt_setup_file(context, &sc, file)?;
set_self_key(context, &armored_key, true, true)?;
maybe_add_bcc_self_device_msg(context)?;
Ok(())
} else {
bail!("Message is no Autocrypt Setup Message.");
}
}
fn set_self_key(
context: &Context,
armored: &str,
set_default: bool,
prefer_encrypt_required: bool,
) -> Result<()> {
// try hard to only modify key-state
let keys = Key::from_armored_string(armored, KeyType::Private)
.and_then(|(k, h)| if k.verify() { Some((k, h)) } else { None })
.and_then(|(k, h)| k.split_key().map(|pub_key| (k, pub_key, h)));
ensure!(keys.is_some(), "Not a valid private key");
let (private_key, public_key, header) = keys.unwrap();
let preferencrypt = header.get("Autocrypt-Prefer-Encrypt");
match preferencrypt.map(|s| s.as_str()) {
Some(headerval) => {
let e2ee_enabled = match headerval {
"nopreference" => 0,
"mutual" => 1,
_ => {
bail!("invalid Autocrypt-Prefer-Encrypt header: {:?}", header);
}
};
context
.sql
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)?;
}
None => {
if prefer_encrypt_required {
bail!("missing Autocrypt-Prefer-Encrypt header");
}
}
};
let self_addr = context.get_config(Config::ConfiguredAddr);
ensure!(self_addr.is_some(), "Missing self addr");
// XXX maybe better make dc_key_save_self_keypair delete things
sql::execute(
context,
&context.sql,
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key.to_bytes(), private_key.to_bytes()],
)?;
if set_default {
sql::execute(
context,
&context.sql,
"UPDATE keypairs SET is_default=0;",
params![],
)?;
}
if !dc_key_save_self_keypair(
context,
&public_key,
&private_key,
self_addr.unwrap_or_default(),
set_default,
&context.sql,
) {
bail!("Cannot save keypair, internal key-state possibly corrupted now!");
}
Ok(())
}
fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
_context: &Context,
passphrase: &str,
file: T,
) -> Result<String> {
let plain_bytes = pgp::symm_decrypt(passphrase, file)?;
let plain_text = std::string::String::from_utf8(plain_bytes)?;
Ok(plain_text)
}
pub fn normalize_setup_code(s: &str) -> String {
let mut out = String::new();
for c in s.chars() {
if c >= '0' && c <= '9' {
out.push(c);
if let 4 | 9 | 14 | 19 | 24 | 29 | 34 | 39 = out.len() {
out += "-"
}
}
}
out
}
#[allow(non_snake_case)]
pub fn JobImexImap(context: &Context, job: &Job) -> Result<()> {
ensure!(context.alloc_ongoing(), "could not allocate ongoing");
let what: Option<ImexMode> = job.param.get_int(Param::Cmd).and_then(ImexMode::from_i32);
let param = job.param.get(Param::Arg).unwrap_or_default();
ensure!(!param.is_empty(), "No Import/export dir/file given.");
info!(context, "Import/export process started.");
context.call_cb(Event::ImexProgress(10));
ensure!(context.sql.is_open(), "Database not opened.");
if what == Some(ImexMode::ExportBackup) || what == Some(ImexMode::ExportSelfKeys) {
// before we export anything, make sure the private key exists
if e2ee::ensure_secret_key_exists(context).is_err() {
context.free_ongoing();
bail!("Cannot create private key or private key not available.");
} else {
dc_create_folder(context, &param)?;
}
}
let path = Path::new(param);
let success = match what {
Some(ImexMode::ExportSelfKeys) => export_self_keys(context, path),
Some(ImexMode::ImportSelfKeys) => import_self_keys(context, path),
Some(ImexMode::ExportBackup) => export_backup(context, path),
Some(ImexMode::ImportBackup) => import_backup(context, path),
None => {
bail!("unknown IMEX type");
}
};
context.free_ongoing();
match success {
Ok(()) => {
info!(context, "IMEX successfully completed");
context.call_cb(Event::ImexProgress(1000));
Ok(())
}
Err(err) => {
context.call_cb(Event::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err);
}
}
}
/// Import Backup
fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
info!(
context,
"Import \"{}\" to \"{}\".",
backup_to_import.as_ref().display(),
context.get_dbfile().display()
);
ensure!(
!dc_is_configured(context),
"Cannot import backups to accounts in use."
);
context.sql.close(&context);
dc_delete_file(context, context.get_dbfile());
ensure!(
!context.get_dbfile().exists(),
"Cannot delete old database."
);
ensure!(
dc_copy_file(context, backup_to_import.as_ref(), context.get_dbfile()),
"could not copy file"
);
/* error already logged */
/* re-open copied database file */
ensure!(
context.sql.open(&context, &context.get_dbfile(), false),
"could not re-open db"
);
let total_files_cnt = context
.sql
.query_get_value::<_, isize>(context, "SELECT COUNT(*) FROM backup_blobs;", params![])
.unwrap_or_default() as usize;
info!(
context,
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
);
let res = context.sql.query_map(
"SELECT file_name, file_content FROM backup_blobs ORDER BY id;",
params![],
|row| {
let name: String = row.get(0)?;
let blob: Vec<u8> = row.get(1)?;
Ok((name, blob))
},
|files| {
for (processed_files_cnt, file) in files.enumerate() {
let (file_name, file_blob) = file?;
if context.shall_stop_ongoing() {
return Ok(false);
}
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
if permille < 10 {
permille = 10
}
if permille > 990 {
permille = 990
}
context.call_cb(Event::ImexProgress(permille));
if file_blob.is_empty() {
continue;
}
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, &file_blob)?;
}
Ok(true)
},
);
match res {
Ok(all_files_extracted) => {
if all_files_extracted {
// only delete backup_blobs if all files were successfully extracted
sql::execute(context, &context.sql, "DROP TABLE backup_blobs;", params![])?;
sql::try_execute(context, &context.sql, "VACUUM;").ok();
Ok(())
} else {
bail!("received stop signal");
}
}
Err(err) => Err(err.into()),
}
}
/*******************************************************************************
* Export backup
******************************************************************************/
/* the FILE_PROGRESS macro calls the callback with the permille of files processed.
The macro avoids weird values of 0% or 100% while still working. */
fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
// FIXME: we should write to a temporary file first and rename it on success. this would guarantee the backup is complete.
// let dest_path_filename = dc_get_next_backup_file(context, dir, res);
let now = time();
let dest_path_filename = dc_get_next_backup_path(dir, now)?;
let dest_path_string = dest_path_filename.to_string_lossy().to_string();
sql::housekeeping(context);
sql::try_execute(context, &context.sql, "VACUUM;").ok();
// we close the database during the copy of the dbfile
context.sql.close(context);
info!(
context,
"Backup '{}' to '{}'.",
context.get_dbfile().display(),
dest_path_filename.display(),
);
let copied = dc_copy_file(context, context.get_dbfile(), &dest_path_filename);
context.sql.open(&context, &context.get_dbfile(), false);
if !copied {
bail!(
"could not copy file from '{}' to '{}'",
context.get_dbfile().display(),
dest_path_string
);
}
let dest_sql = Sql::new();
ensure!(
dest_sql.open(context, &dest_path_filename, false),
"could not open exported database {}",
dest_path_string
);
let res = match add_files_to_export(context, &dest_sql) {
Err(err) => {
dc_delete_file(context, &dest_path_filename);
error!(context, "backup failed: {}", err);
Err(err)
}
Ok(()) => {
dest_sql.set_raw_config_int(context, "backup_time", now as i32)?;
context.call_cb(Event::ImexFileWritten(dest_path_filename));
Ok(())
}
};
dest_sql.close(context);
Ok(res?)
}
fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
// add all files as blobs to the database copy (this does not require
// the source to be locked, neigher the destination as it is used only here)
if !sql.table_exists("backup_blobs") {
sql::execute(
context,
&sql,
"CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);",
params![],
)?
}
// copy all files from BLOBDIR into backup-db
let mut total_files_cnt = 0;
let dir = context.get_blobdir();
let dir_handle = std::fs::read_dir(&dir)?;
total_files_cnt += dir_handle.filter(|r| r.is_ok()).count();
info!(context, "EXPORT: total_files_cnt={}", total_files_cnt);
// scan directory, pass 2: copy files
let dir_handle = std::fs::read_dir(&dir)?;
let exported_all_files = sql.prepare(
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
|mut stmt, _| {
let mut processed_files_cnt = 0;
for entry in dir_handle {
let entry = entry?;
if context.shall_stop_ongoing() {
return Ok(false);
}
processed_files_cnt += 1;
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
context.call_cb(Event::ImexProgress(permille));
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
continue;
}
info!(context, "EXPORT: copying filename={}", name);
let curr_path_filename = context.get_blobdir().join(entry.file_name());
if let Ok(buf) = dc_read_file(context, &curr_path_filename) {
if buf.is_empty() {
continue;
}
// bail out if we can't insert
stmt.execute(params![name, buf])?;
}
}
Ok(true)
},
)?;
ensure!(exported_all_files, "canceled during export-files");
Ok(())
}
/*******************************************************************************
* Classic key import
******************************************************************************/
fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
/* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import
plain ASC keys, at least keys without a password, if we do not want to implement a password entry function.
Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation.
Maybe we should make the "default" key handlong also a little bit smarter
(currently, the last imported key is the standard key unless it contains the string "legacy" in its name) */
let mut set_default: bool;
let mut imported_cnt = 0;
let dir_name = dir.as_ref().to_string_lossy();
let dir_handle = std::fs::read_dir(&dir)?;
for entry in dir_handle {
let entry_fn = entry?.file_name();
let name_f = entry_fn.to_string_lossy();
let path_plus_name = dir.as_ref().join(&entry_fn);
match dc_get_filesuffix_lc(&name_f) {
Some(suffix) => {
if suffix != "asc" {
continue;
}
set_default = if name_f.contains("legacy") {
info!(context, "found legacy key '{}'", path_plus_name.display());
false
} else {
true
}
}
None => {
continue;
}
}
match dc_read_file(context, &path_plus_name) {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
if let Err(err) = set_self_key(context, &armored, set_default, false) {
error!(context, "set_self_key: {}", err);
continue;
}
}
Err(_) => continue,
}
imported_cnt += 1;
}
ensure!(
imported_cnt > 0,
"No private keys found in \"{}\".",
dir_name
);
Ok(())
}
fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let mut export_errors = 0;
context.sql.query_map(
"SELECT id, public_key, private_key, is_default FROM keypairs;",
params![],
|row| {
let id = row.get(0)?;
let public_key_blob: Vec<u8> = row.get(1)?;
let public_key = Key::from_slice(&public_key_blob, KeyType::Public);
let private_key_blob: Vec<u8> = row.get(2)?;
let private_key = Key::from_slice(&private_key_blob, KeyType::Private);
let is_default: i32 = row.get(3)?;
Ok((id, public_key, private_key, is_default))
},
|keys| {
for key_pair in keys {
let (id, public_key, private_key, is_default) = key_pair?;
let id = Some(id).filter(|_| is_default != 0);
if let Some(key) = public_key {
if export_key_to_asc_file(context, &dir, id, &key).is_err() {
export_errors += 1;
}
} else {
export_errors += 1;
}
if let Some(key) = private_key {
if export_key_to_asc_file(context, &dir, id, &key).is_err() {
export_errors += 1;
}
} else {
export_errors += 1;
}
}
Ok(())
},
)?;
ensure!(export_errors == 0, "errors while exporting keys");
Ok(())
}
/*******************************************************************************
* Classic key export
******************************************************************************/
fn export_key_to_asc_file(
context: &Context,
dir: impl AsRef<Path>,
id: Option<i64>,
key: &Key,
) -> std::io::Result<()> {
let file_name = {
let kind = if key.is_public() { "public" } else { "private" };
let id = id.map_or("default".into(), |i| i.to_string());
dir.as_ref().join(format!("{}-key-{}.asc", kind, &id))
};
info!(context, "Exporting key {}", file_name.display());
dc_delete_file(context, &file_name);
let res = key.write_asc_to_file(&file_name, context);
if res.is_err() {
error!(context, "Cannot write key to {}", file_name.display());
} else {
context.call_cb(Event::ImexFileWritten(file_name));
}
res
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::test_utils::*;
use ::pgp::armor::BlockType;
#[test]
fn test_render_setup_file() {
let t = test_context(Some(Box::new(logging_cb)));
configure_alice_keypair(&t.ctx);
let msg = render_setup_file(&t.ctx, "hello").unwrap();
println!("{}", &msg);
// Check some substrings, indicating things got substituted.
// In particular note the mixing of `\r\n` and `\n` depending
// on who generated the stings.
assert!(msg.contains("<title>Autocrypt Setup Message</title"));
assert!(msg.contains("<h1>Autocrypt Setup Message</h1>"));
assert!(msg.contains("<p>This is the Autocrypt Setup Message used to"));
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
assert!(msg.contains("Passphrase-Begin: he\n"));
assert!(msg.contains("==\n"));
assert!(msg.contains("-----END PGP MESSAGE-----\n"));
}
#[test]
fn test_render_setup_file_newline_replace() {
let t = dummy_context();
t.ctx
.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.unwrap();
configure_alice_keypair(&t.ctx);
let msg = render_setup_file(&t.ctx, "pw").unwrap();
println!("{}", &msg);
assert!(msg.contains("<p>hello<br>there</p>"));
}
#[test]
fn test_create_setup_code() {
let t = dummy_context();
let setupcode = create_setup_code(&t.ctx);
assert_eq!(setupcode.len(), 44);
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
assert_eq!(setupcode.chars().nth(9).unwrap(), '-');
assert_eq!(setupcode.chars().nth(14).unwrap(), '-');
assert_eq!(setupcode.chars().nth(19).unwrap(), '-');
assert_eq!(setupcode.chars().nth(24).unwrap(), '-');
assert_eq!(setupcode.chars().nth(29).unwrap(), '-');
assert_eq!(setupcode.chars().nth(34).unwrap(), '-');
assert_eq!(setupcode.chars().nth(39).unwrap(), '-');
}
#[test]
fn test_export_key_to_asc_file() {
let context = dummy_context();
let base64 = include_str!("../test-data/key/public.asc");
let key = Key::from_base64(base64, KeyType::Public).unwrap();
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{}/public-key-default.asc", blobdir);
let bytes = std::fs::read(&filename).unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[test]
fn test_normalize_setup_code() {
let norm = normalize_setup_code("123422343234423452346234723482349234");
assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
let norm =
normalize_setup_code("\t1 2 3422343234- foo bar-- 423-45 2 34 6234723482349234 ");
assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
}
/* S_EM_SETUPFILE is a AES-256 symm. encrypted setup message created by Enigmail
with an "encrypted session key", see RFC 4880. The code is in S_EM_SETUPCODE */
const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
const S_EM_SETUPFILE: &str = include_str!("../test-data/message/stress.txt");
#[test]
fn test_split_and_decrypt() {
let ctx = dummy_context();
let context = &ctx.ctx;
let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();
let (typ, headers, base64) = split_armored_data(&buf_1).unwrap();
assert_eq!(typ, BlockType::Message);
assert!(S_EM_SETUPCODE.starts_with(headers.get(HEADER_SETUPCODE).unwrap()));
assert!(headers.get(HEADER_AUTOCRYPT).is_none());
assert!(!base64.is_empty());
let setup_file = S_EM_SETUPFILE.to_string();
let decrypted = decrypt_setup_file(
context,
S_EM_SETUPCODE,
std::io::Cursor::new(setup_file.as_bytes()),
)
.unwrap();
let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap();
assert_eq!(typ, BlockType::PrivateKey);
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
assert!(headers.get(HEADER_SETUPCODE).is_none());
}
}

1098
src/job.rs

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
use std::sync::{Arc, Condvar, Mutex}; use std::sync::{Arc, Condvar, Mutex};
use crate::configure::*;
use crate::context::Context; use crate::context::Context;
use crate::error::{Error, Result};
use crate::imap::Imap; use crate::imap::Imap;
#[derive(Debug)] #[derive(Debug)]
@@ -15,7 +15,7 @@ pub struct JobThread {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct JobState { pub struct JobState {
idle: bool, idle: bool,
jobs_needed: bool, jobs_needed: i32,
suspended: bool, suspended: bool,
using_handle: bool, using_handle: bool,
} }
@@ -58,22 +58,21 @@ impl JobThread {
pub fn interrupt_idle(&self, context: &Context) { pub fn interrupt_idle(&self, context: &Context) {
{ {
self.state.0.lock().unwrap().jobs_needed = true; self.state.0.lock().unwrap().jobs_needed = 1;
} }
info!(context, "Interrupting {}-IDLE...", self.name); info!(context, "Interrupting {}-IDLE...", self.name);
self.imap.interrupt_idle(context); self.imap.interrupt_idle();
let &(ref lock, ref cvar) = &*self.state.clone(); let &(ref lock, ref cvar) = &*self.state.clone();
let mut state = lock.lock().unwrap(); let mut state = lock.lock().unwrap();
state.idle = true; state.idle = true;
cvar.notify_one(); cvar.notify_one();
info!(context, "Interrupting {}-IDLE... finished", self.name);
} }
pub async fn fetch(&mut self, context: &Context, use_network: bool) { pub fn fetch(&mut self, context: &Context, use_network: bool) {
{ {
let &(ref lock, _) = &*self.state.clone(); let &(ref lock, _) = &*self.state.clone();
let mut state = lock.lock().unwrap(); let mut state = lock.lock().unwrap();
@@ -86,53 +85,53 @@ impl JobThread {
} }
if use_network { if use_network {
if let Err(err) = self.connect_and_fetch(context).await { let start = std::time::Instant::now();
warn!(context, "connect+fetch failed: {}, reconnect & retry", err); if self.connect_to_imap(context) {
self.imap.trigger_reconnect(); info!(context, "{}-fetch started...", self.name);
if let Err(err) = self.connect_and_fetch(context).await { self.imap.fetch(context);
warn!(context, "connect+fetch failed: {}", err);
if self.imap.should_reconnect() {
info!(context, "{}-fetch aborted, starting over...", self.name,);
self.imap.fetch(context);
} }
info!(
context,
"{}-fetch done in {:.3} ms.",
self.name,
start.elapsed().as_millis(),
);
} }
} }
self.state.0.lock().unwrap().using_handle = false; self.state.0.lock().unwrap().using_handle = false;
} }
async fn connect_and_fetch(&mut self, context: &Context) -> Result<()> { fn connect_to_imap(&self, context: &Context) -> bool {
let prefix = format!("{}-fetch", self.name); if self.imap.is_connected() {
match self.imap.connect_configured(context) { return true;
Ok(()) => {
if let Some(watch_folder) = self.get_watch_folder(context) {
let start = std::time::Instant::now();
info!(context, "{} started...", prefix);
let res = self
.imap
.fetch(context, &watch_folder)
.await
.map_err(Into::into);
let elapsed = start.elapsed().as_millis();
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
res
} else {
Err(Error::WatchFolderNotFound("not-set".to_string()))
}
}
Err(err) => Err(crate::error::Error::Message(err.to_string())),
} }
}
fn get_watch_folder(&self, context: &Context) -> Option<String> { let mut ret_connected = dc_connect_to_configured_imap(context, &self.imap) != 0;
match context.sql.get_raw_config(context, self.folder_config_name) {
Some(name) => Some(name), if ret_connected {
None => { if context
if self.folder_config_name == "configured_inbox_folder" { .sql
// initialized with old version, so has not set configured_inbox_folder .get_config_int(context, "folders_configured")
Some("INBOX".to_string()) .unwrap_or_default()
} else { < 3
None {
} self.imap.configure_folders(context, 0x1);
}
if let Some(mvbox_name) = context.sql.get_config(context, self.folder_config_name) {
self.imap.set_watch_folder(mvbox_name);
} else {
self.imap.disconnect(context);
ret_connected = false;
} }
} }
ret_connected
} }
pub fn idle(&self, context: &Context, use_network: bool) { pub fn idle(&self, context: &Context, use_network: bool) {
@@ -140,13 +139,13 @@ impl JobThread {
let &(ref lock, ref cvar) = &*self.state.clone(); let &(ref lock, ref cvar) = &*self.state.clone();
let mut state = lock.lock().unwrap(); let mut state = lock.lock().unwrap();
if state.jobs_needed { if 0 != state.jobs_needed {
info!( info!(
context, context,
"{}-IDLE will not be started as it was interrupted while not ideling.", "{}-IDLE will not be started as it was interrupted while not ideling.",
self.name, self.name,
); );
state.jobs_needed = false; state.jobs_needed = 0;
return; return;
} }
@@ -171,36 +170,10 @@ impl JobThread {
} }
} }
let prefix = format!("{}-IDLE", self.name); self.connect_to_imap(context);
let do_fake_idle = match self.imap.connect_configured(context) { info!(context, "{}-IDLE started...", self.name,);
Ok(()) => { self.imap.idle(context);
if !self.imap.can_idle() { info!(context, "{}-IDLE ended.", self.name);
true // we have to do fake_idle
} else {
let watch_folder = self.get_watch_folder(context);
info!(context, "{} started...", prefix);
let res = self.imap.idle(context, watch_folder);
info!(context, "{} ended...", prefix);
if let Err(err) = res {
warn!(context, "{} failed: {} -> reconnecting", prefix, err);
// something is borked, let's start afresh on the next occassion
self.imap.disconnect(context);
}
false
}
}
Err(err) => {
info!(context, "{}-IDLE connection fail: {:?}", self.name, err);
// if the connection fails, use fake_idle to retry periodically
// fake_idle() will be woken up by interrupt_idle() as
// well so will act on maybe_network events
true
}
};
if do_fake_idle {
let watch_folder = self.get_watch_folder(context);
self.imap.fake_idle(context, watch_folder);
}
self.state.0.lock().unwrap().using_handle = false; self.state.0.lock().unwrap().using_handle = false;
} }

View File

@@ -1,9 +1,8 @@
//! Cryptographic key module
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::io::Cursor; use std::io::Cursor;
use std::path::Path; use std::path::Path;
use libc;
use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey}; use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize; use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait}; use pgp::types::{KeyTrait, SecretKeyTrait};
@@ -13,7 +12,6 @@ use crate::context::Context;
use crate::dc_tools::*; use crate::dc_tools::*;
use crate::sql::{self, Sql}; use crate::sql::{self, Sql};
/// Cryptographic key
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub enum Key { pub enum Key {
Public(SignedPublicKey), Public(SignedPublicKey),
@@ -32,44 +30,44 @@ impl From<SignedSecretKey> for Key {
} }
} }
impl std::convert::TryFrom<Key> for SignedSecretKey { impl std::convert::TryInto<SignedSecretKey> for Key {
type Error = (); type Error = ();
fn try_from(value: Key) -> Result<Self, Self::Error> { fn try_into(self) -> Result<SignedSecretKey, Self::Error> {
match value { match self {
Key::Public(_) => Err(()), Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key), Key::Secret(key) => Ok(key),
} }
} }
} }
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey { impl<'a> std::convert::TryInto<&'a SignedSecretKey> for &'a Key {
type Error = (); type Error = ();
fn try_from(value: &'a Key) -> Result<Self, Self::Error> { fn try_into(self) -> Result<&'a SignedSecretKey, Self::Error> {
match value { match self {
Key::Public(_) => Err(()), Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key), Key::Secret(key) => Ok(key),
} }
} }
} }
impl std::convert::TryFrom<Key> for SignedPublicKey { impl std::convert::TryInto<SignedPublicKey> for Key {
type Error = (); type Error = ();
fn try_from(value: Key) -> Result<Self, Self::Error> { fn try_into(self) -> Result<SignedPublicKey, Self::Error> {
match value { match self {
Key::Public(key) => Ok(key), Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()), Key::Secret(_) => Err(()),
} }
} }
} }
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedPublicKey { impl<'a> std::convert::TryInto<&'a SignedPublicKey> for &'a Key {
type Error = (); type Error = ();
fn try_from(value: &'a Key) -> Result<Self, Self::Error> { fn try_into(self) -> Result<&'a SignedPublicKey, Self::Error> {
match value { match self {
Key::Public(key) => Ok(key), Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()), Key::Secret(_) => Err(()),
} }
@@ -166,8 +164,8 @@ impl Key {
pub fn to_bytes(&self) -> Vec<u8> { pub fn to_bytes(&self) -> Vec<u8> {
match self { match self {
Key::Public(k) => k.to_bytes().unwrap_or_default(), Key::Public(k) => k.to_bytes().unwrap(),
Key::Secret(k) => k.to_bytes().unwrap_or_default(), Key::Secret(k) => k.to_bytes().unwrap(),
} }
} }
@@ -178,9 +176,21 @@ impl Key {
} }
} }
pub fn to_base64(&self) -> String { pub fn to_base64(&self, break_every: usize) -> String {
let buf = self.to_bytes(); let buf = self.to_bytes();
base64::encode(&buf)
let encoded = base64::encode(&buf);
encoded
.as_bytes()
.chunks(break_every)
.fold(String::new(), |mut res, buf| {
// safe because we are using a base64 encoded string
res += unsafe { std::str::from_utf8_unchecked(buf) };
res += " ";
res
})
.trim()
.to_string()
} }
pub fn to_armored_string( pub fn to_armored_string(
@@ -205,18 +215,15 @@ impl Key {
.expect("failed to serialize key") .expect("failed to serialize key")
} }
pub fn write_asc_to_file( pub fn write_asc_to_file(&self, file: impl AsRef<Path>, context: &Context) -> bool {
&self,
file: impl AsRef<Path>,
context: &Context,
) -> std::io::Result<()> {
let file_content = self.to_asc(None).into_bytes(); let file_content = self.to_asc(None).into_bytes();
let res = dc_write_file(context, &file, &file_content); if dc_write_file(context, &file, &file_content) {
if res.is_err() { return true;
} else {
error!(context, "Cannot write key to {}", file.as_ref().display()); error!(context, "Cannot write key to {}", file.as_ref().display());
return false;
} }
res
} }
pub fn fingerprint(&self) -> String { pub fn fingerprint(&self) -> String {
@@ -247,14 +254,14 @@ pub fn dc_key_save_self_keypair(
public_key: &Key, public_key: &Key,
private_key: &Key, private_key: &Key,
addr: impl AsRef<str>, addr: impl AsRef<str>,
is_default: bool, is_default: libc::c_int,
sql: &Sql, sql: &Sql,
) -> bool { ) -> bool {
sql::execute( sql::execute(
context, context,
sql, sql,
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);", "INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);",
params![addr.as_ref(), is_default as i32, public_key.to_bytes(), private_key.to_bytes(), time()], params![addr.as_ref(), is_default, public_key.to_bytes(), private_key.to_bytes(), time()],
).is_ok() ).is_ok()
} }
@@ -375,7 +382,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
#[test] #[test]
#[ignore] // is too expensive #[ignore] // is too expensive
fn test_from_slice_roundtrip() { fn test_from_slice_roundtrip() {
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap(); let (public_key, private_key) = crate::pgp::dc_pgp_create_keypair("hello").unwrap();
let binary = public_key.to_bytes(); let binary = public_key.to_bytes();
let public_key2 = Key::from_slice(&binary, KeyType::Public).expect("invalid public key"); let public_key2 = Key::from_slice(&binary, KeyType::Public).expect("invalid public key");
@@ -410,7 +417,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
#[test] #[test]
#[ignore] // is too expensive #[ignore] // is too expensive
fn test_ascii_roundtrip() { fn test_ascii_roundtrip() {
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap(); let (public_key, private_key) = crate::pgp::dc_pgp_create_keypair("hello").unwrap();
let s = public_key.to_armored_string(None).unwrap(); let s = public_key.to_armored_string(None).unwrap();
let (public_key2, _) = let (public_key2, _) =

View File

@@ -1,9 +1,9 @@
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)] #![deny(clippy::correctness, missing_debug_implementations)]
// for now we hide warnings to not clutter/hide errors during "cargo clippy" // TODO: make all of these errors, such that clippy actually passes.
#![allow(clippy::cognitive_complexity, clippy::too_many_arguments)] #![warn(clippy::all, clippy::perf, clippy::not_unsafe_ptr_arg_deref)]
#![allow(clippy::unreadable_literal, clippy::match_bool)] // This is nice, but for now just annoying.
#![allow(clippy::unreadable_literal)]
#![feature(ptr_wrapping_offset_from)] #![feature(ptr_wrapping_offset_from)]
#![feature(drain_filter)]
#[macro_use] #[macro_use]
extern crate failure_derive; extern crate failure_derive;
@@ -17,20 +17,19 @@ extern crate strum;
#[macro_use] #[macro_use]
extern crate strum_macros; extern crate strum_macros;
#[macro_use] #[macro_use]
extern crate jetscii;
#[macro_use]
extern crate debug_stub_derive; extern crate debug_stub_derive;
#[macro_use] #[macro_use]
pub mod log; mod log;
#[macro_use] #[macro_use]
pub mod error; pub mod error;
pub mod headerdef;
pub(crate) mod events; pub(crate) mod events;
pub use events::*; pub use events::*;
mod aheader; mod aheader;
pub mod blob;
pub mod chat; pub mod chat;
pub mod chatlist; pub mod chatlist;
pub mod config; pub mod config;
@@ -40,40 +39,35 @@ pub mod contact;
pub mod context; pub mod context;
mod e2ee; mod e2ee;
mod imap; mod imap;
mod imap_client;
pub mod imex;
pub mod job; pub mod job;
mod job_thread; mod job_thread;
pub mod key; pub mod key;
pub mod keyring; pub mod keyring;
pub mod location; pub mod location;
mod login_param;
pub mod lot; pub mod lot;
pub mod message; pub mod message;
mod mimefactory;
pub mod mimeparser;
pub mod oauth2; pub mod oauth2;
mod param; mod param;
pub mod peerstate; pub mod peerstate;
pub mod pgp; pub mod pgp;
pub mod qr; pub mod qr;
pub mod securejoin;
mod smtp; mod smtp;
pub mod sql; pub mod sql;
pub mod stock; mod stock;
mod token; pub mod x;
#[macro_use]
mod dehtml;
pub mod dc_array;
mod dc_dehtml;
pub mod dc_imex;
mod dc_mimefactory;
pub mod dc_mimeparser;
pub mod dc_receive_imf; pub mod dc_receive_imf;
mod dc_simplify; mod dc_simplify;
mod dc_strencode;
pub mod dc_tools; pub mod dc_tools;
mod login_param;
/// if set imap/incoming and smtp/outgoing MIME messages will be printed pub mod securejoin;
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG"; mod token;
/// if set IMAP protocol commands and responses will be printed
pub const DCC_IMAP_DEBUG: &str = "DCC_IMAP_DEBUG";
#[cfg(test)] #[cfg(test)]
mod test_utils; mod test_utils;

View File

@@ -1,24 +1,20 @@
//! Location handling
use bitflags::bitflags; use bitflags::bitflags;
use quick_xml; use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText}; use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::chat; use crate::chat;
use crate::config::Config;
use crate::constants::*; use crate::constants::*;
use crate::context::*; use crate::context::*;
use crate::dc_tools::*; use crate::dc_tools::*;
use crate::error::Error; use crate::error::Error;
use crate::events::Event; use crate::events::Event;
use crate::job::*; use crate::job::*;
use crate::message::{Message, MsgId}; use crate::message::*;
use crate::mimeparser::SystemMessage;
use crate::param::*; use crate::param::*;
use crate::sql; use crate::sql;
use crate::stock::StockMessage; use crate::stock::StockMessage;
/// Location record // location handling
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Location { pub struct Location {
pub location_id: u32, pub location_id: u32,
@@ -64,11 +60,14 @@ impl Kml {
Default::default() Default::default()
} }
pub fn parse(context: &Context, content: &[u8]) -> Result<Self, Error> { pub fn parse(context: &Context, content: impl AsRef<str>) -> Result<Self, Error> {
ensure!(content.len() <= 1024 * 1024, "kml-file is too large"); ensure!(
content.as_ref().len() <= (1 * 1024 * 1024),
"A kml-files with {} bytes is larger than reasonably expected.",
content.as_ref().len()
);
let to_parse = String::from_utf8_lossy(content); let mut reader = quick_xml::Reader::from_str(content.as_ref());
let mut reader = quick_xml::Reader::from_str(&to_parse);
reader.trim_text(true); reader.trim_text(true);
let mut kml = Kml::new(); let mut kml = Kml::new();
@@ -195,8 +194,10 @@ impl Kml {
// location streaming // location streaming
pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) { pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
let now = time(); let now = time();
if !(seconds < 0 || chat_id <= DC_CHAT_ID_LAST_SPECIAL) { let mut msg: Message;
let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id); let is_sending_locations_before: bool;
if !(seconds < 0 || chat_id <= 9i32 as libc::c_uint) {
is_sending_locations_before = is_sending_locations_to_chat(context, chat_id);
if sql::execute( if sql::execute(
context, context,
&context.sql, &context.sql,
@@ -213,23 +214,23 @@ pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
.is_ok() .is_ok()
{ {
if 0 != seconds && !is_sending_locations_before { if 0 != seconds && !is_sending_locations_before {
let mut msg = Message::new(Viewtype::Text); msg = dc_msg_new(Viewtype::Text);
msg.text = msg.text =
Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)); Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0));
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled); msg.param.set_int(Param::Cmd, 8);
chat::send_msg(context, chat_id, &mut msg).unwrap_or_default(); chat::send_msg(context, chat_id, &mut msg).unwrap();
} else if 0 == seconds && is_sending_locations_before { } else if 0 == seconds && is_sending_locations_before {
let stock_str = let stock_str =
context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_info_msg(context, chat_id, stock_str); chat::add_device_msg(context, chat_id, stock_str);
} }
context.call_cb(Event::ChatModified(chat_id)); context.call_cb(Event::ChatModified(chat_id));
if 0 != seconds { if 0 != seconds {
schedule_MAYBE_SEND_LOCATIONS(context, false); schedule_MAYBE_SEND_LOCATIONS(context, 0i32);
job_add( job_add(
context, context,
Action::MaybeSendLocationsEnded, Action::MaybeSendLocationsEnded,
chat_id as i32, chat_id as libc::c_int,
Params::new(), Params::new(),
seconds + 1, seconds + 1,
); );
@@ -239,8 +240,8 @@ pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, force_schedule: bool) { fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, flags: i32) {
if force_schedule || !job_action_exists(context, Action::MaybeSendLocations) { if 0 != flags & 0x1 || !job_action_exists(context, Action::MaybeSendLocations) {
job_add(context, Action::MaybeSendLocations, 0, Params::new(), 60); job_add(context, Action::MaybeSendLocations, 0, Params::new(), 60);
}; };
} }
@@ -255,9 +256,9 @@ pub fn is_sending_locations_to_chat(context: &Context, chat_id: u32) -> bool {
.unwrap_or_default() .unwrap_or_default()
} }
pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool { pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> libc::c_int {
if latitude == 0.0 && longitude == 0.0 { if latitude == 0.0 && longitude == 0.0 {
return true; return 1;
} }
let mut continue_streaming = false; let mut continue_streaming = false;
@@ -277,7 +278,7 @@ pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> b
accuracy, accuracy,
time(), time(),
chat_id, chat_id,
DC_CONTACT_ID_SELF, 1,
] ]
) { ) {
warn!(context, "failed to store location {:?}", err); warn!(context, "failed to store location {:?}", err);
@@ -286,12 +287,12 @@ pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> b
} }
} }
if continue_streaming { if continue_streaming {
context.call_cb(Event::LocationChanged(Some(DC_CONTACT_ID_SELF))); context.call_cb(Event::LocationChanged(Some(1)));
}; };
schedule_MAYBE_SEND_LOCATIONS(context, false); schedule_MAYBE_SEND_LOCATIONS(context, 0);
} }
continue_streaming continue_streaming as libc::c_int
} }
pub fn get_range( pub fn get_range(
@@ -304,15 +305,16 @@ pub fn get_range(
if timestamp_to == 0 { if timestamp_to == 0 {
timestamp_to = time() + 10; timestamp_to = time() + 10;
} }
context context
.sql .sql
.query_map( .query_map(
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \ "SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \ 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=?) \ FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
AND (? OR l.from_id=?) \ AND (? OR l.from_id=?) \
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \ AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;", ORDER BY l.timestamp DESC, l.id DESC, m.id DESC;",
params![ params![
if chat_id == 0 { 1 } else { 0 }, if chat_id == 0 { 1 } else { 0 },
chat_id as i32, chat_id as i32,
@@ -329,6 +331,7 @@ pub fn get_range(
} else { } else {
None None
}; };
let loc = Location { let loc = Location {
location_id: row.get(0)?, location_id: row.get(0)?,
latitude: row.get(1)?, latitude: row.get(1)?,
@@ -356,7 +359,7 @@ pub fn get_range(
} }
fn is_marker(txt: &str) -> bool { fn is_marker(txt: &str) -> bool {
txt.len() == 1 && !txt.starts_with(' ') txt.len() == 1 && txt.chars().next().unwrap() != ' '
} }
pub fn delete_all(context: &Context) -> Result<(), Error> { pub fn delete_all(context: &Context) -> Result<(), Error> {
@@ -366,10 +369,14 @@ pub fn delete_all(context: &Context) -> Result<(), Error> {
} }
pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error> { pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error> {
let now = time();
let mut location_count = 0;
let mut ret = String::new();
let mut last_added_location_id = 0; let mut last_added_location_id = 0;
let self_addr = context let self_addr = context
.get_config(Config::ConfiguredAddr) .sql
.get_config(context, "configured_addr")
.unwrap_or_default(); .unwrap_or_default();
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row( let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
@@ -382,24 +389,21 @@ pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error>
Ok((send_begin, send_until, last_sent)) Ok((send_begin, send_until, last_sent))
})?; })?;
let now = time(); if !(locations_send_begin == 0 || now > locations_send_until) {
let mut location_count = 0;
let mut ret = String::new();
if locations_send_begin != 0 && now <= locations_send_until {
ret += &format!( ret += &format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{}\">\n", "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{}\">\n",
self_addr, self_addr,
); );
context.sql.query_map( context.sql.query_map(
"SELECT id, latitude, longitude, accuracy, timestamp \ "SELECT id, latitude, longitude, accuracy, timestamp\
FROM locations WHERE from_id=? \ FROM locations WHERE from_id=? \
AND timestamp>=? \ AND timestamp>=? \
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \ AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
AND independent=0 \ AND independent=0 \
GROUP BY timestamp \ GROUP BY timestamp \
ORDER BY timestamp;", ORDER BY timestamp;",
params![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF], params![1, locations_send_begin, locations_last_sent, 1],
|row| { |row| {
let location_id: i32 = row.get(0)?; let location_id: i32 = row.get(0)?;
let latitude: f64 = row.get(1)?; let latitude: f64 = row.get(1)?;
@@ -413,7 +417,7 @@ pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error>
for row in rows { for row in rows {
let (location_id, latitude, longitude, accuracy, timestamp) = row?; let (location_id, latitude, longitude, accuracy, timestamp) = row?;
ret += &format!( ret += &format!(
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n", "<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n\x00",
timestamp, timestamp,
accuracy, accuracy,
longitude, longitude,
@@ -425,10 +429,10 @@ pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error>
Ok(()) Ok(())
} }
)?; )?;
ret += "</Document>\n</kml>";
} }
ensure!(location_count > 0, "No locations processed"); ensure!(location_count > 0, "No locations processed");
ret += "</Document>\n</kml>";
Ok((ret, last_added_location_id)) Ok((ret, last_added_location_id))
} }
@@ -472,16 +476,12 @@ pub fn set_kml_sent_timestamp(
Ok(()) Ok(())
} }
pub fn set_msg_location_id( pub fn set_msg_location_id(context: &Context, msg_id: u32, location_id: u32) -> Result<(), Error> {
context: &Context,
msg_id: MsgId,
location_id: u32,
) -> Result<(), Error> {
sql::execute( sql::execute(
context, context,
&context.sql, &context.sql,
"UPDATE msgs SET location_id=? WHERE id=?;", "UPDATE msgs SET location_id=? WHERE id=?;",
params![location_id, msg_id], params![location_id, msg_id as i32],
)?; )?;
Ok(()) Ok(())
@@ -492,59 +492,55 @@ pub fn save(
chat_id: u32, chat_id: u32,
contact_id: u32, contact_id: u32,
locations: &[Location], locations: &[Location],
independent: bool, independent: i32,
) -> Result<u32, Error> { ) -> Result<u32, Error> {
ensure!(chat_id > DC_CHAT_ID_LAST_SPECIAL, "Invalid chat id"); ensure!(chat_id > 9, "Invalid chat id");
context context.sql.prepare2(
.sql "SELECT id FROM locations WHERE timestamp=? AND from_id=?",
.prepare2( "INSERT INTO locations\
"SELECT id FROM locations WHERE timestamp=? AND from_id=?", (timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
"INSERT INTO locations\ VALUES (?,?,?,?,?,?,?);",
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \ |mut stmt_test, mut stmt_insert, conn| {
VALUES (?,?,?,?,?,?,?);", let mut newest_timestamp = 0;
|mut stmt_test, mut stmt_insert, conn| { let mut newest_location_id = 0;
let mut newest_timestamp = 0;
let mut newest_location_id = 0;
for location in locations { for location in locations {
let exists = let exists = stmt_test.exists(params![location.timestamp, contact_id as i32])?;
stmt_test.exists(params![location.timestamp, contact_id as i32])?;
if independent || !exists { if 0 != independent || !exists {
stmt_insert.execute(params![ 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, location.timestamp,
"from_id",
contact_id as i32, 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) }
}, Ok(newest_location_id)
) },
.map_err(Into::into) )
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn JobMaybeSendLocations(context: &Context, _job: &Job) { pub fn job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context: &Context, _job: &Job) {
let now = time(); let now = time();
let mut continue_streaming = false; let mut continue_streaming: libc::c_int = 1;
info!( info!(
context, context,
" ----------------- MAYBE_SEND_LOCATIONS -------------- ", " ----------------- MAYBE_SEND_LOCATIONS -------------- ",
@@ -559,7 +555,7 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) {
let chat_id: i32 = row.get(0)?; let chat_id: i32 = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?; let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?; let locations_last_sent: i64 = row.get(2)?;
continue_streaming = true; continue_streaming = 1;
// be a bit tolerant as the timer may not align exactly with time(NULL) // be a bit tolerant as the timer may not align exactly with time(NULL)
if now - locations_last_sent < (60 - 3) { if now - locations_last_sent < (60 - 3) {
@@ -589,11 +585,7 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) {
.into_iter() .into_iter()
.filter_map(|(chat_id, locations_send_begin, locations_last_sent)| { .filter_map(|(chat_id, locations_send_begin, locations_last_sent)| {
if !stmt_locations if !stmt_locations
.exists(params![ .exists(params![1, locations_send_begin, locations_last_sent,])
DC_CONTACT_ID_SELF,
locations_send_begin,
locations_last_sent,
])
.unwrap_or_default() .unwrap_or_default()
{ {
// if there is no new location, there's nothing to send. // if there is no new location, there's nothing to send.
@@ -609,9 +601,9 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) {
// the easiest way to determine this, is to check for an empty message queue. // the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later // (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok) // and dc_set_location() is typically called periodically, this is ok)
let mut msg = Message::new(Viewtype::Text); let mut msg = dc_msg_new(Viewtype::Text);
msg.hidden = true; msg.hidden = true;
msg.param.set_cmd(SystemMessage::LocationOnly); msg.param.set_int(Param::Cmd, 9);
Some((chat_id, msg)) Some((chat_id, msg))
} }
}) })
@@ -623,16 +615,16 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) {
for (chat_id, mut msg) in msgs.into_iter() { for (chat_id, mut msg) in msgs.into_iter() {
// TODO: better error handling // TODO: better error handling
chat::send_msg(context, chat_id as u32, &mut msg).unwrap_or_default(); chat::send_msg(context, chat_id as u32, &mut msg).unwrap();
} }
} }
if continue_streaming { if 0 != continue_streaming {
schedule_MAYBE_SEND_LOCATIONS(context, true); schedule_MAYBE_SEND_LOCATIONS(context, 0x1);
} }
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) { pub fn job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context: &Context, job: &mut Job) {
// this function is called when location-streaming _might_ have ended for a chat. // this function is called when location-streaming _might_ have ended for a chat.
// the function checks, if location-streaming is really ended; // the function checks, if location-streaming is really ended;
// if so, a device-message is added if not yet done. // if so, a device-message is added if not yet done.
@@ -655,7 +647,7 @@ pub fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) {
params![chat_id as i32], params![chat_id as i32],
).is_ok() { ).is_ok() {
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_info_msg(context, chat_id, stock_str); chat::add_device_msg(context, chat_id, stock_str);
context.call_cb(Event::ChatModified(chat_id)); context.call_cb(Event::ChatModified(chat_id));
} }
} }
@@ -673,9 +665,9 @@ mod tests {
let context = dummy_context(); let context = dummy_context();
let xml = let xml =
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>"; "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";
let kml = Kml::parse(&context.ctx, xml).expect("parsing failed"); let kml = Kml::parse(&context.ctx, &xml).expect("parsing failed");
assert!(kml.addr.is_some()); assert!(kml.addr.is_some());
assert_eq!(kml.addr.as_ref().unwrap(), "user@example.org",); assert_eq!(kml.addr.as_ref().unwrap(), "user@example.org",);

View File

@@ -1,21 +1,12 @@
//! # Logging macros
#[macro_export] #[macro_export]
macro_rules! info { macro_rules! info {
($ctx:expr, $msg:expr) => { ($ctx:expr, $msg:expr) => {
info!($ctx, $msg,) info!($ctx, $msg,)
}; };
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{ ($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
let formatted = format!($msg, $($args),*); let formatted = format!($msg, $($args),*);
let thread = ::std::thread::current(); emit_event!($ctx, $crate::Event::Info(formatted));
let full = format!("{thid:?}/{thname} {file}:{line}: {msg}", };
thid = thread.id(),
thname = thread.name().unwrap_or("unnamed"),
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::Event::Info(full));
}};
} }
#[macro_export] #[macro_export]
@@ -23,17 +14,10 @@ macro_rules! warn {
($ctx:expr, $msg:expr) => { ($ctx:expr, $msg:expr) => {
warn!($ctx, $msg,) warn!($ctx, $msg,)
}; };
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{ ($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
let formatted = format!($msg, $($args),*); let formatted = format!($msg, $($args),*);
let thread = ::std::thread::current(); emit_event!($ctx, $crate::Event::Warning(formatted));
let full = format!("{thid:?}/{thname} {file}:{line}: {msg}", };
thid = thread.id(),
thname = thread.name().unwrap_or("unnamed"),
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::Event::Warning(full));
}};
} }
#[macro_export] #[macro_export]
@@ -41,10 +25,10 @@ macro_rules! error {
($ctx:expr, $msg:expr) => { ($ctx:expr, $msg:expr) => {
error!($ctx, $msg,) error!($ctx, $msg,)
}; };
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{ ($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
let formatted = format!($msg, $($args),*); let formatted = format!($msg, $($args),*);
emit_event!($ctx, $crate::Event::Error(formatted)); emit_event!($ctx, $crate::Event::Error(formatted));
}}; };
} }
#[macro_export] #[macro_export]

View File

@@ -1,29 +1,8 @@
//! # Login parameters
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt; use std::fmt;
use crate::context::Context; use crate::context::Context;
use crate::error::Error;
#[derive(Copy, Clone, Debug, Display, FromPrimitive)]
#[repr(i32)]
#[strum(serialize_all = "snake_case")]
pub enum CertificateChecks {
Automatic = 0,
Strict = 1,
/// Same as AcceptInvalidCertificates
/// Previously known as AcceptInvalidHostnames, now deprecated.
AcceptInvalidCertificates2 = 2,
AcceptInvalidCertificates = 3,
}
impl Default for CertificateChecks {
fn default() -> Self {
Self::Automatic
}
}
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct LoginParam { pub struct LoginParam {
@@ -32,14 +11,10 @@ pub struct LoginParam {
pub mail_user: String, pub mail_user: String,
pub mail_pw: String, pub mail_pw: String,
pub mail_port: i32, pub mail_port: i32,
/// IMAP TLS options: whether to allow invalid certificates and/or invalid hostnames
pub imap_certificate_checks: CertificateChecks,
pub send_server: String, pub send_server: String,
pub send_user: String, pub send_user: String,
pub send_pw: String, pub send_pw: String,
pub send_port: i32, pub send_port: i32,
/// SMTP TLS options: whether to allow invalid certificates and/or invalid hostnames
pub smtp_certificate_checks: CertificateChecks,
pub server_flags: i32, pub server_flags: i32,
} }
@@ -56,66 +31,48 @@ impl LoginParam {
let key = format!("{}addr", prefix); let key = format!("{}addr", prefix);
let addr = sql let addr = sql
.get_raw_config(context, key) .get_config(context, key)
.unwrap_or_default() .unwrap_or_default()
.trim() .trim()
.to_string(); .to_string();
let key = format!("{}mail_server", prefix); let key = format!("{}mail_server", prefix);
let mail_server = sql.get_raw_config(context, key).unwrap_or_default(); let mail_server = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}mail_port", prefix); let key = format!("{}mail_port", prefix);
let mail_port = sql.get_raw_config_int(context, key).unwrap_or_default(); let mail_port = sql.get_config_int(context, key).unwrap_or_default();
let key = format!("{}mail_user", prefix); let key = format!("{}mail_user", prefix);
let mail_user = sql.get_raw_config(context, key).unwrap_or_default(); let mail_user = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}mail_pw", prefix); let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_raw_config(context, key).unwrap_or_default(); let mail_pw = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}imap_certificate_checks", prefix);
let imap_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key) {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else {
Default::default()
};
let key = format!("{}send_server", prefix); let key = format!("{}send_server", prefix);
let send_server = sql.get_raw_config(context, key).unwrap_or_default(); let send_server = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}send_port", prefix); let key = format!("{}send_port", prefix);
let send_port = sql.get_raw_config_int(context, key).unwrap_or_default(); let send_port = sql.get_config_int(context, key).unwrap_or_default();
let key = format!("{}send_user", prefix); let key = format!("{}send_user", prefix);
let send_user = sql.get_raw_config(context, key).unwrap_or_default(); let send_user = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}send_pw", prefix); let key = format!("{}send_pw", prefix);
let send_pw = sql.get_raw_config(context, key).unwrap_or_default(); let send_pw = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}smtp_certificate_checks", prefix);
let smtp_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key) {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else {
Default::default()
};
let key = format!("{}server_flags", prefix); let key = format!("{}server_flags", prefix);
let server_flags = sql.get_raw_config_int(context, key).unwrap_or_default(); let server_flags = sql.get_config_int(context, key).unwrap_or_default();
LoginParam { LoginParam {
addr, addr: addr.to_string(),
mail_server, mail_server,
mail_user, mail_user,
mail_pw, mail_pw,
mail_port, mail_port,
imap_certificate_checks,
send_server, send_server,
send_user, send_user,
send_pw, send_pw,
send_port, send_port,
smtp_certificate_checks,
server_flags, server_flags,
} }
} }
@@ -129,45 +86,39 @@ impl LoginParam {
&self, &self,
context: &Context, context: &Context,
prefix: impl AsRef<str>, prefix: impl AsRef<str>,
) -> crate::sql::Result<()> { ) -> Result<(), Error> {
let prefix = prefix.as_ref(); let prefix = prefix.as_ref();
let sql = &context.sql; let sql = &context.sql;
let key = format!("{}addr", prefix); let key = format!("{}addr", prefix);
sql.set_raw_config(context, key, Some(&self.addr))?; sql.set_config(context, key, Some(&self.addr))?;
let key = format!("{}mail_server", prefix); let key = format!("{}mail_server", prefix);
sql.set_raw_config(context, key, Some(&self.mail_server))?; sql.set_config(context, key, Some(&self.mail_server))?;
let key = format!("{}mail_port", prefix); let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(context, key, self.mail_port)?; sql.set_config_int(context, key, self.mail_port)?;
let key = format!("{}mail_user", prefix); let key = format!("{}mail_user", prefix);
sql.set_raw_config(context, key, Some(&self.mail_user))?; sql.set_config(context, key, Some(&self.mail_user))?;
let key = format!("{}mail_pw", prefix); let key = format!("{}mail_pw", prefix);
sql.set_raw_config(context, key, Some(&self.mail_pw))?; sql.set_config(context, key, Some(&self.mail_pw))?;
let key = format!("{}imap_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.imap_certificate_checks as i32)?;
let key = format!("{}send_server", prefix); let key = format!("{}send_server", prefix);
sql.set_raw_config(context, key, Some(&self.send_server))?; sql.set_config(context, key, Some(&self.send_server))?;
let key = format!("{}send_port", prefix); let key = format!("{}send_port", prefix);
sql.set_raw_config_int(context, key, self.send_port)?; sql.set_config_int(context, key, self.send_port)?;
let key = format!("{}send_user", prefix); let key = format!("{}send_user", prefix);
sql.set_raw_config(context, key, Some(&self.send_user))?; sql.set_config(context, key, Some(&self.send_user))?;
let key = format!("{}send_pw", prefix); let key = format!("{}send_pw", prefix);
sql.set_raw_config(context, key, Some(&self.send_pw))?; sql.set_config(context, key, Some(&self.send_pw))?;
let key = format!("{}smtp_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.smtp_certificate_checks as i32)?;
let key = format!("{}server_flags", prefix); let key = format!("{}server_flags", prefix);
sql.set_raw_config_int(context, key, self.server_flags)?; sql.set_config_int(context, key, self.server_flags)?;
Ok(()) Ok(())
} }
@@ -182,24 +133,21 @@ impl fmt::Display for LoginParam {
write!( write!(
f, f,
"{} imap:{}:{}:{}:{}:cert_{} smtp:{}:{}:{}:{}:cert_{} {}", "{} {}:{}:{}:{} {}:{}:{}:{} {}",
unset_empty(&self.addr), unset_empty(&self.addr),
unset_empty(&self.mail_user), unset_empty(&self.mail_user),
if !self.mail_pw.is_empty() { pw } else { unset }, if !self.mail_pw.is_empty() { pw } else { unset },
unset_empty(&self.mail_server), unset_empty(&self.mail_server),
self.mail_port, self.mail_port,
self.imap_certificate_checks,
unset_empty(&self.send_user), unset_empty(&self.send_user),
if !self.send_pw.is_empty() { pw } else { unset }, if !self.send_pw.is_empty() { pw } else { unset },
unset_empty(&self.send_server), unset_empty(&self.send_server),
self.send_port, self.send_port,
self.smtp_certificate_checks,
flags_readable, flags_readable,
) )
} }
} }
#[allow(clippy::ptr_arg)]
fn unset_empty(s: &String) -> Cow<String> { fn unset_empty(s: &String) -> Cow<String> {
if s.is_empty() { if s.is_empty() {
Cow::Owned("unset".to_string()) Cow::Owned("unset".to_string())
@@ -208,45 +156,44 @@ fn unset_empty(s: &String) -> Cow<String> {
} }
} }
#[allow(clippy::useless_let_if_seq)]
fn get_readable_flags(flags: i32) -> String { fn get_readable_flags(flags: i32) -> String {
let mut res = String::new(); let mut res = String::new();
for bit in 0..31 { for bit in 0..31 {
if 0 != flags & 1 << bit { if 0 != flags & 1 << bit {
let mut flag_added = false; let mut flag_added = 0;
if 1 << bit == 0x2 { if 1 << bit == 0x2 {
res += "OAUTH2 "; res += "OAUTH2 ";
flag_added = true; flag_added = 1;
} }
if 1 << bit == 0x4 { if 1 << bit == 0x4 {
res += "AUTH_NORMAL "; res += "AUTH_NORMAL ";
flag_added = true; flag_added = 1;
} }
if 1 << bit == 0x100 { if 1 << bit == 0x100 {
res += "IMAP_STARTTLS "; res += "IMAP_STARTTLS ";
flag_added = true; flag_added = 1;
} }
if 1 << bit == 0x200 { if 1 << bit == 0x200 {
res += "IMAP_SSL "; res += "IMAP_SSL ";
flag_added = true; flag_added = 1;
} }
if 1 << bit == 0x400 { if 1 << bit == 0x400 {
res += "IMAP_PLAIN "; res += "IMAP_PLAIN ";
flag_added = true; flag_added = 1;
} }
if 1 << bit == 0x10000 { if 1 << bit == 0x10000 {
res += "SMTP_STARTTLS "; res += "SMTP_STARTTLS ";
flag_added = true; flag_added = 1
} }
if 1 << bit == 0x20000 { if 1 << bit == 0x20000 {
res += "SMTP_SSL "; res += "SMTP_SSL ";
flag_added = true; flag_added = 1
} }
if 1 << bit == 0x40000 { if 1 << bit == 0x40000 {
res += "SMTP_PLAIN "; res += "SMTP_PLAIN ";
flag_added = true; flag_added = 1
} }
if flag_added { if 0 == flag_added {
res += &format!("{:#0x}", 1 << bit); res += &format!("{:#0x}", 1 << bit);
} }
} }
@@ -257,39 +204,3 @@ fn get_readable_flags(flags: i32) -> String {
res res
} }
pub fn dc_build_tls(
certificate_checks: CertificateChecks,
) -> Result<native_tls::TlsConnector, native_tls::Error> {
let mut tls_builder = native_tls::TlsConnector::builder();
match certificate_checks {
CertificateChecks::Automatic => {
// Same as AcceptInvalidCertificates for now.
// TODO: use provider database when it becomes available
tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
}
CertificateChecks::Strict => &mut tls_builder,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true),
}
.build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_certificate_checks_display() {
use std::string::ToString;
assert_eq!(
"accept_invalid_certificates".to_string(),
CertificateChecks::AcceptInvalidCertificates.to_string()
);
}
}

View File

@@ -5,7 +5,7 @@ use deltachat_derive::{FromSql, ToSql};
/// Lot objects are created /// Lot objects are created
/// eg. by chatlist.get_summary() or dc_msg_get_summary(). /// eg. by chatlist.get_summary() or dc_msg_get_summary().
/// ///
/// *Lot* is used in the meaning *heap* here. /// _Lot_ is used in the meaning _heap_ here.
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
pub struct Lot { pub struct Lot {
pub(crate) text1_meaning: Meaning, pub(crate) text1_meaning: Meaning,

Some files were not shown because too many files have changed in this diff Show More