Compare commits
1 Commits
1.0.0-beta
...
fix/setupc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
601b284ed8 |
@@ -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:
|
||||||
|
|||||||
47
.github/workflows/code-quality.yml
vendored
@@ -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
@@ -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
|
|
||||||
|
|||||||
276
CHANGELOG.md
@@ -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
47
Cargo.toml
@@ -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"
|
||||||
|
|||||||
15
README.md
@@ -1,9 +1,11 @@
|
|||||||
# Delta Chat Rust
|
# Delta Chat Rust
|
||||||
|
|
||||||
> Deltachat-core written in Rust
|
> Project porting deltachat-core to rust
|
||||||
|
|
||||||
[![CircleCI build status][circle-shield]][circle] [![Appveyor build status][appveyor-shield]][appveyor]
|
[![CircleCI build status][circle-shield]][circle] [![Appveyor build status][appveyor-shield]][appveyor]
|
||||||
|
|
||||||
|
Current commit on deltachat/deltachat-core: `12ef73c8e76185f9b78e844ea673025f56a959ab`.
|
||||||
|
|
||||||
## Installing Rust and Cargo
|
## Installing Rust and Cargo
|
||||||
|
|
||||||
To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment:
|
To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment:
|
||||||
@@ -14,7 +16,7 @@ curl https://sh.rustup.rs -sSf | sh
|
|||||||
|
|
||||||
## Using the CLI client
|
## Using the CLI client
|
||||||
|
|
||||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
Compile and run Delta Chat Core using `cargo`:
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo run --example repl -- /path/to/db
|
cargo run --example repl -- /path/to/db
|
||||||
@@ -87,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
@@ -0,0 +1,6 @@
|
|||||||
|
[dependencies.std]
|
||||||
|
features = ["panic-unwind"]
|
||||||
|
|
||||||
|
# if using `cargo test`
|
||||||
|
[dependencies.test]
|
||||||
|
stage = 1
|
||||||
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.8 KiB |
@@ -1,83 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
inkscape:export-ydpi="409.60001"
|
|
||||||
inkscape:export-xdpi="409.60001"
|
|
||||||
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-device.png"
|
|
||||||
version="1.0"
|
|
||||||
width="60"
|
|
||||||
height="60"
|
|
||||||
viewBox="0 0 45 45"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
id="svg4344"
|
|
||||||
sodipodi:docname="icon-device.svg"
|
|
||||||
inkscape:version="1.0beta1 (32d4812, 2019-09-19)">
|
|
||||||
<defs
|
|
||||||
id="defs4348" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
inkscape:snap-global="false"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1600"
|
|
||||||
inkscape:window-height="1035"
|
|
||||||
id="namedview4346"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:zoom="3.959798"
|
|
||||||
inkscape:cx="28.322498"
|
|
||||||
inkscape:cy="24.898474"
|
|
||||||
inkscape:window-x="45"
|
|
||||||
inkscape:window-y="23"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg4344" />
|
|
||||||
<metadata
|
|
||||||
id="metadata4336">
|
|
||||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<rect
|
|
||||||
y="-4.4408921e-16"
|
|
||||||
x="0"
|
|
||||||
height="45"
|
|
||||||
width="45"
|
|
||||||
id="rect860"
|
|
||||||
style="opacity:1;fill:#76868b;fill-opacity:1;stroke-width:0.819271" />
|
|
||||||
<g
|
|
||||||
fill="#000000"
|
|
||||||
stroke="none"
|
|
||||||
style="fill:#ffffff;fill-opacity:1"
|
|
||||||
transform="matrix(0.00255113,0,0,-0.00255113,5.586152,38.200477)"
|
|
||||||
id="g4342">
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1"
|
|
||||||
d="m 8175,12765 c -703,-114 -1248,-608 -1387,-1258 -17,-82 -21,-136 -22,-277 0,-202 15,-307 70,-470 149,-446 499,-733 1009,-828 142,-26 465,-23 619,6 691,131 1201,609 1328,1244 31,158 31,417 0,565 -114,533 -482,889 -1038,1004 -133,27 -448,35 -579,14 z"
|
|
||||||
id="path4338"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1"
|
|
||||||
d="m 7070,9203 c -212,-20 -275,-27 -397,-48 -691,-117 -1400,-444 -2038,-940 -182,-142 -328,-270 -585,-517 -595,-571 -911,-974 -927,-1181 -6,-76 11,-120 69,-184 75,-80 159,-108 245,-79 109,37 263,181 632,595 539,606 774,826 1035,969 135,75 231,105 341,106 82,1 94,-2 138,-27 116,-68 161,-209 122,-376 -9,-36 -349,-868 -757,-1850 -407,-982 -785,-1892 -838,-2021 -287,-694 -513,-1389 -615,-1889 -70,-342 -90,-683 -52,-874 88,-440 381,-703 882,-792 124,-23 401,-30 562,-16 783,69 1674,461 2561,1125 796,596 1492,1354 1607,1751 43,146 -33,308 -168,360 -61,23 -100,15 -173,-36 -105,-74 -202,-170 -539,-529 -515,-551 -762,-783 -982,-927 -251,-164 -437,-186 -543,-65 -56,64 -74,131 -67,247 13,179 91,434 249,815 135,324 1588,4102 1646,4280 106,325 151,561 159,826 9,281 -22,463 -112,652 -58,122 -114,199 -211,292 -245,233 -582,343 -1044,338 -91,-1 -181,-3 -200,-5 z"
|
|
||||||
id="path4340"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,71 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
inkscape:export-ydpi="409.60001"
|
|
||||||
inkscape:export-xdpi="409.60001"
|
|
||||||
inkscape:export-filename="/home/kerle/test-icon.png"
|
|
||||||
version="1.0"
|
|
||||||
width="60"
|
|
||||||
height="60"
|
|
||||||
viewBox="0 0 45 45"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
id="svg4344"
|
|
||||||
sodipodi:docname="icon-saved-messages.svg"
|
|
||||||
inkscape:version="1.0beta1 (32d4812, 2019-09-19)">
|
|
||||||
<defs
|
|
||||||
id="defs4348" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1395"
|
|
||||||
inkscape:window-height="855"
|
|
||||||
id="namedview4346"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:zoom="4"
|
|
||||||
inkscape:cx="29.308676"
|
|
||||||
inkscape:cy="49.03624"
|
|
||||||
inkscape:window-x="89"
|
|
||||||
inkscape:window-y="108"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg4344"
|
|
||||||
inkscape:lockguides="false" />
|
|
||||||
<metadata
|
|
||||||
id="metadata4336">
|
|
||||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<rect
|
|
||||||
y="0"
|
|
||||||
x="0"
|
|
||||||
height="45"
|
|
||||||
width="45"
|
|
||||||
id="rect1420"
|
|
||||||
style="fill:#87aade;fill-opacity:1;stroke:none;stroke-width:0.968078" />
|
|
||||||
<path
|
|
||||||
id="rect846"
|
|
||||||
style="fill:#ffffff;stroke-width:0.58409804"
|
|
||||||
d="M 13.5,7.5 V 39 h 0.08654 L 22.533801,29.370239 31.482419,39 h 0.01758 V 7.5 Z m 9.004056,4.108698 1.879508,4.876388 5.039514,0.359779 -3.879358,3.363728 1.227764,5.095749 -4.276893,-2.796643 -4.280949,2.788618 1.237229,-5.093073 -3.873949,-3.371754 5.040866,-0.350417 z"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 113 KiB |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
#export RUST_TEST_THREADS=1
|
export RUST_TEST_THREADS=1
|
||||||
export RUST_BACKTRACE=1
|
export RUST_BACKTRACE=1
|
||||||
export RUSTFLAGS='--deny warnings'
|
export RUSTFLAGS='--deny warnings'
|
||||||
export OPT="--target=$TARGET"
|
export OPT="--target=$TARGET"
|
||||||
@@ -31,15 +31,16 @@ fi
|
|||||||
if [[ $NORUN == "1" ]]; then
|
if [[ $NORUN == "1" ]]; then
|
||||||
export CARGO_SUBCMD="build"
|
export CARGO_SUBCMD="build"
|
||||||
else
|
else
|
||||||
export CARGO_SUBCMD="test --all"
|
export CARGO_SUBCMD="test"
|
||||||
export OPT="${OPT} "
|
export OPT="${OPT} "
|
||||||
export OPT_RELEASE="${OPT_RELEASE} "
|
export OPT_RELEASE="${OPT_RELEASE} "
|
||||||
export OPT_RELEASE_IGNORED="${OPT_RELEASE} -- --ignored"
|
export OPT_RELEASE_IGNORED="${OPT_RELEASE} -- --ignored"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run all the test configurations
|
# Run all the test configurations:
|
||||||
# RUSTC_WRAPPER=SCCACHE seems to destroy parallelism / prolong the test
|
|
||||||
unset RUSTC_WRAPPER
|
|
||||||
$CARGO_CMD $CARGO_SUBCMD $OPT
|
$CARGO_CMD $CARGO_SUBCMD $OPT
|
||||||
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE
|
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE
|
||||||
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE_IGNORED
|
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE_IGNORED
|
||||||
|
|
||||||
|
# Build the ffi lib
|
||||||
|
$CARGO_CMD $CARGO_SUBCMD $OPT_FFI_RELEASE
|
||||||
@@ -1,46 +1,52 @@
|
|||||||
|
|
||||||
# Continuous Integration Scripts for Delta Chat
|
# Continuous Integration Scripts for Delta Chat
|
||||||
|
|
||||||
Continuous Integration, run through CircleCI and an own build machine.
|
Continuous Integration is run through CircleCI
|
||||||
|
but is largely independent of it.
|
||||||
## Description of scripts
|
|
||||||
|
|
||||||
- `../.circleci/config.yml` describing the build jobs that are run
|
|
||||||
by Circle-CI
|
|
||||||
|
|
||||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
|
||||||
`run-python-test.sh` remotely on the build machine.
|
|
||||||
|
|
||||||
- `remote_tests_rust.sh` rsyncs to the build machine and runs
|
|
||||||
`run-rust-test.sh` remotely on the build machine.
|
|
||||||
|
|
||||||
- `doxygen/Dockerfile` specifies an image that contains
|
|
||||||
the doxygen tool which is used by `run-doxygen.sh`
|
|
||||||
to generate C-docs which are then uploaded
|
|
||||||
via `ci_upload.sh` to `https://c.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
|
|
||||||
(and the master branch is linked to https://c.delta.chat proper).
|
|
||||||
|
|
||||||
|
|
||||||
## Triggering runs on the build machine locally (fast!)
|
## Generating docker containers for performing build step work
|
||||||
|
|
||||||
There is experimental support for triggering a remote Python or Rust test run
|
All tests, docs and wheel building is run in docker containers:
|
||||||
from your local checkout/branch. You will need to be authorized to login to
|
|
||||||
the build machine (ask your friendly sysadmin on #deltachat freenode) to type::
|
|
||||||
|
|
||||||
ci_scripts/manual_remote_tests.sh rust
|
- **coredeps/Dockerfile** specifies an image that contains all
|
||||||
ci_scripts/manual_remote_tests.sh python
|
of Delta Chat's core dependencies as linkable libraries.
|
||||||
|
It also serves to run python tests and build wheels
|
||||||
|
(binary packages for Python).
|
||||||
|
|
||||||
This will **rsync** your current checkout to the remote build machine
|
- **doxygen/Dockerfile** specifies an image that contains
|
||||||
(no need to commit before) and then run either rust or python tests.
|
the doxygen tool which is used to generate C-docs.
|
||||||
|
|
||||||
# Outdated files (for later re-use)
|
To run tests locally you can pull existing images from "docker.io",
|
||||||
|
the hub for sharing Docker images::
|
||||||
|
|
||||||
`coredeps/Dockerfile` specifies an image that contains all
|
docker pull deltachat/coredeps
|
||||||
of Delta Chat's core dependencies. It used to run
|
docker pull deltachat/doxygen
|
||||||
python tests and build wheels (binary packages for Python)
|
|
||||||
|
|
||||||
You can build the docker images yourself locally
|
or you can build the docker images yourself locally
|
||||||
to avoid the relatively large download::
|
to avoid the relatively large download::
|
||||||
|
|
||||||
cd ci_scripts # where all CI things are
|
cd ci_scripts # where all CI things are
|
||||||
docker build -t deltachat/coredeps docker-coredeps
|
docker build -t deltachat/coredeps docker-coredeps
|
||||||
docker build -t deltachat/doxygen docker-doxygen
|
docker build -t deltachat/doxygen docker-doxygen
|
||||||
|
|
||||||
|
## ci_run.sh (main entrypoint called by circle-ci)
|
||||||
|
|
||||||
|
Once you have the docker images available
|
||||||
|
you can run python testing, documentation generation
|
||||||
|
and building binary wheels::
|
||||||
|
|
||||||
|
sh DOCS=1 TESTS=1 ci_scripts/ci_run.sh
|
||||||
|
|
||||||
|
## ci_upload.sh (uploading artifacts on success)
|
||||||
|
|
||||||
|
- python docs to `https://py.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
|
||||||
|
|
||||||
|
- doxygen docs to `https://c.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
|
||||||
|
|
||||||
|
- python wheels to `https://m.devpi.net/dc/<BRANCH>`
|
||||||
|
so that you install fully self-contained wheels like this:
|
||||||
|
`pip install -U -i https://m.devpi.net/dc/<BRANCH> deltachat`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
22
ci_scripts/ci_run.sh
Executable file
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
|||||||
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
||||||
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
||||||
|
|
||||||
|
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||||
|
|
||||||
|
# Install python tools (auditwheels,tox, ...)
|
||||||
|
ADD deps/build_python.sh /builder/build_python.sh
|
||||||
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
|
# Install Rust nightly
|
||||||
|
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||||
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
# Install a recent Perl, needed to install OpenSSL
|
# Install a recent Perl, needed to install OpenSSL
|
||||||
ADD deps/build_perl.sh /builder/build_perl.sh
|
ADD deps/build_perl.sh /builder/build_perl.sh
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||||
@@ -13,12 +23,3 @@ RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
|||||||
ADD deps/build_openssl.sh /builder/build_openssl.sh
|
ADD deps/build_openssl.sh /builder/build_openssl.sh
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_openssl.sh && cd .. && rm -r tmp1
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_openssl.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
|
||||||
|
|
||||||
# Install python tools (auditwheels,tox, ...)
|
|
||||||
ADD deps/build_python.sh /builder/build_python.sh
|
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
|
||||||
|
|
||||||
# Install Rust nightly
|
|
||||||
ADD deps/build_rust.sh /builder/build_rust.sh
|
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
PERL_VERSION=5.30.0
|
PERL_VERSION=5.28.0
|
||||||
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
||||||
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||||
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||||
tar -xzf perl-${PERL_VERSION}.tar.gz
|
tar xzf perl-${PERL_VERSION}.tar.gz
|
||||||
cd perl-${PERL_VERSION}
|
cd perl-${PERL_VERSION}
|
||||||
|
|
||||||
./Configure -de
|
./Configure -de
|
||||||
make
|
make
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -e -x
|
set -e -x
|
||||||
|
|
||||||
# Install Rust
|
# Install Rust
|
||||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-09-12 -y
|
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-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/
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -xe
|
|
||||||
export CIRCLE_JOB=remote_tests_${1:?need to specify 'rust' or 'python'}
|
|
||||||
export CIRCLE_BUILD_NUM=$USER
|
|
||||||
export CIRCLE_BRANCH=`git branch | grep \* | cut -d ' ' -f2`
|
|
||||||
export CIRCLE_PROJECT_REPONAME=$(basename `git rev-parse --show-toplevel`)
|
|
||||||
|
|
||||||
time bash ci_scripts/$CIRCLE_JOB.sh
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -Dwarnings
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_and_test:
|
|
||||||
name: Build and test
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
|
||||||
rust: [nightly]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- name: Install ${{ matrix.rust }}
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: check
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
if: matrix.rust == 'nightly'
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
args: --all --bins --examples --tests
|
|
||||||
|
|
||||||
- name: tests
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all
|
|
||||||
|
|
||||||
- name: tests ignored
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all --release -- --ignored
|
|
||||||
|
|
||||||
check_fmt:
|
|
||||||
name: Checking fmt and docs
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: nightly
|
|
||||||
override: true
|
|
||||||
components: rustfmt
|
|
||||||
|
|
||||||
- name: fmt
|
|
||||||
run: cargo fmt --all -- --check
|
|
||||||
|
|
||||||
# clippy_check:
|
|
||||||
# name: Clippy check
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
#
|
|
||||||
# steps:
|
|
||||||
# - uses: actions/checkout@v1
|
|
||||||
# - uses: actions-rs/toolchain@v1
|
|
||||||
# with:
|
|
||||||
# profile: minimal
|
|
||||||
# toolchain: nightly
|
|
||||||
# override: true
|
|
||||||
# components: clippy
|
|
||||||
#
|
|
||||||
# - name: clippy
|
|
||||||
# run: cargo clippy --all
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
|
||||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
|
||||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
|
||||||
|
|
||||||
# we construct the BUILDDIR such that we can easily share the
|
|
||||||
# CARGO_TARGET_DIR between runs ("..")
|
|
||||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
|
||||||
|
|
||||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
|
||||||
|
|
||||||
set -xe
|
|
||||||
|
|
||||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
|
||||||
git ls-files >.rsynclist
|
|
||||||
# we seem to need .git for setuptools_scm versioning
|
|
||||||
find .git >>.rsynclist
|
|
||||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
|
||||||
|
|
||||||
set +x
|
|
||||||
|
|
||||||
echo "--- Running $CIRCLE_JOB remotely"
|
|
||||||
|
|
||||||
ssh $SSHTARGET <<_HERE
|
|
||||||
set +x -e
|
|
||||||
cd $BUILDDIR
|
|
||||||
# let's share the target dir with our last run on this branch/job-type
|
|
||||||
# cargo will make sure to block/unblock us properly
|
|
||||||
export CARGO_TARGET_DIR=\`pwd\`/../target
|
|
||||||
export TARGET=release
|
|
||||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
|
||||||
|
|
||||||
#we rely on tox/virtualenv being available in the host
|
|
||||||
#rm -rf virtualenv venv
|
|
||||||
#virtualenv -q -p python3.7 venv
|
|
||||||
#source venv/bin/activate
|
|
||||||
#pip install -q tox virtualenv
|
|
||||||
|
|
||||||
set -x
|
|
||||||
which python
|
|
||||||
source \$HOME/venv/bin/activate
|
|
||||||
which python
|
|
||||||
|
|
||||||
bash ci_scripts/run-python-test.sh
|
|
||||||
_HERE
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
|
||||||
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
|
||||||
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
|
||||||
|
|
||||||
# we construct the BUILDDIR such that we can easily share the
|
|
||||||
# CARGO_TARGET_DIR between runs ("..")
|
|
||||||
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
|
||||||
|
|
||||||
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
|
||||||
git ls-files >.rsynclist
|
|
||||||
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
|
||||||
|
|
||||||
echo "--- Running $CIRCLE_JOB remotely"
|
|
||||||
|
|
||||||
ssh $SSHTARGET <<_HERE
|
|
||||||
set +x -e
|
|
||||||
cd $BUILDDIR
|
|
||||||
# let's share the target dir with our last run on this branch/job-type
|
|
||||||
# cargo will make sure to block/unblock us properly
|
|
||||||
export CARGO_TARGET_DIR=\`pwd\`/../target
|
|
||||||
export TARGET=x86_64-unknown-linux-gnu
|
|
||||||
export RUSTC_WRAPPER=sccache
|
|
||||||
|
|
||||||
bash ci_scripts/run-rust-test.sh
|
|
||||||
_HERE
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
cd deltachat-ffi
|
|
||||||
doxygen
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Run functional tests for Delta Chat core using the python bindings
|
|
||||||
# and tox/pytest.
|
|
||||||
|
|
||||||
set -e -x
|
|
||||||
|
|
||||||
# for core-building and python install step
|
|
||||||
export DCC_RS_TARGET=release
|
|
||||||
export DCC_RS_DEV=`pwd`
|
|
||||||
|
|
||||||
cd python
|
|
||||||
|
|
||||||
python install_python_bindings.py onlybuild
|
|
||||||
|
|
||||||
# remove and inhibit writing PYC files
|
|
||||||
rm -rf tests/__pycache__
|
|
||||||
rm -rf src/deltachat/__pycache__
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
|
||||||
|
|
||||||
# run python tests (tox invokes pytest to run tests in python/tests)
|
|
||||||
#TOX_PARALLEL_NO_SPINNER=1 tox -e lint,doc
|
|
||||||
tox -e lint
|
|
||||||
tox -e doc,py37
|
|
||||||
@@ -36,25 +36,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
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
extern crate deltachat_provider_database;
|
|
||||||
|
|
||||||
use std::ptr;
|
|
||||||
|
|
||||||
use crate::string::{to_string_lossy, StrExt};
|
|
||||||
use deltachat_provider_database::StatusState;
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub type dc_provider_t = deltachat_provider_database::Provider;
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_new_from_domain(
|
|
||||||
domain: *const libc::c_char,
|
|
||||||
) -> *const dc_provider_t {
|
|
||||||
match deltachat_provider_database::get_provider_info(&to_string_lossy(domain)) {
|
|
||||||
Some(provider) => provider,
|
|
||||||
None => ptr::null(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_new_from_email(
|
|
||||||
email: *const libc::c_char,
|
|
||||||
) -> *const dc_provider_t {
|
|
||||||
let email = to_string_lossy(email);
|
|
||||||
let domain = deltachat_provider_database::get_domain_from_email(&email);
|
|
||||||
match deltachat_provider_database::get_provider_info(domain) {
|
|
||||||
Some(provider) => provider,
|
|
||||||
None => ptr::null(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! null_guard {
|
|
||||||
($context:tt) => {
|
|
||||||
if $context.is_null() {
|
|
||||||
return ptr::null_mut() as *mut libc::c_char;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_overview_page(
|
|
||||||
provider: *const dc_provider_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
format!(
|
|
||||||
"{}/{}",
|
|
||||||
deltachat_provider_database::PROVIDER_OVERVIEW_URL,
|
|
||||||
(*provider).overview_page
|
|
||||||
)
|
|
||||||
.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_name(provider: *const dc_provider_t) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
(*provider).name.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_markdown(
|
|
||||||
provider: *const dc_provider_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
(*provider).markdown.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_status_date(
|
|
||||||
provider: *const dc_provider_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
null_guard!(provider);
|
|
||||||
(*provider).status.date.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> u32 {
|
|
||||||
if provider.is_null() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
match (*provider).status.state {
|
|
||||||
StatusState::OK => 1,
|
|
||||||
StatusState::PREPARATION => 2,
|
|
||||||
StatusState::BROKEN => 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {
|
|
||||||
()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO expose general provider overview url?
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
use failure::Fail;
|
|
||||||
use std::ffi::{CStr, CString};
|
|
||||||
|
|
||||||
/// Duplicates a string
|
|
||||||
///
|
|
||||||
/// returns an empty string if NULL is given, never returns NULL (exits on errors)
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust,norun
|
|
||||||
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
|
|
||||||
/// unsafe {
|
|
||||||
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
|
|
||||||
/// let str_a_copy = dc_strdup(str_a);
|
|
||||||
/// assert_eq!(to_string_lossy(str_a_copy), "foobar");
|
|
||||||
/// assert_ne!(str_a, str_a_copy);
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
|
||||||
let ret: *mut libc::c_char;
|
|
||||||
if !s.is_null() {
|
|
||||||
ret = libc::strdup(s);
|
|
||||||
assert!(!ret.is_null());
|
|
||||||
} else {
|
|
||||||
ret = libc::calloc(1, 1) as *mut libc::c_char;
|
|
||||||
assert!(!ret.is_null());
|
|
||||||
}
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error type for the [OsStrExt] trait
|
|
||||||
#[derive(Debug, Fail, PartialEq)]
|
|
||||||
pub enum CStringError {
|
|
||||||
/// The string contains an interior null byte
|
|
||||||
#[fail(display = "String contains an interior null byte")]
|
|
||||||
InteriorNullByte,
|
|
||||||
/// The string is not valid Unicode
|
|
||||||
#[fail(display = "String is not valid unicode")]
|
|
||||||
NotUnicode,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extra convenience methods on [std::ffi::OsStr] to work with `*libc::c_char`.
|
|
||||||
///
|
|
||||||
/// The primary function of this trait is to more easily convert
|
|
||||||
/// [OsStr], [OsString] or [Path] into pointers to C strings. This always
|
|
||||||
/// allocates a new string since it is very common for the source
|
|
||||||
/// string not to have the required terminal null byte.
|
|
||||||
///
|
|
||||||
/// It is implemented for `AsRef<std::ffi::OsStr>>` trait, which
|
|
||||||
/// allows any type which implements this trait to transparently use
|
|
||||||
/// this. This is how the conversion for [Path] works.
|
|
||||||
///
|
|
||||||
/// [OsStr]: std::ffi::OsStr
|
|
||||||
/// [OsString]: std::ffi::OsString
|
|
||||||
/// [Path]: std::path::Path
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use deltachat::dc_tools::{dc_strdup, OsStrExt};
|
|
||||||
/// let path = std::path::Path::new("/some/path");
|
|
||||||
/// let path_c = path.to_c_string().unwrap();
|
|
||||||
/// unsafe {
|
|
||||||
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub trait OsStrExt {
|
|
||||||
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
|
|
||||||
///
|
|
||||||
/// This is useful to convert e.g. a [std::path::Path] to
|
|
||||||
/// [*libc::c_char] by using
|
|
||||||
/// [Path::as_os_str()](std::path::Path::as_os_str) and
|
|
||||||
/// [CStr::as_ptr()](std::ffi::CStr::as_ptr).
|
|
||||||
///
|
|
||||||
/// This returns [CString] and not [&CStr] because not all [OsStr]
|
|
||||||
/// slices end with a null byte, particularly those coming from
|
|
||||||
/// [Path] do not have a null byte and having to handle this as
|
|
||||||
/// the caller would defeat the point of this function.
|
|
||||||
///
|
|
||||||
/// On Windows this requires that the [OsStr] contains valid
|
|
||||||
/// unicode, which should normally be the case for a [Path].
|
|
||||||
///
|
|
||||||
/// [CString]: std::ffi::CString
|
|
||||||
/// [CStr]: std::ffi::CStr
|
|
||||||
/// [OsStr]: std::ffi::OsStr
|
|
||||||
/// [Path]: std::path::Path
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Since a C `*char` is terminated by a NULL byte this conversion
|
|
||||||
/// will fail, when the [OsStr] has an interior null byte. The
|
|
||||||
/// function will return
|
|
||||||
/// `[Err]([CStringError::InteriorNullByte])`. When converting
|
|
||||||
/// from a [Path] it should be safe to
|
|
||||||
/// [`.unwrap()`](std::result::Result::unwrap) this anyway since a
|
|
||||||
/// [Path] should not contain interior null bytes.
|
|
||||||
///
|
|
||||||
/// On windows when the string contains invalid Unicode
|
|
||||||
/// `[Err]([CStringError::NotUnicode])` is returned.
|
|
||||||
fn to_c_string(&self) -> Result<CString, CStringError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsRef<std::ffi::OsStr>> OsStrExt for T {
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn to_c_string(&self) -> Result<CString, CStringError> {
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
CString::new(self.as_ref().as_bytes()).map_err(|err| match err {
|
|
||||||
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn to_c_string(&self) -> Result<CString, CStringError> {
|
|
||||||
os_str_to_c_string_unicode(&self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation for os_str_to_c_string on windows.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn os_str_to_c_string_unicode(
|
|
||||||
os_str: &dyn AsRef<std::ffi::OsStr>,
|
|
||||||
) -> Result<CString, CStringError> {
|
|
||||||
match os_str.as_ref().to_str() {
|
|
||||||
Some(val) => CString::new(val.as_bytes()).map_err(|err| match err {
|
|
||||||
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
|
||||||
}),
|
|
||||||
None => Err(CStringError::NotUnicode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience methods/associated functions for working with [CString]
|
|
||||||
///
|
|
||||||
/// This is helps transitioning from unsafe code.
|
|
||||||
pub trait CStringExt {
|
|
||||||
/// Create a new [CString], yolo style
|
|
||||||
///
|
|
||||||
/// This unwrap the result, panicking when there are embedded NULL
|
|
||||||
/// bytes.
|
|
||||||
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
|
|
||||||
CString::new(t).expect("String contains null byte, can not be CString")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CStringExt for CString {}
|
|
||||||
|
|
||||||
/// Convenience methods to make transitioning from raw C strings easier.
|
|
||||||
///
|
|
||||||
/// To interact with (legacy) C APIs we often need to convert from
|
|
||||||
/// Rust strings to raw C strings. This can be clumsy to do correctly
|
|
||||||
/// and the compiler sometimes allows it in an unsafe way. These
|
|
||||||
/// methods make it more succinct and help you get it right.
|
|
||||||
pub trait StrExt {
|
|
||||||
/// Allocate a new raw C `*char` version of this string.
|
|
||||||
///
|
|
||||||
/// This allocates a new raw C string which must be freed using
|
|
||||||
/// `free`. It takes care of some common pitfalls with using
|
|
||||||
/// [CString.as_ptr].
|
|
||||||
///
|
|
||||||
/// [CString.as_ptr]: std::ffi::CString.as_ptr
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// This function will panic when the original string contains an
|
|
||||||
/// interior null byte as this can not be represented in raw C
|
|
||||||
/// strings.
|
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsRef<str>> StrExt for T {
|
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
|
||||||
let tmp = CString::yolo(self.as_ref());
|
|
||||||
dc_strdup(tmp.as_ptr())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
|
||||||
if s.is_null() {
|
|
||||||
return "".into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let cstr = unsafe { CStr::from_ptr(s) };
|
|
||||||
|
|
||||||
cstr.to_string_lossy().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
|
||||||
if s.is_null() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(to_string_lossy(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a C `*char` pointer to a [std::path::Path] slice.
|
|
||||||
///
|
|
||||||
/// This converts a `*libc::c_char` pointer to a [Path] slice. This
|
|
||||||
/// essentially has to convert the pointer to [std::ffi::OsStr] to do
|
|
||||||
/// so and thus is the inverse of [OsStrExt::to_c_string]. Just like
|
|
||||||
/// [OsStrExt::to_c_string] requires valid Unicode on Windows, this
|
|
||||||
/// requires that the pointer contains valid UTF-8 on Windows.
|
|
||||||
///
|
|
||||||
/// Because this returns a reference the [Path] silce can not outlive
|
|
||||||
/// the original pointer.
|
|
||||||
///
|
|
||||||
/// [Path]: std::path::Path
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
|
||||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
unsafe {
|
|
||||||
let c_str = std::ffi::CStr::from_ptr(s).to_bytes();
|
|
||||||
let os_str = std::ffi::OsStr::from_bytes(c_str);
|
|
||||||
std::path::Path::new(os_str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// as_path() implementation for windows, documented above.
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
|
||||||
as_path_unicode(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation for as_path() on Windows.
|
|
||||||
//
|
|
||||||
// Having this as a separate function means it can be tested on unix
|
|
||||||
// too.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
|
||||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
|
||||||
|
|
||||||
let cstr = unsafe { CStr::from_ptr(s) };
|
|
||||||
let str = cstr.to_str().unwrap_or_else(|err| panic!("{}", err));
|
|
||||||
|
|
||||||
std::path::Path::new(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use libc::{free, strcmp};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_os_str_to_c_string_cwd() {
|
|
||||||
let some_dir = std::env::current_dir().unwrap();
|
|
||||||
some_dir.as_os_str().to_c_string().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_os_str_to_c_string_unicode() {
|
|
||||||
let some_str = String::from("/some/valid/utf8");
|
|
||||||
let some_dir = std::path::Path::new(&some_str);
|
|
||||||
assert_eq!(
|
|
||||||
some_dir.as_os_str().to_c_string().unwrap(),
|
|
||||||
CString::new("/some/valid/utf8").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_os_str_to_c_string_nul() {
|
|
||||||
let some_str = std::ffi::OsString::from("foo\x00bar");
|
|
||||||
assert_eq!(
|
|
||||||
some_str.to_c_string().err().unwrap(),
|
|
||||||
CStringError::InteriorNullByte
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_path_to_c_string_cwd() {
|
|
||||||
let some_dir = std::env::current_dir().unwrap();
|
|
||||||
some_dir.to_c_string().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_path_to_c_string_unicode() {
|
|
||||||
let some_str = String::from("/some/valid/utf8");
|
|
||||||
let some_dir = std::path::Path::new(&some_str);
|
|
||||||
assert_eq!(
|
|
||||||
some_dir.as_os_str().to_c_string().unwrap(),
|
|
||||||
CString::new("/some/valid/utf8").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_os_str_to_c_string_unicode_fn() {
|
|
||||||
let some_str = std::ffi::OsString::from("foo");
|
|
||||||
assert_eq!(
|
|
||||||
os_str_to_c_string_unicode(&some_str).unwrap(),
|
|
||||||
CString::new("foo").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_path_to_c_string_unicode_fn() {
|
|
||||||
let some_str = String::from("/some/path");
|
|
||||||
let some_path = std::path::Path::new(&some_str);
|
|
||||||
assert_eq!(
|
|
||||||
os_str_to_c_string_unicode(&some_path).unwrap(),
|
|
||||||
CString::new("/some/path").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_os_str_to_c_string_unicode_fn_nul() {
|
|
||||||
let some_str = std::ffi::OsString::from("fooz\x00bar");
|
|
||||||
assert_eq!(
|
|
||||||
os_str_to_c_string_unicode(&some_str).err().unwrap(),
|
|
||||||
CStringError::InteriorNullByte
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_as_path() {
|
|
||||||
let some_path = CString::new("/some/path").unwrap();
|
|
||||||
let ptr = some_path.as_ptr();
|
|
||||||
assert_eq!(as_path(ptr), std::ffi::OsString::from("/some/path"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_as_path_unicode_fn() {
|
|
||||||
let some_path = CString::new("/some/path").unwrap();
|
|
||||||
let ptr = some_path.as_ptr();
|
|
||||||
assert_eq!(as_path_unicode(ptr), std::ffi::OsString::from("/some/path"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cstring_yolo() {
|
|
||||||
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_strdup_str() {
|
|
||||||
unsafe {
|
|
||||||
let s = "hello".strdup();
|
|
||||||
let cmp = strcmp(s, b"hello\x00" as *const u8 as *const libc::c_char);
|
|
||||||
free(s as *mut libc::c_void);
|
|
||||||
assert_eq!(cmp, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_strdup_string() {
|
|
||||||
unsafe {
|
|
||||||
let s = String::from("hello").strdup();
|
|
||||||
let cmp = strcmp(s, b"hello\x00" as *const u8 as *const libc::c_char);
|
|
||||||
free(s as *mut libc::c_void);
|
|
||||||
assert_eq!(cmp, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# Seeds for failure cases proptest has generated in the past. It is
|
|
||||||
# automatically read and these particular cases re-run before any
|
|
||||||
# novel cases are generated.
|
|
||||||
#
|
|
||||||
# It is recommended to check this file in to source control so that
|
|
||||||
# everyone who runs the test benefits from these saved cases.
|
|
||||||
cc 03cab93c6d1f3a8245f63cf84dacb307944294fe6333c1e38f078a6600659c7a # shrinks to data = "a\t0aA\ta\t0 \ta\t0 \ta a\t\ta A\tAA0a0a 0\t a\t aA \t a\t A0\t AAa\taA0\taAAaA\t0\taa0a\ta Aa Aaaa A0A\t a aA 0\t A\t0\t0\t\t\t\t\t\tA \t\t a\tA Aa aAA0A0AA0aaA A\t\t aa0\ta\t \tAa\taA\t00 AA A a\tA a aAAa \t 00\t0 \t\t a A 0\t\t\t aAA Aa \taAAAA0a A0A\t\t1\\E$\t$R\tc\t^=\t\"\tQ<Uk\t\t>A\t\t&\t}v&\tM^\'|\tW5?dn\t\t+\t\tP\te`\t\t>:Brlq--?\t$#Q\tK=zJ\tb\"9*.\"\t`\tF&T*\tBs,\tg\'*\\\t:\t?l$\t\t|A\"HR:Hk\t\\KkV\t\t{=&!^e%:|_*0wV\t[$`\t:\t$%f\t\t[!\"Y. \tP\t\th\'?\'/?%:++NfQo#@\"+?\t(\\??{\t\'\'$Dzj\t0.?{s4?&?Y=/yj]Z=\t4n\t?Ja\"\t{I\t$\t;I}_8V\t&\t?N\'\tI2/\t9.\tFT%9%`\'\tz\to7Y\t|AXP&@G12g\t\'w\t\t%??\t\"h$?F\"\"6%q\\\\{\tT\t\"]$87$}h\'\t<\t$\tc%U:mT2:<v\t#Rl!;U\t\t\"^D\tRZ(BZ{n\t%[$fgH`\t{B}:*\t\t%%*F`%W\t//B}PQ\t\tsu2\tLz<1*!p-X\tnKv&&0\thm4<n\\.\\/.w\'\t<)E1g3#ood\t`?\t\\({N?,8_?h\ty\t0%\t*$A\t\t*w-ViQUj\tTiH\t%\t%&0p\'\'\tA%r**Fo\'Z\\\tNI*ely4=I?}$.o`\t$\ts\'<\t\",:~=Nv/0O%=+<LS\t%P\'\t$r%d.\t{G{/L:\t&I&8-`cy*\"{\t/%fP9.P<\t\t\'/`\t\t`\t\t`!t:\t::\t\tW\'X^\t@.uL&a\tN\t\t\t.\t?0*\tvUK>UJ\\\tQbj%w]P=Js\t\"R\t&s^?</=%\t\'VI:\" kT`{b*<\t\tF&?Y\t\t:/\t!C\'e0`\t\t\tx-\t*\\N\\wwu.I4\tb/y\t\"P:P%\"\t\tQ][\\\t^x\t\t):\t\t&s\t$1-\t\t\tXt`0\t;\t/UeCP*\"G\t\t\':\tk@91Hf&#\t(Uzz&`p2;\t{]\t\"I_%=\\%,XD\"\'06QfCd<;$:q^\t8:\"*\"H\t\to\t&xK/\t\ty0/<}<j<|*`~::b\t=S.=}=Ki\t<Y.\'{\tf\t{Ub*H%(_4*?*\tn2\t/\'\t\t\t/,4]\tt\t<y\t\t\tWi\t\tT&\"\t\t\t\t\t=/1Wu?\t\'A\"W-P\t$?e\\\t`\t6`vD\t8`\t\tccD J\tY&jI//?_\t\\j\t_\tsiq$\t?9\tQ\t.^%&..?%Jm??A%+<\tN&*\t.g\tS$W\"\"\tMpq\t\t:&\\\thTz&<iz%/%T&\'F\t\\]&\t\t}\t\t\tXr=dtp`:+<,\t%60Y*<(&K*kJ\todO\t=<V?&\tMU/\"\t= Y!)<\tV\t9\t)\t&v8q{\t\t&pT\t3\ttB,qcq\'i$;gv%j_%M_{[\"&;\t\t\t.B;6Y\\%\t\"\tY/a\t\\`:?\t<\t?]\taNwD;\\\t%l*74%5T?QS :~yR/E*R\t\t=u\t\\\t\t.Q<;\\\t_S/{&F$^\tw_>\'h=|%\t\t:W!\\<U2\'$\tb&`\t=|=L9\t\t\t\\WZ:! }{\t ;\t;\t\t 0.*\t.%\"\'r%{<Mh_t[[}\t-\tJo\"b/AC*-=`=T\tz$2\tC\t\t/k{47\"\t\t,q%\tZ\tT3\t\tf>%\t\'?%@^tx\t7\"1Bk{b{n\t\"Pj3\tHc\t\tt\tY<\t#?\tSh\\yk/N\\\t8 7=R4*9Cw&m\t\\-\'f\t|\'#t(Etu.Hdu(E\t%&v:\'aqW~A5\t\t w.s{J%lX<\"\t\'*%m<&:/B<&\':U}$&`.{)\t\t6S\t:/$*kQ-Z\t^\'t${/tH*\'v\t3\t=\t\tDyp:B\t`I_R&4SO\t\t&-j=*.\t87&\'e*( \t\t\t\'<$\\DJ<$p?{}\'&\tv\t\\Xk<Y:Y`!$K{\tF&\tzd\t\t*i$\tj\'\t<)R*\t%?\t!.\t=\"@#~:=*\t\tXO=_T,1\"\'.%%\"`{\\:\t\"\tfkeOb/\'$I~\ta\t|&\t[\\KK\"1&Z\t<k\t\t)%\'-~\"2n\tj\tW?*<@w{g%d\ta\\\'\'I\t;:ySR%ke:4\tc\t$=\t&9P]x4\tJ=\t6C6%a\t`0\tF\tm-\tTr\t}\t\tQum\t&@\typ|w2&\t\t3`i&t\t\tT5\"\t.&b&e*/==1.\'*\\[U*\tqPL%?$-0/}~|q`\t\t}\t$\tq==o+T$\'!H\t\ti&um\"?\"%%\t/\'p\tg>?{0{J{\t\t/\t\t{zKZ&>=\t[\"1h<H%z/8,/]s\tv{7\t\t:j*H,M//\t\t\td\'.)\t"
|
|
||||||
@@ -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"
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
1
python/doc/_templates/globaltoc.html
vendored
@@ -6,6 +6,7 @@
|
|||||||
<li><a href="{{ pathto('install') }}">install</a></li>
|
<li><a href="{{ pathto('install') }}">install</a></li>
|
||||||
<li><a href="{{ pathto('api') }}">high level API</a></li>
|
<li><a href="{{ pathto('api') }}">high level API</a></li>
|
||||||
<li><a href="{{ pathto('lapi') }}">low level API</a></li>
|
<li><a href="{{ pathto('lapi') }}">low level API</a></li>
|
||||||
|
<li><a href="{{ pathto('capi') }}">C deltachat.h</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<b>external links:</b>
|
<b>external links:</b>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ high level API reference
|
|||||||
|
|
||||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
- :class:`deltachat.account.Account` (your main entry point, creates the
|
||||||
other classes)
|
other classes)
|
||||||
- :class:`deltachat.contact.Contact`
|
- :class:`deltachat.chatting.Contact`
|
||||||
- :class:`deltachat.chat.Chat`
|
- :class:`deltachat.chatting.Chat`
|
||||||
- :class:`deltachat.message.Message`
|
- :class:`deltachat.message.Message`
|
||||||
|
|
||||||
Account
|
Account
|
||||||
@@ -22,13 +22,13 @@ Account
|
|||||||
Contact
|
Contact
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. autoclass:: deltachat.contact.Contact
|
.. autoclass:: deltachat.chatting.Contact
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Chat
|
Chat
|
||||||
----
|
----
|
||||||
|
|
||||||
.. autoclass:: deltachat.chat.Chat
|
.. autoclass:: deltachat.chatting.Chat
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Message
|
Message
|
||||||
|
|||||||
@@ -2,13 +2,7 @@
|
|||||||
low level API reference
|
low level API reference
|
||||||
===================================
|
===================================
|
||||||
|
|
||||||
for full doxygen-generated C-docs, defines and functions please checkout
|
for full C-docs, defines and function checkout :doc:`capi`
|
||||||
|
|
||||||
https://c.delta.chat
|
|
||||||
|
|
||||||
|
|
||||||
Python low-level capi calls
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: deltachat.capi.lib
|
.. automodule:: deltachat.capi.lib
|
||||||
|
|||||||
@@ -6,26 +6,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", "."
|
])
|
||||||
])
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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__
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from deltachat import const
|
|
||||||
from deltachat import provider
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_from_email():
|
|
||||||
example = provider.Provider.from_email("email@example.com")
|
|
||||||
assert example.overview_page == "https://providers.delta.chat/example.com"
|
|
||||||
assert example.name == "Example"
|
|
||||||
assert example.markdown == "\n..."
|
|
||||||
assert example.status_date == "2018-09"
|
|
||||||
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_from_domain():
|
|
||||||
example = provider.Provider("example.com")
|
|
||||||
assert example.overview_page == "https://providers.delta.chat/example.com"
|
|
||||||
assert example.name == "Example"
|
|
||||||
assert example.markdown == "\n..."
|
|
||||||
assert example.status_date == "2018-09"
|
|
||||||
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
|
|
||||||
|
|
||||||
|
|
||||||
def test_provider_info_none():
|
|
||||||
with pytest.raises(provider.ProviderNotFoundError):
|
|
||||||
provider.Provider.from_email("email@unexistent.no")
|
|
||||||
@@ -1,38 +1,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
|
||||||
|
|||||||
@@ -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 "$@"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
nightly-2019-11-06
|
nightly-2019-08-13
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
201
src/aheader.rs
@@ -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"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
634
src/blob.rs
@@ -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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1783
src/chat.rs
275
src/chatlist.rs
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
107
src/config.rs
@@ -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(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(¶m_in.addr, &xml_raw);
|
|
||||||
if let Err(err) = &res {
|
|
||||||
warn!(
|
|
||||||
context,
|
|
||||||
"Failed to parse Thunderbird autoconfiguration XML: {}", err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
1139
src/configure/mod.rs
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
370
src/contact.rs
@@ -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"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
318
src/context.rs
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
1294
src/dc_mimefactory.rs
Normal file
1665
src/dc_mimeparser.rs
Normal 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 =
|
||||||
"<>"'& äÄöÖüÜß fooÆçÇ ♦‎‏‌&noent;‍";
|
"<>"'& äÄöÖüÜß fooÆçÇ ♦‎‏‌&noent;‍";
|
||||||
|
|
||||||
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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1683
src/dc_tools.rs
1230
src/e2ee.rs
96
src/error.rs
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
273
src/imap/idle.rs
@@ -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)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1273
src/imap/mod.rs
@@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
849
src/imex.rs
@@ -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, ¶m)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
69
src/key.rs
@@ -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, _) =
|
||||||
|
|||||||
44
src/lib.rs
@@ -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;
|
||||||
|
|||||||
194
src/location.rs
@@ -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",);
|
||||||
|
|||||||
32
src/log.rs
@@ -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]
|
||||||
|
|||||||
@@ -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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||