Compare commits

..

6 Commits

Author SHA1 Message Date
holger krekel
0dc87e9a0f remove one attr from mimeparser 2019-12-07 22:41:43 +01:00
holger krekel
c011a8cfef run lint without installing the package 2019-12-07 21:44:34 +01:00
holger krekel
2d7d79fc44 allow to use a different buildhost :) 2019-12-07 21:44:34 +01:00
holger krekel
119d4fa689 - test and fix receiving text/html attachment in multipart/mixed situations
They are now preserved as attachment, instead of diving into parsing-html
  and simplifying.

- adapt mime-debugging
2019-12-07 21:44:34 +01:00
holger krekel
b7a9f73329 passes test but needs cleanup 2019-12-07 21:44:34 +01:00
holger krekel
a52a3e0d24 try fix incoming text/html in multipart/mixed 2019-12-07 21:44:34 +01:00
95 changed files with 5374 additions and 7231 deletions

View File

@@ -36,6 +36,12 @@ jobs:
executor: default
steps:
- checkout
- run:
name: Update submodules
command: git submodule update --init --recursive
- run:
name: Calculate dependencies
command: cargo generate-lockfile
- restore_cache:
keys:
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
@@ -43,6 +49,7 @@ jobs:
- run: rustup default $(cat rust-toolchain)
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
- run: rustup component add --toolchain $(cat rust-toolchain) clippy-preview
- run: cargo update
- run: cargo fetch
- run: rustc +stable --version
- run: rustc +$(cat rust-toolchain) --version
@@ -84,6 +91,7 @@ jobs:
curl https://sh.rustup.rs -sSf | sh -s -- -y
- run: rustup install $(cat rust-toolchain)
- run: rustup default $(cat rust-toolchain)
- run: cargo update
- run: cargo fetch
- run:
name: Test
@@ -120,19 +128,32 @@ jobs:
paths:
- c-docs
remote_python_packaging:
machine: true
steps:
- checkout
# the following commands on success produces
# workspace/{wheelhouse,py-docs} as artefact directories
- run: bash ci_scripts/remote_python_packaging.sh
- persist_to_workspace:
root: workspace
paths:
# - c-docs
- py-docs
- wheelhouse
# build_test_docs_wheel:
# 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
@@ -144,7 +165,10 @@ jobs:
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:
machine: true
@@ -152,7 +176,6 @@ jobs:
- checkout
- attach_workspace:
at: workspace
- run: pyenv versions
- run: pyenv global 3.5.2
- run: ls -laR workspace
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
@@ -164,7 +187,7 @@ jobs:
- *restore-cache
- run:
name: Run cargo clippy
command: cargo clippy
command: cargo clippy --all
workflows:
@@ -172,44 +195,24 @@ workflows:
test:
jobs:
# - cargo_fetch
- cargo_fetch
- remote_tests_rust:
filters:
tags:
only: /.*/
- remote_tests_rust
- remote_tests_python:
filters:
tags:
only: /.*/
- remote_tests_python
- remote_python_packaging:
requires:
- remote_tests_python
- remote_tests_rust
filters:
tags:
only: /.*/
- upload_docs_wheels:
# - upload_docs_wheels:
# requires:
# - build_test_docs_wheel
# - build_doxygen
- rustfmt:
requires:
- remote_python_packaging
- build_doxygen
filters:
tags:
only: /.*/
# - rustfmt:
# requires:
# - cargo_fetch
# - clippy:
# requires:
# - cargo_fetch
- cargo_fetch
- clippy:
requires:
- cargo_fetch
- build_doxygen:
filters:
tags:
only: /.*/
- build_doxygen
# Linux Desktop 64bit
# - test_x86_64-unknown-linux-gnu:

View File

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

View File

@@ -1,175 +1,8 @@
# Changelog
## 1.0.0-beta.24
- fix oauth2/gmail bug introduced in beta23 (not used in releases) #1219
- fix panic when receiving eg. cyrillic filenames #1216
- delete all consumed secure-join handshake messagess #1209 #1212
- rust-level cleanups #1218 #1217 #1210 #1205
- python-level cleanups #1204 #1202 #1201
## 1.0.0-beta.23
- #1197 fix imap-deletion of messages
- #1171 Combine multiple MDNs into a single mail, reducing traffic
- #1155 fix to not send out gossip always, reducing traffic
- #1160 fix reply-to-encrypted determination
- #1182 Add "Auto-Submitted: auto-replied" header to MDNs
- #1194 produce python wheels again, fix c/py.delta.chat
master-deployment
- rust-level housekeeping and improvements #1161 #1186 #1185 #1190 #1194 #1199 #1191 #1190 #1184 and more
- #1063 clarify licensing
- #1147 use mailparse 0.10.2
## 1.0.0-beta.22
- #1095 normalize email lineends to CRLF
- #1095 enable link-time-optimization, saves eg. on android 11 mb
- #1099 fix import regarding devicechats
- #1092 improve logging
- #1096 #1097 #1094 #1090 #1091 internal cleanups
## 1.0.0-beta.21
- #1078 #1082 ensure RFC compliance by producing 78 column lines for
encoded attachments.
- #1080 don't recreate and thus break group membership if an unknown
sender (or mailer-daemon) sends a message referencing the group chat
- #1081 #1079 some internal cleanups
- update imap-proto dependency, to fix yandex/oauth
## 1.0.0-beta.20
- #1074 fix OAUTH2/gmail
- #1072 fix group members not appearing in contact list
- #1071 never block interrupt_idle (thus hopefully also not on maybe_network())
- #1069 reduce smtp-timeout to 30 seconds
- #1066 #1065 avoid unwrap in dehtml, make literals more readable
## 1.0.0-beta.19
- #1058 timeout smtp-send if it doesn't complete in 15 minutes
- #1059 trim down logging
## 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
## next (pending)
- restructured (but not change) imap idle handling into own file. cc @link2xt
## 1.0.0-beta.12

1379
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,27 @@
[package]
name = "deltachat"
version = "1.0.0-beta.24"
version = "1.0.0-beta.12"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
[profile.release]
# lto = true
license = "MPL"
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.4.0", default-features = false }
pgp = { git = "https://github.com/rpgp/rpgp", branch = "master", default-features = false }
hex = "0.4.0"
sha2 = "0.8.0"
rand = "0.7.0"
smallvec = "1.0.0"
reqwest = { version = "0.10.0", features = ["blocking", "json"] }
num-derive = "0.3.0"
rand = "0.6.5"
smallvec = "0.6.9"
reqwest = { version = "0.9.15", default-features = false, features = ["rustls-tls"] }
num-derive = "0.2.5"
num-traits = "0.2.6"
async-smtp = "0.2"
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
async-imap = "0.2"
async-native-tls = "0.3.1"
async-std = { version = "1.4", features = ["unstable"] }
lettre = { git = "https://github.com/deltachat/lettre", branch = "feat/mail" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "feat/mail" }
async-imap = { git = "https://github.com/async-email/async-imap", branch="master" }
async-tls = "0.6"
async-std = { version = "1.0", features = ["unstable"] }
base64 = "0.11"
charset = "0.1"
percent-encoding = "2.0"
@@ -34,11 +30,12 @@ serde_json = "1.0"
chrono = "0.4.6"
failure = "0.1.5"
failure_derive = "0.1.5"
indexmap = "1.3.0"
# TODO: make optional
rustyline = "4.1.0"
lazy_static = "1.4.0"
regex = "1.1.6"
rusqlite = { version = "0.21", features = ["bundled"] }
r2d2_sqlite = "0.13.0"
rusqlite = { version = "0.20", features = ["bundled"] }
r2d2_sqlite = "0.12.0"
r2d2 = "0.8.5"
strum = "0.16.0"
strum_macros = "0.16.0"
@@ -47,19 +44,17 @@ backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.8.0"
image-meta = "0.1.0"
quick-xml = "0.17.1"
quick-xml = "0.15.0"
escaper = "0.1.0"
bitflags = "1.1.0"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.10.2"
rustls = "0.16.0"
webpki-roots = "0.18.0"
webpki = "0.21.0"
mailparse = "0.10.1"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
pretty_env_logger = "0.3.1"
rustyline = { version = "4.1.0", optional = true }
crossbeam = "0.7.3"
[dev-dependencies]
tempfile = "3.0"
@@ -80,11 +75,10 @@ path = "examples/simple.rs"
[[example]]
name = "repl"
path = "examples/repl/main.rs"
required-features = ["rustyline"]
[features]
default = ["nightly", "ringbuf"]
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"]
vendored = []
nightly = ["pgp/nightly"]
ringbuf = ["pgp/ringbuf"]

View File

@@ -2,6 +2,9 @@ The files in this directory and under its subdirectories
are (c) 2019 by Bjoern Petersen and contributors and released under the
Mozilla Public License Version 2.0, see below for a copy.
NOTE that the files in the "libs" directory are copyrighted by third parties
and come with their own respective licenses.
Mozilla Public License Version 2.0
==================================

View File

@@ -114,22 +114,3 @@ $ cargo test -- --ignored
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/
[appveyor-shield]: https://ci.appveyor.com/api/projects/status/lqpegel3ld4ipxj8/branch/master?style=flat-square
[appveyor]: https://ci.appveyor.com/project/dignifiedquire/deltachat-core-rust/branch/master
## Language bindings and frontend projects
Language bindings are available for:
- [C](https://c.delta.chat)
- [Node.js](https://www.npmjs.com/package/deltachat-node)
- [Python](https://py.delta.chat)
- [Go](https://github.com/hugot/go-deltachat/)
- **Java** and **Swift** (contained in the Android/iOS repos)
The following "frontend" projects make use of the Rust-library
or its language bindings:
- [Android](https://github.com/deltachat/deltachat-android)
- [iOS](https://github.com/deltachat/deltachat-ios)
- [Desktop](https://github.com/deltachat/deltachat-desktop)
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
- several **Bots**

View File

@@ -8,6 +8,7 @@ install:
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -vV
- cargo -vV
- cargo update
build: false

View File

@@ -14,22 +14,24 @@ DOXYDOCDIR=${3:?directory where doxygen docs to be found}
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
# python docs to py.delta.chat
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
rsync -avz \
--delete \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
"$PYDOCDIR/html/" \
delta@py.delta.chat:build/${BRANCH}
# DISABLED: python docs to py.delta.chat
#ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
#rsync -avz \
# -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
# "$PYDOCDIR/html/" \
# delta@py.delta.chat:build/${BRANCH}
# C docs to c.delta.chat
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@c.delta.chat mkdir -p build-c/${BRANCH}
rsync -avz \
--delete \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
"$DOXYDOCDIR/html/" \
delta@c.delta.chat:build-c/${BRANCH}
exit 0
# OUTDATED -- for re-use from python release-scripts
echo -----------------------
echo upload wheels
echo -----------------------
@@ -37,7 +39,6 @@ echo -----------------------
# Bundle external shared libraries into the wheels
pushd $WHEELHOUSEDIR
pip3 install -U pip
pip3 install devpi-client
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN
@@ -49,9 +50,6 @@ devpi use dc/$N_BRANCH || {
devpi use dc/$N_BRANCH
}
devpi index $N_BRANCH bases=/root/pypi
devpi upload deltachat*
devpi upload deltachat*.whl
popd
# remove devpi non-master dc indices if thy are too old
python ci_scripts/cleanup_devpi_indices.py

View File

@@ -1,69 +0,0 @@
"""
Remove old "dc" indices except for master which always stays.
"""
from requests import Session
import datetime
import sys
import subprocess
MAXDAYS=7
session = Session()
session.headers["Accept"] = "application/json"
def get_indexes(baseurl, username):
response = session.get(baseurl + username)
assert response.status_code == 200
result = response.json()["result"]
return result["indexes"]
def get_projectnames(baseurl, username, indexname):
response = session.get(baseurl + username + "/" + indexname)
assert response.status_code == 200
result = response.json()["result"]
return result["projects"]
def get_release_dates(baseurl, username, indexname, projectname):
response = session.get(baseurl + username + "/" + indexname + "/" + projectname)
assert response.status_code == 200
result = response.json()["result"]
dates = set()
for value in result.values():
if "+links" not in value:
continue
for link in value["+links"]:
for log in link["log"]:
dates.add(tuple(log["when"]))
return dates
def run():
baseurl = "https://m.devpi.net/"
username = "dc"
for indexname in get_indexes(baseurl, username):
projectnames = get_projectnames(baseurl, username, indexname)
if indexname == "master" or not indexname:
continue
assert projectnames == ["deltachat"]
for projectname in projectnames:
dates = get_release_dates(baseurl, username, indexname, projectname)
if not dates:
print(
"%s has no releases" % (baseurl + username + "/" + indexname),
file=sys.stderr)
date = datetime.datetime.now()
else:
date = datetime.datetime(*max(dates))
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
assert username and indexname
url = baseurl + username + "/" + indexname
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
if __name__ == '__main__':
run()

View File

@@ -5,11 +5,14 @@ RUN echo /usr/local/lib64 > /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
# Install a recent Perl, needed to install the openssl crate
# Install a recent Perl, needed to install OpenSSL
ADD deps/build_perl.sh /builder/build_perl.sh
RUN rm /usr/bin/perl
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
# Install OpenSSL
ADD deps/build_openssl.sh /builder/build_openssl.sh
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, ...)

View File

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

View File

@@ -0,0 +1,49 @@
#!/bin/bash
#
# Build the Delta Chat C/Rust library
#
set -e -x
# perform clean build of core and install
export TOXWORKDIR=.docker-tox
# build core library
cargo build --release -p deltachat_ffi
# configure access to a base python and
# to several python interpreters needed by tox below
export PATH=$PATH:/opt/python/cp35-cp35m/bin
export PYTHONDONTWRITEBYTECODE=1
pushd /bin
ln -s /opt/python/cp27-cp27m/bin/python2.7
ln -s /opt/python/cp36-cp36m/bin/python3.6
ln -s /opt/python/cp37-cp37m/bin/python3.7
popd
#
# run python tests
#
if [ -n "$TESTS" ]; then
echo ----------------
echo run python tests
echo ----------------
pushd python
# first run all tests ...
rm -rf tests/__pycache__
rm -rf src/deltachat/__pycache__
export PYTHONDONTWRITEBYTECODE=1
tox --workdir "$TOXWORKDIR" -e py27,py35,py36,py37
popd
fi
if [ -n "$DOCS" ]; then
echo -----------------------
echo generating python docs
echo -----------------------
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
fi

View File

@@ -1,51 +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
# we have to create a remote file for the remote-docker run
# so we can do a simple ssh command with a TTY
# so that when our job dies, all container-runs are aborted.
# sidenote: the circle-ci machinery will kill ongoing jobs
# if there are new commits and we want to ensure that
# everything is terminated/cleaned up and we have no orphaned
# useless still-running docker-containers consuming resources.
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
set +x -e
cd $BUILDDIR
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
set -x
# run everything else inside docker
docker run -e DCC_PY_LIVECONFIG \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
_HERE
echo "--- Running $CIRCLE_JOB remotely"
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
mkdir -p workspace
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux1*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs

View File

@@ -3,4 +3,5 @@
set -ex
cd deltachat-ffi
PROJECT_NUMBER=$(git log -1 --format "%h (%cd)") doxygen
doxygen

View File

@@ -1,48 +0,0 @@
#!/bin/bash
#
# Build the Delta Chat Core Rust library, Python wheels and docs
set -e -x
# Perform clean build of core and install.
export TOXWORKDIR=.docker-tox
# compile core lib
export PATH=/root/.cargo/bin:$PATH
cargo build --release -p deltachat_ffi
# cargo test --all --all-features
# Statically link against libdeltachat.a.
export DCC_RS_DEV=$(pwd)
# Configure access to a base python and to several python interpreters
# needed by tox below.
export PATH=$PATH:/opt/python/cp35-cp35m/bin
export PYTHONDONTWRITEBYTECODE=1
pushd /bin
ln -s /opt/python/cp27-cp27m/bin/python2.7
ln -s /opt/python/cp36-cp36m/bin/python3.6
ln -s /opt/python/cp37-cp37m/bin/python3.7
ln -s /opt/python/cp38-cp38/bin/python3.8
popd
pushd python
# prepare a clean tox run
rm -rf tests/__pycache__
rm -rf src/deltachat/__pycache__
mkdir -p $TOXWORKDIR
# disable live-account testing to speed up test runs and wheel building
# XXX we may switch on some live-tests on for better ensurances
# Note that the independent remote_tests_python step does all kinds of
# live-testing already.
unset DCC_PY_LIVECONFIG
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
popd
echo -----------------------
echo generating python docs
echo -----------------------
(cd python && tox --workdir "$TOXWORKDIR" -e doc)

View File

@@ -1,11 +1,11 @@
[package]
name = "deltachat_ffi"
version = "1.0.0-beta.24"
version = "1.0.0-beta.12"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
readme = "README.md"
license = "MPL-2.0"
license = "MIT OR Apache-2.0"
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
categories = ["cryptography", "std", "email"]
@@ -21,7 +21,6 @@ libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"
failure = "0.1.6"
serde_json = "1.0"
[features]
default = ["vendored", "nightly", "ringbuf"]

View File

@@ -243,7 +243,7 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
*/
dc_context_t* dc_context_new (void* userdata, const char* os_name);
dc_context_t* dc_context_new (dc_callback_t cb, void* userdata, const char* os_name);
/**
@@ -404,14 +404,14 @@ int dc_set_config (dc_context_t* context, const char*
char* dc_get_config (dc_context_t* context, const char* key);
/**
* Set stock string translation.
* Set stock string translation.
*
* The function will emit warnings if it returns an error state.
* The function will emit warnings if it returns an error state.
*
* @memberof dc_context_t
* @param context The context object
* @param stock_id the integer id of the stock message (DC_STR_*)
* @param stock_msg the message to be used
* @param stock_msg the message to be used
* @return int (==0 on error, 1 on success)
*/
int dc_set_stock_translation(dc_context_t* context, uint32_t stock_id, const char* stock_msg);
@@ -538,14 +538,307 @@ int dc_is_configured (const dc_context_t* context);
/**
* TODO: Document
* Execute pending imap-jobs.
* This function and dc_perform_imap_fetch() and dc_perform_imap_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* imap_thread_func(void* context)
* {
* while (true) {
* dc_perform_imap_jobs(context);
* dc_perform_imap_fetch(context);
* dc_perform_imap_idle(context);
* }
* }
*
* // start imap-thread that runs forever
* pthread_t imap_thread;
* pthread_create(&imap_thread, NULL, imap_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_imap_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_context_run (dc_context_t* context, dc_callback_t cb);
void dc_perform_imap_jobs (dc_context_t* context);
/**
* TODO: Document
* Fetch new messages, if any.
* This function and dc_perform_imap_jobs() and dc_perform_imap_idle() must be called from the same thread,
* typically in a loop.
*
* See dc_perform_imap_jobs() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_context_shutdown (dc_context_t* context);
void dc_perform_imap_fetch (dc_context_t* context);
/**
* Wait for messages or jobs.
* This function and dc_perform_imap_jobs() and dc_perform_imap_fetch() must be called from the same thread,
* typically in a loop.
*
* You should call this function directly after calling dc_perform_imap_fetch().
*
* See dc_perform_imap_jobs() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_imap_idle (dc_context_t* context);
/**
* Interrupt waiting for imap-jobs.
* If dc_perform_imap_jobs(), dc_perform_imap_fetch() and dc_perform_imap_idle() are called in a loop,
* calling this function causes imap-jobs to be executed and messages to be fetched.
*
* dc_interrupt_imap_idle() does _not_ interrupt dc_perform_imap_jobs() or dc_perform_imap_fetch().
* If the imap-thread is inside one of these functions when dc_interrupt_imap_idle() is called, however,
* the next call of the imap-thread to dc_perform_imap_idle() is interrupted immediately.
*
* Internally, this function is called whenever a imap-jobs should be processed
* (delete message, markseen etc.).
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_imap_idle (dc_context_t* context);
/**
* Execute pending mvbox-jobs.
* This function and dc_perform_mvbox_fetch() and dc_perform_mvbox_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* mvbox_thread_func(void* context)
* {
* while (true) {
* dc_perform_mvbox_jobs(context);
* dc_perform_mvbox_fetch(context);
* dc_perform_mvbox_idle(context);
* }
* }
*
* // start mvbox-thread that runs forever
* pthread_t mvbox_thread;
* pthread_create(&mvbox_thread, NULL, mvbox_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_mvbox_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_jobs (dc_context_t* context);
/**
* Fetch new messages from the MVBOX, if any.
* The MVBOX is a folder on the account where chat messages are moved to.
* The moving is done to not disturb shared accounts that are used by both,
* Delta Chat and a classical MUA.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_fetch (dc_context_t* context);
/**
* Wait for messages or jobs in the MVBOX-thread.
* This function and dc_perform_mvbox_fetch().
* must be called from the same thread, typically in a loop.
*
* You should call this function directly after calling dc_perform_mvbox_fetch().
*
* See dc_perform_mvbox_fetch() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_idle (dc_context_t* context);
/**
* Interrupt waiting for MVBOX-fetch.
* dc_interrupt_mvbox_idle() does _not_ interrupt dc_perform_mvbox_fetch().
* If the MVBOX-thread is inside this function when dc_interrupt_mvbox_idle() is called, however,
* the next call of the MVBOX-thread to dc_perform_mvbox_idle() is interrupted immediately.
*
* Internally, this function is called whenever a imap-jobs should be processed.
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_mvbox_idle (dc_context_t* context);
/**
* Execute pending sentbox-jobs.
* This function and dc_perform_sentbox_fetch() and dc_perform_sentbox_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* sentbox_thread_func(void* context)
* {
* while (true) {
* dc_perform_sentbox_jobs(context);
* dc_perform_sentbox_fetch(context);
* dc_perform_sentbox_idle(context);
* }
* }
*
* // start sentbox-thread that runs forever
* pthread_t sentbox_thread;
* pthread_create(&sentbox_thread, NULL, sentbox_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_sentbox_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_jobs (dc_context_t* context);
/**
* Fetch new messages from the Sent folder, if any.
* This function and dc_perform_sentbox_idle()
* must be called from the same thread, typically in a loop.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_fetch (dc_context_t* context);
/**
* Wait for messages or jobs in the SENTBOX-thread.
* This function and dc_perform_sentbox_fetch()
* must be called from the same thread, typically in a loop.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_idle (dc_context_t* context);
/**
* Interrupt waiting for messages or jobs in the SENTBOX-thread.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_sentbox_idle (dc_context_t* context);
/**
* Execute pending smtp-jobs.
* This function and dc_perform_smtp_idle() must be called from the same thread,
* typically in a loop.
*
* Example:
*
* void* smtp_thread_func(void* context)
* {
* while (true) {
* dc_perform_smtp_jobs(context);
* dc_perform_smtp_idle(context);
* }
* }
*
* // start smtp-thread that runs forever
* pthread_t smtp_thread;
* pthread_create(&smtp_thread, NULL, smtp_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_smtp_idle() in the thread above
* // to return so that jobs are executed
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_smtp_jobs (dc_context_t* context);
/**
* Wait for smtp-jobs.
* This function and dc_perform_smtp_jobs() must be called from the same thread,
* typically in a loop.
*
* See dc_interrupt_smtp_idle() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_smtp_idle (dc_context_t* context);
/**
* Interrupt waiting for smtp-jobs.
* If dc_perform_smtp_jobs() and dc_perform_smtp_idle() are called in a loop,
* calling this function causes jobs to be executed.
*
* dc_interrupt_smtp_idle() does _not_ interrupt dc_perform_smtp_jobs().
* If the smtp-thread is inside this function when dc_interrupt_smtp_idle() is called, however,
* the next call of the smtp-thread to dc_perform_smtp_idle() is interrupted immediately.
*
* Internally, this function is called whenever a message is to be sent.
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_smtp_idle (dc_context_t* context);
/**
* This function can be called whenever there is a hint
@@ -1310,7 +1603,7 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
void dc_delete_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
/**
* Empty IMAP server folder: delete all messages.
* Empty IMAP server folder: delete all messages.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new()
@@ -1463,7 +1756,7 @@ uint32_t dc_create_contact (dc_context_t* context, const char*
* Trying to add email-addresses that are already in the contact list,
* results in updating the name unless the name was changed manually by the user.
* If any email-address or any name is really updated,
* 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 dc_create_contact(),
* however, for adding a bunch of addresses, this function is _much_ faster.
@@ -1814,22 +2107,20 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
/**
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
* Get QR code text that will offer an secure-join verification.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works eg. with OpenKeychain.
*
* The scanning device will pass the scanned content to dc_check_qr() then;
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* if this function returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id If set to a group-chat-id,
* the Verified-Group-Invite protocol is offered in the QR code;
* the group-join-protocol is offered in the QR code;
* works for verified groups as well as for normal groups.
* If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
* If set to 0, the setup-Verified-contact-protocol is offered in the QR code.
* @return Text that should go to the QR code,
* On errors, an empty QR code is returned, NULL is never returned.
* The returned string must be released using dc_str_unref() after usage.
@@ -1838,29 +2129,13 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
/**
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with dc_get_securejoin_qr().
* Join an out-of-band-verification initiated on another device with dc_get_securejoin_qr().
* This function is typically called when dc_check_qr() returns
* lot.state=DC_QR_ASK_VERIFYCONTACT or lot.state=DC_QR_ASK_VERIFYGROUP.
*
* Depending on the given QR code,
* this function may takes some time and sends and receives several messages.
* Therefore, you should call it always in a separate thread;
* if you want to abort it, you should call dc_stop_ongoing_process().
*
* - If the given QR code starts the Setup-Contact protocol,
* the function typically returns immediately
* and the handshake runs in background.
* Subsequent calls of dc_join_securejoin() will abort unfinished tasks.
* The returned chat is the one-to-one opportunistic chat.
* When the protocol has finished, an info-message is added to that chat.
* - If the given QR code starts the Verified-Group-Invite protocol,
* the function waits until the protocol has finished.
* This is because the verified group is not opportunistic
* and can be created only when the contacts have verified each other.
*
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
* This function takes some time and sends and receives several messages.
* You should call it in a separate thread; if you want to abort it, you should
* call dc_stop_ongoing_process().
*
* @memberof dc_context_t
* @param context The context object
@@ -1868,9 +2143,6 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
* to dc_check_qr().
* @return Chat-id of the joined chat, the UI may redirect to the this chat.
* If the out-of-band verification failed or was aborted, 0 is returned.
* A returned chat-id does not guarantee that the chat or the belonging contact is verified.
* If needed, this be checked with dc_chat_is_verified() and dc_contact_is_verified(),
* however, in practise, the UI will just listen to #DC_EVENT_CONTACTS_CHANGED unconditionally.
*/
uint32_t dc_join_securejoin (dc_context_t* context, const char* qr);
@@ -2346,18 +2618,18 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
/**
* Get info summary for a chat, in json format.
* Get info summary for a chat, in json format.
*
* The returned json string has the following key/values:
* The returned json string has the following key/values:
*
* id: chat id
* name: chat/group name
* color: color of this chat
* last-message-from: who sent the last message
* last-message-text: message (truncated)
* id: chat id
* name: chat/group name
* color: color of this chat
* last-message-from: who sent the last message
* last-message-text: message (truncated)
* last-message-state: DC_STATE* constant
* last-message-date:
* avatar-path: path-to-blobfile
* last-message-date:
* avatar-path: path-to-blobfile
* is_verified: yes/no
* @return a utf8-encoded json string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
@@ -2717,15 +2989,15 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
* Get the state of a message.
*
* Incoming message states:
* - DC_STATE_IN_FRESH (10) - Incoming _fresh_ message. Fresh messages are neither noticed nor seen and are typically shown in notifications. Use dc_get_fresh_msgs() to get all fresh messages.
* - DC_STATE_IN_NOTICED (13) - Incoming _noticed_ message. E.g. chat opened but message not yet read - noticed messages are not counted as unread but did not marked as read nor resulted in MDNs. Use dc_marknoticed_chat() or dc_marknoticed_contact() to mark messages as being noticed.
* - DC_STATE_IN_SEEN (16) - Incoming message, really _seen_ by the user. Marked as read on IMAP and MDN may be sent. Use dc_markseen_msgs() to mark messages as being seen.
* - DC_STATE_IN_FRESH (10) - Incoming _fresh_ message. Fresh messages are not noticed nor seen and are typically shown in notifications. Use dc_get_fresh_msgs() to get all fresh messages.
* - DC_STATE_IN_NOTICED (13) - Incoming _noticed_ message. Eg. chat opened but message not yet read - noticed messages are not counted as unread but did not marked as read nor resulted in MDNs. Use dc_marknoticed_chat() or dc_marknoticed_contact() to mark messages as being noticed.
* - DC_STATE_IN_SEEN (16) - Incoming message, really _seen_ by the user. Marked as read on IMAP and MDN may be send. Use dc_markseen_msgs() to mark messages as being seen.
*
* Outgoing message states:
* - DC_STATE_OUT_PREPARING (18) - For files which need time to be prepared before they can be sent,
* the message enters this state before DC_STATE_OUT_PENDING.
* - DC_STATE_OUT_DRAFT (19) - Message saved as draft using dc_set_draft()
* - DC_STATE_OUT_PENDING (20) - The user has pressed the "send" button but the
* - DC_STATE_OUT_PENDING (20) - The user has send the "send" button but the
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
* - DC_STATE_OUT_FAILED (24) - _Unrecoverable_ error (_recoverable_ errors result in pending messages), you'll receive the event #DC_EVENT_MSG_FAILED.
* - DC_STATE_OUT_DELIVERED (26) - Outgoing message successfully delivered to server (one checkmark). Note, that already delivered messages may get into the state DC_STATE_OUT_FAILED if we get such a hint from the server.
@@ -3373,15 +3645,11 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Check if a contact was verified. E.g. by a secure-join QR code scan
* and if the key has not changed since this verification.
* Same as dc_contact_is_verified() but allows speeding up things
* by adding the peerstate belonging to the contact.
* If you do not have the peerstate available, it is loaded automatically.
*
* The UI may draw a checkbox or something like that beside verified contacts.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0: contact is not verified.
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
* @private @memberof dc_context_t
*/
int dc_contact_is_verified (dc_contact_t* contact);
@@ -3782,6 +4050,11 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_CERTCK_STRICT 1
/**
* Accept invalid hostnames, but not invalid certificates.
*/
#define DC_CERTCK_ACCEPT_INVALID_HOSTNAMES 2
/**
* Accept invalid certificates, including self-signed ones
* or having incorrect hostname.
@@ -3895,30 +4168,30 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
/**
* Emitted when an IMAP folder was emptied.
* Emitted when an IMAP folder was emptied.
*
* @param data1 0
* @param data2 (const char*) folder name.
* @param data2 (const char*) folder name.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_IMAP_FOLDER_EMPTIED 106
/**
* Emitted when a new blob file was successfully written
* Emitted when a new blob file was successfully written
*
* @param data1 0
* @param data2 (const char*) path name
* @param data2 (const char*) path name
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_NEW_BLOB_FILE 150
/**
* Emitted when a blob file was successfully deleted
* Emitted when a blob file was successfully deleted
*
* @param data1 0
* @param data2 (const char*) path name
* @param data2 (const char*) path name
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
@@ -4075,7 +4348,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Contact(s) created, renamed, verified, blocked or deleted.
* Contact(s) created, renamed, blocked or deleted.
*
* @param data1 (int) If not 0, this is the contact_id of an added contact that should be selected.
* @param data2 0

View File

@@ -4,14 +4,12 @@
non_upper_case_globals,
non_upper_case_globals,
non_camel_case_types,
clippy::missing_safety_doc,
clippy::expect_fun_call
non_snake_case
)]
#[macro_use]
extern crate human_panic;
extern crate num_traits;
extern crate serde_json;
use std::collections::HashMap;
use std::convert::TryInto;
@@ -24,7 +22,6 @@ use std::sync::RwLock;
use libc::uintptr_t;
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::chat::ChatId;
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::Contact;
use deltachat::context::Context;
@@ -32,8 +29,6 @@ use deltachat::message::MsgId;
use deltachat::stock::StockMessage;
use deltachat::*;
mod dc_array;
mod string;
use self::string::*;
@@ -57,6 +52,7 @@ use self::string::*;
/// and protected by an [RwLock]. Other than that it needs to store
/// the data which is passed into [dc_context_new].
pub struct ContextWrapper {
cb: Option<dc_callback_t>,
userdata: *mut libc::c_void,
os_name: String,
inner: RwLock<Option<context::Context>>,
@@ -90,10 +86,7 @@ impl ContextWrapper {
///
/// This function makes it easy to log an error.
unsafe fn error(&self, msg: &str) {
self.with_inner(|ctx| {
ctx.call_cb(Event::Error(msg.to_string()));
})
.unwrap();
self.translate_cb(Event::Error(msg.to_string()));
}
/// Unlock the context and execute a closure with it.
@@ -119,19 +112,88 @@ impl ContextWrapper {
match guard.as_ref() {
Some(ref ctx) => Ok(ctxfn(ctx)),
None => {
eprintln!("context not open");
unsafe { self.error("context not open") };
Err(())
}
}
}
fn is_open(&self) -> bool {
self.inner.read().unwrap().is_some()
/// Translates the callback from the rust style to the C-style version.
unsafe fn translate_cb(&self, event: Event) -> uintptr_t {
match self.cb {
Some(ffi_cb) => {
let event_id = event.as_id();
match event {
Event::Info(msg)
| Event::SmtpConnected(msg)
| Event::ImapConnected(msg)
| Event::SmtpMessageSent(msg)
| Event::ImapMessageDeleted(msg)
| Event::ImapMessageMoved(msg)
| Event::ImapFolderEmptied(msg)
| Event::NewBlobFile(msg)
| Event::DeletedBlobFile(msg)
| Event::Warning(msg)
| Event::Error(msg)
| Event::ErrorNetwork(msg)
| Event::ErrorSelfNotInGroup(msg) => {
let data2 = CString::new(msg).unwrap_or_default();
ffi_cb(self, event_id, 0, data2.as_ptr() as uintptr_t)
}
Event::MsgsChanged { chat_id, msg_id }
| Event::IncomingMsg { chat_id, msg_id }
| Event::MsgDelivered { chat_id, msg_id }
| Event::MsgFailed { chat_id, msg_id }
| Event::MsgRead { chat_id, msg_id } => ffi_cb(
self,
event_id,
chat_id as uintptr_t,
msg_id.to_u32() as uintptr_t,
),
Event::ChatModified(chat_id) => ffi_cb(self, event_id, chat_id as uintptr_t, 0),
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
let id = id.unwrap_or_default();
ffi_cb(self, event_id, id as uintptr_t, 0)
}
Event::ConfigureProgress(progress) | Event::ImexProgress(progress) => {
ffi_cb(self, event_id, progress as uintptr_t, 0)
}
Event::ImexFileWritten(file) => {
let data1 = file.to_c_string().unwrap_or_default();
ffi_cb(self, event_id, data1.as_ptr() as uintptr_t, 0)
}
Event::SecurejoinInviterProgress {
contact_id,
progress,
}
| Event::SecurejoinJoinerProgress {
contact_id,
progress,
} => ffi_cb(
self,
event_id,
contact_id as uintptr_t,
progress as uintptr_t,
),
Event::SecurejoinMemberAdded {
chat_id,
contact_id,
} => ffi_cb(
self,
event_id,
chat_id as uintptr_t,
contact_id as uintptr_t,
),
}
}
None => 0,
}
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_new(
cb: Option<dc_callback_t>,
userdata: *mut libc::c_void,
os_name: *const libc::c_char,
) -> *mut dc_context_t {
@@ -143,11 +205,11 @@ pub unsafe extern "C" fn dc_context_new(
to_string_lossy(os_name)
};
let ffi_ctx = ContextWrapper {
cb,
userdata,
os_name,
inner: RwLock::new(None),
};
Box::into_raw(Box::new(ffi_ctx))
}
@@ -185,11 +247,17 @@ pub unsafe extern "C" fn dc_open(
return 0;
}
let ffi_context = &*context;
let rust_cb = move |_ctx: &Context, evt: Event| ffi_context.translate_cb(evt);
let ctx = if blobdir.is_null() || *blobdir == 0 {
Context::new(ffi_context.os_name.clone(), as_path(dbfile).to_path_buf())
Context::new(
Box::new(rust_cb),
ffi_context.os_name.clone(),
as_path(dbfile).to_path_buf(),
)
} else {
Context::with_blobdir(
Box::new(rust_cb),
ffi_context.os_name.clone(),
as_path(dbfile).to_path_buf(),
as_path(blobdir).to_path_buf(),
@@ -222,7 +290,11 @@ pub unsafe extern "C" fn dc_is_open(context: *mut dc_context_t) -> libc::c_int {
return 0;
}
let ffi_context = &*context;
ffi_context.is_open() as libc::c_int
let inner_guard = ffi_context.inner.read().unwrap();
match *inner_guard {
Some(_) => 1,
None => 0,
}
}
#[no_mangle]
@@ -334,7 +406,7 @@ fn render_info(
) -> std::result::Result<String, std::fmt::Error> {
let mut res = String::new();
for (key, value) in &info {
writeln!(&mut res, "{}={}", key, value)?;
write!(&mut res, "{}={}\n", key, value)?;
}
Ok(res)
@@ -373,7 +445,9 @@ pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
return;
}
let ffi_context = &*context;
ffi_context.with_inner(|ctx| ctx.configure()).unwrap_or(())
ffi_context
.with_inner(|ctx| configure::configure(ctx))
.unwrap_or(())
}
#[no_mangle]
@@ -384,120 +458,188 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| ctx.is_configured() as libc::c_int)
.with_inner(|ctx| configure::dc_is_configured(ctx) as libc::c_int)
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_run(context: *mut dc_context_t, cb: Option<dc_callback_t>) {
eprintln!("dc_context_run");
pub unsafe extern "C" fn dc_perform_imap_jobs(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_run()");
eprintln!("ignoring careless call to dc_perform_imap_jobs()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
eprintln!("RUN");
ctx.run(|_ctx, event| {
translate_cb(ffi_context, cb, event);
});
eprintln!("RUN DONE");
})
.with_inner(|ctx| job::perform_inbox_jobs(ctx))
.unwrap_or(())
}
/// Translates the callback from the rust style to the C-style version.
unsafe fn translate_cb(ctx: &ContextWrapper, ffi_cb: Option<dc_callback_t>, event: Event) {
if let Some(ffi_cb) = ffi_cb {
let event_id = event.as_id();
match event {
Event::Info(msg)
| Event::SmtpConnected(msg)
| Event::ImapConnected(msg)
| Event::SmtpMessageSent(msg)
| Event::ImapMessageDeleted(msg)
| Event::ImapMessageMoved(msg)
| Event::ImapFolderEmptied(msg)
| Event::NewBlobFile(msg)
| Event::DeletedBlobFile(msg)
| Event::Warning(msg)
| Event::Error(msg)
| Event::ErrorNetwork(msg)
| Event::ErrorSelfNotInGroup(msg) => {
let data2 = CString::new(msg).unwrap_or_default();
ffi_cb(ctx, event_id, 0, data2.as_ptr() as uintptr_t);
}
Event::MsgsChanged { chat_id, msg_id }
| Event::IncomingMsg { chat_id, msg_id }
| Event::MsgDelivered { chat_id, msg_id }
| Event::MsgFailed { chat_id, msg_id }
| Event::MsgRead { chat_id, msg_id } => {
ffi_cb(
ctx,
event_id,
chat_id.to_u32() as uintptr_t,
msg_id.to_u32() as uintptr_t,
);
}
Event::ChatModified(chat_id) => {
ffi_cb(ctx, event_id, chat_id.to_u32() as uintptr_t, 0);
}
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
let id = id.unwrap_or_default();
ffi_cb(ctx, event_id, id as uintptr_t, 0);
}
Event::ConfigureProgress(progress) | Event::ImexProgress(progress) => {
ffi_cb(ctx, event_id, progress as uintptr_t, 0);
}
Event::ImexFileWritten(file) => {
let data1 = file.to_c_string().unwrap_or_default();
ffi_cb(ctx, event_id, data1.as_ptr() as uintptr_t, 0);
}
Event::SecurejoinInviterProgress {
contact_id,
progress,
}
| Event::SecurejoinJoinerProgress {
contact_id,
progress,
} => {
ffi_cb(
ctx,
event_id,
contact_id as uintptr_t,
progress as uintptr_t,
);
}
Event::SecurejoinMemberAdded {
chat_id,
contact_id,
} => {
ffi_cb(
ctx,
event_id,
chat_id.to_u32() as uintptr_t,
contact_id as uintptr_t,
);
}
}
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_shutdown(context: *mut dc_context_t) {
pub unsafe extern "C" fn dc_perform_imap_fetch(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_shutdown()");
eprintln!("ignoring careless call to dc_perform_imap_fetch()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
eprintln!("SHUTDOWN");
ctx.shutdown();
eprintln!("SHUTDOWN:DONE")
})
.with_inner(|ctx| job::perform_inbox_fetch(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_imap_idle(context: *mut dc_context_t) {
// TODO rename function in co-ordination with UIs
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_imap_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_inbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_imap_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_imap_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_inbox_idle(ctx, true))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_mvbox_fetch(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_mvbox_fetch()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_mvbox_fetch(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_mvbox_jobs(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_mvbox_jobs()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_mvbox_jobs(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_mvbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_mvbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_mvbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_mvbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_mvbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_mvbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_sentbox_fetch(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_sentbox_fetch()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_sentbox_fetch(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_sentbox_jobs(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_sentbox_jobs()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_sentbox_jobs(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_sentbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_sentbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_sentbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_sentbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_sentbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_sentbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_smtp_jobs(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_smtp_jobs()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_smtp_jobs(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_smtp_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_smtp_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_smtp_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_smtp_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_smtp_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_smtp_idle(ctx))
.unwrap_or(())
}
@@ -556,9 +698,7 @@ pub unsafe extern "C" fn dc_create_chat_by_msg_id(context: *mut dc_context_t, ms
ffi_context
.with_inner(|ctx| {
chat::create_by_msg_id(ctx, MsgId::new(msg_id))
.log_err(ctx, "Failed to create chat from msg_id")
.map(|id| id.to_u32())
.unwrap_or(0)
.unwrap_or_log_default(ctx, "Failed to create chat")
})
.unwrap_or(0)
}
@@ -576,9 +716,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
ffi_context
.with_inner(|ctx| {
chat::create_by_contact_id(ctx, contact_id)
.log_err(ctx, "Failed to create chat from contact_id")
.map(|id| id.to_u32())
.unwrap_or(0)
.unwrap_or_log_default(ctx, "Failed to create chat")
})
.unwrap_or(0)
}
@@ -594,12 +732,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
chat::get_by_contact_id(ctx, contact_id)
.log_err(ctx, "Failed to get chat for contact_id")
.map(|id| id.to_u32())
.unwrap_or(0)
})
.with_inner(|ctx| chat::get_by_contact_id(ctx, contact_id).unwrap_or(0))
.unwrap_or(0)
}
@@ -617,7 +750,7 @@ pub unsafe extern "C" fn dc_prepare_msg(
let ffi_msg: &mut MessageWrapper = &mut *msg;
ffi_context
.with_inner(|ctx| {
chat::prepare_msg(ctx, ChatId::new(chat_id), &mut ffi_msg.message)
chat::prepare_msg(ctx, chat_id, &mut ffi_msg.message)
.unwrap_or_log_default(ctx, "Failed to prepare message")
})
.map(|msg_id| msg_id.to_u32())
@@ -638,7 +771,7 @@ pub unsafe extern "C" fn dc_send_msg(
let ffi_msg = &mut *msg;
ffi_context
.with_inner(|ctx| {
chat::send_msg(ctx, ChatId::new(chat_id), &mut ffi_msg.message)
chat::send_msg(ctx, chat_id, &mut ffi_msg.message)
.unwrap_or_log_default(ctx, "Failed to send message")
})
.map(|msg_id| msg_id.to_u32())
@@ -659,7 +792,7 @@ pub unsafe extern "C" fn dc_send_text_msg(
let text_to_send = to_string_lossy(text_to_send);
ffi_context
.with_inner(|ctx| {
chat::send_text_msg(ctx, ChatId::new(chat_id), text_to_send)
chat::send_text_msg(ctx, chat_id, text_to_send)
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to send text message")
})
@@ -684,7 +817,7 @@ pub unsafe extern "C" fn dc_set_draft(
Some(&mut ffi_msg.message)
};
ffi_context
.with_inner(|ctx| ChatId::new(chat_id).set_draft(ctx, msg))
.with_inner(|ctx| chat::set_draft(ctx, chat_id, msg))
.unwrap_or(())
}
@@ -759,7 +892,7 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| match ChatId::new(chat_id).get_draft(ctx) {
.with_inner(|ctx| match chat::get_draft(ctx, chat_id) {
Ok(Some(draft)) => {
let ffi_msg = MessageWrapper {
context,
@@ -796,7 +929,7 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
ffi_context
.with_inner(|ctx| {
let arr = dc_array_t::from(
chat::get_chat_msgs(ctx, ChatId::new(chat_id), flags, marker_flag)
chat::get_chat_msgs(ctx, chat_id, flags, marker_flag)
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
@@ -814,7 +947,7 @@ pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| ChatId::new(chat_id).get_msg_cnt(ctx) as libc::c_int)
.with_inner(|ctx| chat::get_msg_cnt(ctx, chat_id) as libc::c_int)
.unwrap_or(0)
}
@@ -829,7 +962,7 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| ChatId::new(chat_id).get_fresh_msg_cnt(ctx) as libc::c_int)
.with_inner(|ctx| chat::get_fresh_msg_cnt(ctx, chat_id) as libc::c_int)
.unwrap_or(0)
}
@@ -864,9 +997,7 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
chat::marknoticed_chat(ctx, ChatId::new(chat_id))
.log_err(ctx, "Failed marknoticed chat")
.unwrap_or(())
chat::marknoticed_chat(ctx, chat_id).log_err(ctx, "Failed marknoticed chat")
})
.unwrap_or(())
}
@@ -880,9 +1011,7 @@ pub unsafe extern "C" fn dc_marknoticed_all_chats(context: *mut dc_context_t) {
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
chat::marknoticed_all_chats(ctx)
.log_err(ctx, "Failed marknoticed all chats")
.unwrap_or(())
chat::marknoticed_all_chats(ctx).log_err(ctx, "Failed marknoticed all chats")
})
.unwrap_or(())
}
@@ -916,16 +1045,10 @@ pub unsafe extern "C" fn dc_get_chat_media(
ffi_context
.with_inner(|ctx| {
let arr = dc_array_t::from(
chat::get_chat_media(
ctx,
ChatId::new(chat_id),
msg_type,
or_msg_type2,
or_msg_type3,
)
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
chat::get_chat_media(ctx, chat_id, msg_type, or_msg_type2, or_msg_type3)
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
Box::into_raw(Box::new(arr))
})
@@ -992,12 +1115,7 @@ pub unsafe extern "C" fn dc_archive_chat(
return;
};
ffi_context
.with_inner(|ctx| {
ChatId::new(chat_id)
.set_archived(ctx, archive)
.log_err(ctx, "Failed archive chat")
.unwrap_or(())
})
.with_inner(|ctx| chat::archive(ctx, chat_id, archive).log_err(ctx, "Failed archive chat"))
.unwrap_or(())
}
@@ -1009,12 +1127,7 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
ChatId::new(chat_id)
.delete(ctx)
.log_err(ctx, "Failed chat delete")
.unwrap_or(())
})
.with_inner(|ctx| chat::delete(ctx, chat_id).log_err(ctx, "Failed chat delete"))
.unwrap_or(())
}
@@ -1030,7 +1143,7 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
let arr = dc_array_t::from(chat::get_chat_contacts(ctx, ChatId::new(chat_id)));
let arr = dc_array_t::from(chat::get_chat_contacts(ctx, chat_id));
Box::into_raw(Box::new(arr))
})
.unwrap_or_else(|_| ptr::null_mut())
@@ -1050,7 +1163,7 @@ pub unsafe extern "C" fn dc_search_msgs(
ffi_context
.with_inner(|ctx| {
let arr = dc_array_t::from(
ctx.search_msgs(ChatId::new(chat_id), to_string_lossy(query))
ctx.search_msgs(chat_id, to_string_lossy(query))
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
@@ -1068,15 +1181,13 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
}
let ffi_context = &*context;
ffi_context
.with_inner(
|ctx| match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)) {
Ok(chat) => {
let ffi_chat = ChatWrapper { context, chat };
Box::into_raw(Box::new(ffi_chat))
}
Err(_) => ptr::null_mut(),
},
)
.with_inner(|ctx| match chat::Chat::load_from_db(ctx, chat_id) {
Ok(chat) => {
let ffi_chat = ChatWrapper { context, chat };
Box::into_raw(Box::new(ffi_chat))
}
Err(_) => ptr::null_mut(),
})
.unwrap_or_else(|_| ptr::null_mut())
}
@@ -1099,9 +1210,7 @@ pub unsafe extern "C" fn dc_create_group_chat(
ffi_context
.with_inner(|ctx| {
chat::create_group_chat(ctx, verified, to_string_lossy(name))
.log_err(ctx, "Failed to create group chat")
.map(|id| id.to_u32())
.unwrap_or(0)
.unwrap_or_log_default(ctx, "Failed to create group chat")
})
.unwrap_or(0)
}
@@ -1118,7 +1227,7 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| chat::is_contact_in_chat(ctx, ChatId::new(chat_id), contact_id))
.with_inner(|ctx| chat::is_contact_in_chat(ctx, chat_id, contact_id))
.unwrap_or_default()
.into()
}
@@ -1135,9 +1244,7 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
chat::add_contact_to_chat(ctx, ChatId::new(chat_id), contact_id) as libc::c_int
})
.with_inner(|ctx| chat::add_contact_to_chat(ctx, chat_id, contact_id) as libc::c_int)
.unwrap_or(0)
}
@@ -1154,7 +1261,7 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
chat::remove_contact_from_chat(ctx, ChatId::new(chat_id), contact_id)
chat::remove_contact_from_chat(ctx, chat_id, contact_id)
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to remove contact")
})
@@ -1174,7 +1281,7 @@ pub unsafe extern "C" fn dc_set_chat_name(
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
chat::set_chat_name(ctx, ChatId::new(chat_id), to_string_lossy(name))
chat::set_chat_name(ctx, chat_id, to_string_lossy(name))
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set chat name")
})
@@ -1194,7 +1301,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
chat::set_chat_profile_image(ctx, ChatId::new(chat_id), to_string_lossy(image))
chat::set_chat_profile_image(ctx, chat_id, to_string_lossy(image))
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set profile image")
})
@@ -1230,7 +1337,7 @@ pub unsafe extern "C" fn dc_get_mime_headers(
.with_inner(|ctx| {
message::get_mime_headers(ctx, MsgId::new(msg_id))
.map(|s| s.strdup())
.unwrap_or_else(ptr::null_mut)
.unwrap_or_else(|| ptr::null_mut())
})
.unwrap_or_else(|_| ptr::null_mut())
}
@@ -1283,7 +1390,7 @@ pub unsafe extern "C" fn dc_forward_msgs(
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
chat::forward_msgs(ctx, &msg_ids[..], ChatId::new(chat_id))
chat::forward_msgs(ctx, &msg_ids[..], chat_id)
.unwrap_or_log_default(ctx, "Failed to forward message")
})
.unwrap_or_default()
@@ -1696,8 +1803,8 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
securejoin::dc_get_securejoin_qr(ctx, ChatId::new(chat_id))
.unwrap_or_else(|| "".to_string())
securejoin::dc_get_securejoin_qr(ctx, chat_id)
.unwrap_or("".to_string())
.strdup()
})
.unwrap_or_else(|_| "".strdup())
@@ -1714,7 +1821,7 @@ pub unsafe extern "C" fn dc_join_securejoin(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| securejoin::dc_join_securejoin(ctx, &to_string_lossy(qr)).to_u32())
.with_inner(|ctx| securejoin::dc_join_securejoin(ctx, &to_string_lossy(qr)))
.unwrap_or(0)
}
@@ -1730,9 +1837,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
location::send_locations_to_chat(ctx, ChatId::new(chat_id), seconds as i64)
})
.with_inner(|ctx| location::send_locations_to_chat(ctx, chat_id, seconds as i64))
.ok();
}
@@ -1747,9 +1852,7 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
location::is_sending_locations_to_chat(ctx, ChatId::new(chat_id)) as libc::c_int
})
.with_inner(|ctx| location::is_sending_locations_to_chat(ctx, chat_id) as libc::c_int)
.unwrap_or(0)
}
@@ -1787,7 +1890,7 @@ pub unsafe extern "C" fn dc_get_locations(
.with_inner(|ctx| {
let res = location::get_range(
ctx,
ChatId::new(chat_id),
chat_id,
contact_id,
timestamp_begin as i64,
timestamp_end as i64,
@@ -1909,7 +2012,8 @@ pub unsafe extern "C" fn dc_array_get_chat_id(
eprintln!("ignoring careless call to dc_array_get_chat_id()");
return 0;
}
(*array).get_location(index).chat_id.to_u32()
(*array).get_location(index).chat_id
}
#[no_mangle]
pub unsafe extern "C" fn dc_array_get_contact_id(
@@ -2046,7 +2150,7 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id(
return 0;
}
let ffi_list = &*chatlist;
ffi_list.list.get_chat_id(index as usize).to_u32()
ffi_list.list.get_chat_id(index as usize)
}
#[no_mangle]
@@ -2138,7 +2242,7 @@ pub unsafe extern "C" fn dc_chat_get_id(chat: *mut dc_chat_t) -> u32 {
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.get_id().to_u32()
ffi_chat.chat.get_id()
}
#[no_mangle]
@@ -2284,27 +2388,12 @@ pub unsafe extern "C" fn dc_chat_get_info_json(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
let chat = match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)) {
Ok(chat) => chat,
Err(err) => {
error!(ctx, "dc_get_chat_info_json() failed to load chat: {}", err);
return "".strdup();
}
};
let info = match chat.get_info(ctx) {
Ok(info) => info,
Err(err) => {
error!(
ctx,
"dc_get_chat_info_json() failed to get chat info: {}", err
);
return "".strdup();
}
};
serde_json::to_string(&info)
.unwrap_or_log_default(ctx, "dc_get_chat_info_json() failed to serialise to json")
.strdup()
.with_inner(|ctx| match chat::get_info_json(ctx, chat_id) {
Ok(s) => s.strdup(),
Err(err) => {
error!(ctx, "get_info_json({}) returned: {}", chat_id, err);
return "".strdup();
}
})
.unwrap_or_else(|_| "".strdup())
}
@@ -2381,7 +2470,7 @@ pub unsafe extern "C" fn dc_msg_get_chat_id(msg: *mut dc_msg_t) -> u32 {
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_chat_id().to_u32()
ffi_msg.message.get_chat_id()
}
#[no_mangle]
@@ -2488,7 +2577,7 @@ pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c
if let Some(x) = ffi_msg.message.get_filemime() {
x.strdup()
} else {
dc_strdup(ptr::null())
return dc_strdup(ptr::null());
}
}
@@ -2880,7 +2969,7 @@ pub unsafe extern "C" fn dc_contact_get_profile_image(
.contact
.get_profile_image(ctx)
.map(|p| p.to_string_lossy().strdup())
.unwrap_or_else(std::ptr::null_mut)
.unwrap_or_else(|| std::ptr::null_mut())
})
.unwrap_or_else(|_| ptr::null_mut())
}
@@ -3006,12 +3095,12 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
pub mod providers;
pub trait ResultExt<T, E> {
pub trait ResultExt<T> {
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
fn log_err(self, context: &context::Context, message: &str) -> Result<T, E>;
fn log_err(&self, context: &context::Context, message: &str);
}
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
impl<T: Default, E: std::fmt::Display> ResultExt<T> for Result<T, E> {
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T {
match self {
Ok(t) => t,
@@ -3022,11 +3111,10 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
}
}
fn log_err(self, context: &context::Context, message: &str) -> Result<T, E> {
self.map_err(|err| {
warn!(context, "{}: {}", message, err);
err
})
fn log_err(&self, context: &context::Context, message: &str) {
if let Err(err) = self {
error!(context, "{}: {}", message, err);
}
}
}

View File

@@ -86,6 +86,8 @@ pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t)
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {}
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {
()
}
// TODO expose general provider overview url?

View File

@@ -1,13 +1,12 @@
[package]
name = "deltachat_derive"
version = "2.0.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
version = "0.1.0"
authors = ["Dmitry Bogatov <KAction@debian.org>"]
edition = "2018"
license = "MPL-2.0"
[lib]
proc-macro = true
[dependencies]
syn = "1.0.13"
quote = "1.0.2"
syn = "0.14.4"
quote = "0.6.3"

View File

@@ -1,7 +1,7 @@
use std::path::Path;
use std::str::FromStr;
use deltachat::chat::{self, Chat, ChatId};
use deltachat::chat::{self, Chat};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::constants::*;
@@ -20,14 +20,14 @@ use deltachat::qr::*;
use deltachat::sql;
use deltachat::Event;
/// Reset database tables.
/// Reset database tables. This function is called from Core cmdline.
/// 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.
fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
println!("Resetting tables ({})...", bits);
pub fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
info!(context, "Resetting tables ({})...", bits);
if 0 != bits & 1 {
sql::execute(context, &context.sql, "DELETE FROM jobs;", params![]).unwrap();
println!("(1) Jobs reset.");
info!(context, "(1) Jobs reset.");
}
if 0 != bits & 2 {
sql::execute(
@@ -37,11 +37,11 @@ fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
params![],
)
.unwrap();
println!("(2) Peerstates reset.");
info!(context, "(2) Peerstates reset.");
}
if 0 != bits & 4 {
sql::execute(context, &context.sql, "DELETE FROM keypairs;", params![]).unwrap();
println!("(4) Private keypairs reset.");
info!(context, "(4) Private keypairs reset.");
}
if 0 != bits & 8 {
sql::execute(
@@ -80,11 +80,11 @@ fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
)
.unwrap();
sql::execute(context, &context.sql, "DELETE FROM leftgrps;", params![]).unwrap();
println!("(8) Rest but server config reset.");
info!(context, "(8) Rest but server config reset.");
}
context.call_cb(Event::MsgsChanged {
chat_id: ChatId::new(0),
chat_id: 0,
msg_id: MsgId::new(0),
});
@@ -94,9 +94,7 @@ fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> {
let data = dc_read_file(context, filename)?;
if let Err(err) = dc_receive_imf(context, &data, "import", 0, 0) {
println!("dc_receive_imf errored: {:?}", err);
}
dc_receive_imf(context, &data, "import", 0, 0);
Ok(())
}
@@ -155,7 +153,7 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
let name = name_f.to_string_lossy();
if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", &real_spec, name);
println!("Import: {}", path_plus_name);
info!(context, "Import: {}", path_plus_name);
if dc_poke_eml_file(context, path_plus_name).is_ok() {
read_cnt += 1
}
@@ -163,10 +161,13 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
}
}
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
info!(
context,
"Import: {} items read from \"{}\".", read_cnt, &real_spec
);
if read_cnt > 0 {
context.call_cb(Event::MsgsChanged {
chat_id: ChatId::new(0),
chat_id: 0,
msg_id: MsgId::new(0),
});
}
@@ -187,7 +188,8 @@ fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
};
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text();
println!(
info!(
context,
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]",
prefix.as_ref(),
msg.get_id(),
@@ -221,14 +223,16 @@ fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
let mut lines_out = 0;
for &msg_id in msglist {
if msg_id.is_daymarker() {
println!(
info!(
context,
"--------------------------------------------------------------------------------"
);
lines_out += 1
} else if !msg_id.is_special() {
if lines_out == 0 {
println!(
info!(
context,
"--------------------------------------------------------------------------------",
);
lines_out += 1
@@ -238,7 +242,8 @@ fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
}
}
if lines_out > 0 {
println!(
info!(
context,
"--------------------------------------------------------------------------------"
);
}
@@ -288,7 +293,7 @@ fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
);
}
println!("Contact#{}: {}{}", contact_id, line, line2);
info!(context, "Contact#{}: {}{}", contact_id, line, line2);
}
}
}
@@ -299,7 +304,7 @@ fn chat_prefix(chat: &Chat) -> &'static str {
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let chat_id = *context.cmdline_sel_chat_id.read().unwrap();
let mut sel_chat = if !chat_id.is_unset() {
let mut sel_chat = if chat_id > 0 {
Chat::load_from_db(context, chat_id).ok()
} else {
None
@@ -484,7 +489,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
println!("{:#?}", context.get_info());
}
"interrupt" => {
interrupt_inbox_idle(context);
interrupt_inbox_idle(context, true);
}
"maybenetwork" => {
maybe_network(context);
@@ -503,18 +508,20 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let cnt = chatlist.len();
if cnt > 0 {
println!(
info!(
context,
"================================================================================"
);
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
println!(
info!(
context,
"{}#{}: {} [{} fresh]",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
chat.get_id().get_fresh_msg_cnt(context),
chat::get_fresh_msg_cnt(context, chat.get_id()),
);
let lot = chatlist.get_summary(context, i, Some(&chat));
let statestr = if chat.is_archived() {
@@ -531,7 +538,8 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let timestr = dc_timestamp_to_str(lot.get_timestamp());
let text1 = lot.get_text1();
let text2 = lot.get_text2();
println!(
info!(
context,
"{}{}{}{} [{}]{}",
text1.unwrap_or(""),
if text1.is_some() { ": " } else { "" },
@@ -544,13 +552,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
""
},
);
println!(
info!(
context,
"================================================================================"
);
}
}
if location::is_sending_locations_to_chat(context, ChatId::new(0)) {
println!("Location streaming enabled.");
if location::is_sending_locations_to_chat(context, 0) {
info!(context, "Location streaming enabled.");
}
println!("{} chats", cnt);
}
@@ -559,8 +568,8 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
bail!("Argument [chat-id] is missing.");
}
if !arg1.is_empty() {
let chat_id = ChatId::new(arg1.parse()?);
println!("Selecting chat {}", chat_id);
let chat_id = arg1.parse()?;
println!("Selecting chat #{}", chat_id);
sel_chat = Some(Chat::load_from_db(context, chat_id)?);
*context.cmdline_sel_chat_id.write().unwrap() = chat_id;
}
@@ -578,7 +587,8 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
} else {
format!("{} member(s)", members.len())
};
println!(
info!(
context,
"{}#{}: {} [{}]{}{}",
chat_prefix(sel_chat),
sel_chat.get_id(),
@@ -598,11 +608,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
},
);
log_msglist(context, &msglist)?;
if let Some(draft) = sel_chat.get_id().get_draft(context)? {
if let Some(draft) = chat::get_draft(context, sel_chat.get_id())? {
log_msg(context, "Draft", &draft);
}
println!("{} messages.", sel_chat.get_id().get_msg_cnt(context));
println!(
"{} messages.",
chat::get_msg_cnt(context, sel_chat.get_id())
);
chat::marknoticed_chat(context, sel_chat.get_id())?;
}
"createchat" => {
@@ -678,7 +691,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(sel_chat.is_some(), "No chat selected.");
let contacts = chat::get_chat_contacts(context, sel_chat.as_ref().unwrap().get_id());
println!("Memberlist:");
info!(context, "Memberlist:");
log_contactlist(context, &contacts);
println!(
@@ -704,7 +717,8 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let default_marker = "-".to_string();
for location in &locations {
let marker = location.marker.as_ref().unwrap_or(&default_marker);
println!(
info!(
context,
"Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} {} {}",
location.location_id,
dc_timestamp_to_str(location.timestamp),
@@ -718,7 +732,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
);
}
if locations.is_empty() {
println!("No locations.");
info!(context, "No locations.");
}
}
"sendlocations" => {
@@ -784,7 +798,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let chat = if let Some(ref sel_chat) = sel_chat {
sel_chat.get_id()
} else {
ChatId::new(0)
0 as libc::c_uint
};
let msglist = context.search_msgs(chat, arg1);
@@ -798,14 +812,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
if !arg1.is_empty() {
let mut draft = Message::new(Viewtype::Text);
draft.set_text(Some(arg1.to_string()));
sel_chat
.as_ref()
.unwrap()
.get_id()
.set_draft(context, Some(&mut draft));
chat::set_draft(
context,
sel_chat.as_ref().unwrap().get_id(),
Some(&mut draft),
);
println!("Draft saved.");
} else {
sel_chat.as_ref().unwrap().get_id().set_draft(context, None);
chat::set_draft(context, sel_chat.as_ref().unwrap().get_id(), None);
println!("Draft deleted.");
}
}
@@ -843,13 +857,13 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
}
"archive" | "unarchive" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id.set_archived(context, arg0 == "archive")?;
let chat_id = arg1.parse()?;
chat::archive(context, chat_id, arg0 == "archive")?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id.delete(context)?;
let chat_id = arg1.parse()?;
chat::delete(context, chat_id)?;
}
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
@@ -870,7 +884,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
);
let mut msg_ids = [MsgId::new(0); 1];
let chat_id = ChatId::new(arg2.parse()?);
let chat_id = arg2.parse()?;
msg_ids[0] = MsgId::new(arg1.parse()?);
chat::forward_msgs(context, &msg_ids, chat_id)?;
}
@@ -922,14 +936,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let contact = Contact::get_by_id(context, contact_id)?;
let name_n_addr = contact.get_name_n_addr();
let mut res = format!(
"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(),
}
);
let mut res = format!("Contact info for: {}:\n\n", name_n_addr);
res += &Contact::get_encrinfo(context, contact_id)?;

View File

@@ -9,17 +9,21 @@ extern crate deltachat;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate rusqlite;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use std::sync::{Arc, RwLock};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use deltachat::chat::ChatId;
use deltachat::config;
use deltachat::configure::*;
use deltachat::context::*;
use deltachat::job::*;
use deltachat::oauth2::*;
use deltachat::securejoin::*;
use deltachat::Event;
@@ -37,7 +41,7 @@ use self::cmdline::*;
// Event Handler
fn receive_event(_context: &Context, event: Event) {
fn receive_event(_context: &Context, event: Event) -> libc::uintptr_t {
match event {
Event::Info(msg) => {
/* do not show the event as this would fill the screen */
@@ -107,8 +111,111 @@ fn receive_event(_context: &Context, event: Event) {
print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event);
}
}
0
}
// Threads for waiting for messages and for jobs
lazy_static! {
static ref HANDLE: Arc<Mutex<Option<Handle>>> = Arc::new(Mutex::new(None));
static ref IS_RUNNING: AtomicBool = AtomicBool::new(true);
}
struct Handle {
handle_imap: Option<std::thread::JoinHandle<()>>,
handle_mvbox: Option<std::thread::JoinHandle<()>>,
handle_sentbox: Option<std::thread::JoinHandle<()>>,
handle_smtp: Option<std::thread::JoinHandle<()>>,
}
macro_rules! while_running {
($code:block) => {
if IS_RUNNING.load(Ordering::Relaxed) {
$code
} else {
break;
}
};
}
fn start_threads(c: Arc<RwLock<Context>>) {
if HANDLE.clone().lock().unwrap().is_some() {
return;
}
println!("Starting threads");
IS_RUNNING.store(true, Ordering::Relaxed);
let ctx = c.clone();
let handle_imap = std::thread::spawn(move || loop {
while_running!({
perform_inbox_jobs(&ctx.read().unwrap());
perform_inbox_fetch(&ctx.read().unwrap());
while_running!({
let context = ctx.read().unwrap();
perform_inbox_idle(&context);
});
});
});
let ctx = c.clone();
let handle_mvbox = std::thread::spawn(move || loop {
while_running!({
perform_mvbox_fetch(&ctx.read().unwrap());
while_running!({
perform_mvbox_idle(&ctx.read().unwrap());
});
});
});
let ctx = c.clone();
let handle_sentbox = std::thread::spawn(move || loop {
while_running!({
perform_sentbox_fetch(&ctx.read().unwrap());
while_running!({
perform_sentbox_idle(&ctx.read().unwrap());
});
});
});
let ctx = c;
let handle_smtp = std::thread::spawn(move || loop {
while_running!({
perform_smtp_jobs(&ctx.read().unwrap());
while_running!({
perform_smtp_idle(&ctx.read().unwrap());
});
});
});
*HANDLE.clone().lock().unwrap() = Some(Handle {
handle_imap: Some(handle_imap),
handle_mvbox: Some(handle_mvbox),
handle_sentbox: Some(handle_sentbox),
handle_smtp: Some(handle_smtp),
});
}
fn stop_threads(context: &Context) {
if let Some(ref mut handle) = *HANDLE.clone().lock().unwrap() {
println!("Stopping threads");
IS_RUNNING.store(false, Ordering::Relaxed);
interrupt_inbox_idle(context, true);
interrupt_mvbox_idle(context);
interrupt_sentbox_idle(context);
interrupt_smtp_idle(context);
handle.handle_imap.take().unwrap().join().unwrap();
handle.handle_mvbox.take().unwrap().join().unwrap();
handle.handle_sentbox.take().unwrap().join().unwrap();
handle.handle_smtp.take().unwrap().join().unwrap();
}
}
// === The main loop
struct DcHelper {
completer: FilenameCompleter,
highlighter: MatchingBracketHighlighter,
@@ -314,7 +421,9 @@ fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
}
rl.save_history(".dc-history.txt")?;
println!("history saved");
ctx.read().unwrap().shutdown();
{
stop_threads(&ctx.read().unwrap());
}
Ok(())
}
@@ -332,20 +441,28 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
match arg0 {
"connect" => {
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
start_threads(ctx);
}
"disconnect" => {
ctx.read().unwrap().shutdown();
stop_threads(&ctx.read().unwrap());
}
"smtp-jobs" => {
if HANDLE.clone().lock().unwrap().is_some() {
println!("smtp-jobs are already running in a thread.",);
} else {
perform_smtp_jobs(&ctx.read().unwrap());
}
}
"imap-jobs" => {
if HANDLE.clone().lock().unwrap().is_some() {
println!("inbox-jobs are already running in a thread.");
} else {
perform_inbox_jobs(&ctx.read().unwrap());
}
}
"configure" => {
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
ctx.read().unwrap().configure();
start_threads(ctx.clone());
configure(&ctx.read().unwrap());
}
"oauth2" => {
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
@@ -368,15 +485,10 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
print!("\x1b[1;1H\x1b[2J");
}
"getqr" | "getbadqr" => {
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
if let Some(mut qr) = dc_get_securejoin_qr(
&ctx.read().unwrap(),
ChatId::new(arg1.parse().unwrap_or_default()),
) {
start_threads(ctx.clone());
if let Some(mut qr) =
dc_get_securejoin_qr(&ctx.read().unwrap(), arg1.parse().unwrap_or_default())
{
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
@@ -392,12 +504,8 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
}
}
"joinqr" => {
start_threads(ctx.clone());
if !arg0.is_empty() {
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
dc_join_securejoin(&ctx.read().unwrap(), arg1);
}
}

View File

@@ -1,27 +1,36 @@
extern crate deltachat;
use std::sync::{Arc, RwLock};
use std::{thread, time};
use tempfile::tempdir;
use deltachat::chat;
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::configure::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::job::{
perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle,
perform_smtp_jobs,
};
use deltachat::Event;
fn cb(_ctx: &Context, event: Event) {
fn cb(_ctx: &Context, event: Event) -> usize {
print!("[{:?}]", event);
match event {
Event::ConfigureProgress(progress) => {
print!(" progress: {}\n", progress);
0
}
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
print!(" {}\n", msg);
0
}
_ => {
print!("\n");
0
}
}
}
@@ -30,52 +39,77 @@ fn main() {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
println!("creating database {:?}", dbfile);
let ctx = Context::new("FakeOs".into(), dbfile).expect("Failed to create context");
let ctx =
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context");
let running = Arc::new(RwLock::new(true));
let info = ctx.get_info();
let duration = time::Duration::from_millis(8000);
let duration = time::Duration::from_millis(4000);
println!("info: {:#?}", info);
crossbeam::scope(|s| {
let t1 = s.spawn(|_| {
ctx.run(cb);
});
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);
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();
ctx.configure();
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,);
if *r1.read().unwrap() {
perform_inbox_idle(&ctx1);
}
}
}
});
thread::sleep(duration);
let ctx1 = ctx.clone();
let r1 = running.clone();
let t2 = thread::spawn(move || {
while *r1.read().unwrap() {
perform_smtp_jobs(&ctx1);
if *r1.read().unwrap() {
perform_smtp_idle(&ctx1);
}
}
});
println!("stopping threads");
ctx.shutdown();
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);
println!("joining");
t1.join().unwrap();
thread::sleep(duration);
println!("closing");
})
.unwrap();
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,);
}
thread::sleep(duration);
println!("stopping threads");
*running.write().unwrap() = false;
deltachat::job::interrupt_inbox_idle(&ctx, true);
deltachat::job::interrupt_smtp_idle(&ctx);
println!("joining");
t1.join().unwrap();
t2.join().unwrap();
println!("closing");
}

View File

@@ -1,10 +1,6 @@
0.700.0
0.600.1
---------
- lots of new Python APIs
- use rust core-beta23
- introduce automatic versioning via setuptools_scm,
based on py-X.Y.Z tags

View File

@@ -3,54 +3,89 @@ deltachat python bindings
=========================
This package provides bindings to the deltachat-core_ Rust -library
which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
a low-level Chat/Contact/Message API to user interfaces and bots.
which provides imap/smtp/crypto handling as well as chat/group/messages
handling to Android, Desktop and IO user interfaces.
Installing pre-built packages (linux-only)
==========================================
If you have a linux system you may install the ``deltachat`` binary "wheel" package
without any "build-from-source" steps.
1. `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
then create a fresh python environment and activate it in your shell::
virtualenv venv # or: python -m venv
source venv/bin/activate
Afterwards, invoking ``python`` or ``pip install`` will only
modify files in your ``venv`` directory and leave your system installation
alone.
2. Install the wheel for linux::
pip install deltachat
Verify it worked by typing::
python -c "import deltachat"
Installing bindings from source (Updated: 20-Jan-2020)
=========================================================
Installing a wheel from a PR/branch
---------------------------------------
Install Rust and Cargo first. Deltachat needs a specific nightly
version, the easiest is probably to first install Rust stable from
rustup and then use this to install the correct nightly version.
For Linux, we automatically build wheels for all github PR branches
and push them to a python package index. To install the latest github ``master`` branch::
Bootstrap Rust and Cargo by using rustup::
pip install -i https://m.devpi.net/dc/master deltachat
curl https://sh.rustup.rs -sSf | sh
.. note::
Then GIT clone the deltachat-core-rust repo and get the actual
rust- and cargo-toolchain needed by deltachat::
If you can help to automate the building of wheels for Mac or Windows,
that'd be much appreciated! please then get
`in contact with us <https://delta.chat/en/contribute>`_.
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
rustup show
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
Installing bindings from source
===============================
Ensure you are in the deltachat-core-rust/python directory, create the
virtual environment and activate it in your shell::
If you can't use "binary" method above then you need to compile
to core deltachat library::
cd python
python3 -m venv venv # or: virtualenv venv
source venv/bin/activate
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
cd python
You should now be able to build the python bindings using the supplied script::
If you don't have one active, create and activate a python "virtualenv":
./install_python_bindings.py
python virtualenv venv # or python -m venv
source venv/bin/activate
Afterwards ``which python`` tells you that it comes out of the "venv"
directory that contains all python install artifacts. Let's first
install test tools::
pip install pytest pytest-timeout pytest-rerunfailures requests
then cargo-build and install the deltachat bindings::
python install_python_bindings.py
The installation might take a while, depending on your machine.
The bindings will be installed in release mode but with debug symbols.
The release mode is currently necessary because some tests generate RSA keys
which is prohibitively slow in non-release mode.
The release mode is necessary because some tests generate RSA keys
which is prohibitively slow in debug mode.
After successful binding installation you can install a few more
Python packages before running the tests::
After successful binding installation you can finally run the tests::
python -m pip install pytest pytest-timeout pytest-rerunfailures requests
pytest -v tests
.. note::
Some tests are sometimes failing/hanging because of
https://github.com/deltachat/deltachat-core-rust/issues/331
and
https://github.com/deltachat/deltachat-core-rust/issues/326
running "live" tests (experimental)
-----------------------------------
@@ -69,38 +104,6 @@ With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real
e-mail accounts and run through all functional "liveconfig" tests.
Installing pre-built packages (Linux-only)
========================================================
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps.
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
then create a fresh Python virtual environment and activate it in your shell::
virtualenv venv # or: python -m venv
source venv/bin/activate
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``venv`` directory and leaves
your system installation alone.
For Linux, we automatically build wheels for all github PR branches
and push them to a python package index. To install the latest
github ``master`` branch::
pip install --pre -i https://m.devpi.net/dc/master deltachat
To verify it worked::
python -c "import deltachat"
.. note::
If you can help to automate the building of wheels for Mac or Windows,
that'd be much appreciated! please then get
`in contact with us <https://delta.chat/en/contribute>`_.
Code examples
=============
@@ -112,11 +115,15 @@ You may look at `examples <https://py.delta.chat/examples.html>`_.
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
Building manylinux1 based wheels
================================
Building manylinux1 wheels
==========================
.. note::
This section may not fully work.
Building portable manylinux1 wheels which come with libdeltachat.so
can be done with docker-tooling.
and all it's dependencies is easy using the provided docker tooling.
using docker pull / premade images
------------------------------------
@@ -129,9 +136,9 @@ organization::
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e DCC_PY_LIVECONFIG \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
$ bash ci_scripts/ci_run.sh
This command runs tests and build-wheel scripts in a docker container.
Optionally build your own docker image

View File

@@ -1,7 +0,0 @@
from __future__ import print_function
from deltachat import capi
from deltachat.capi import ffi, lib
if __name__ == "__main__":
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
lib.dc_context_shutdown(ctx)

View File

@@ -13,7 +13,6 @@ def main():
"root": "..",
"relative_to": __file__,
'tag_regex': r'^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
'git_describe_command': "git describe --dirty --tags --long --match py-*.*",
},
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
long_description=long_description,

View File

@@ -3,7 +3,6 @@
from __future__ import print_function
import atexit
import threading
import os
import re
import time
from array import array
@@ -26,7 +25,7 @@ class Account(object):
by the underlying deltachat core library. All public Account methods are
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, debug=True):
""" initialize account object.
:param db_path: a path to the account database. The database
@@ -34,11 +33,10 @@ class Account(object):
:param logid: an optional logging prefix that should be used with
the default internal logging.
: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(
lib.dc_context_new(ffi.NULL, as_dc_charpointer(os_name)),
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
_destroy_dc_context,
)
if eventlogging:
@@ -96,12 +94,9 @@ class Account(object):
"""
self._check_config_key(name)
name = name.encode("utf8")
value = value.encode("utf8")
if name == b"addr" and self.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)
def get_config(self, name):
@@ -137,18 +132,6 @@ class Account(object):
"""
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):
""" Raise ValueError if this account is not configured. """
if not self.is_configured():
@@ -380,6 +363,8 @@ class Account(object):
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():
lib.dc_perform_imap_jobs(self._dc_context)
files_written = []
while True:
ev = self._imex_events.get()
@@ -408,6 +393,8 @@ class Account(object):
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():
lib.dc_perform_imap_jobs(self._dc_context)
if not self._imex_events.get():
raise ValueError("import from path '{}' failed".format(path))
@@ -486,7 +473,7 @@ class Account(object):
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
return self.get_message_by_id(ev[2])
def start_threads(self):
def start_threads(self, mvbox=False, sentbox=False):
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
@@ -494,7 +481,7 @@ class Account(object):
"""
if not self.is_configured():
self.configure()
self._threads.start()
self._threads.start(mvbox=mvbox, sentbox=sentbox)
def stop_threads(self, wait=True):
""" stop IMAP/SMTP threads. """
@@ -556,9 +543,16 @@ class IOThreads:
def is_started(self):
return len(self._name2thread) > 0
def start(self):
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
assert not self.is_started()
self._start_one_thread("deltachat", self.dc_thread_run)
if imap:
self._start_one_thread("inbox", 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:
self._start_one_thread("smtp", self.smtp_thread_run)
def _start_one_thread(self, name, func):
self._name2thread[name] = t = threading.Thread(target=func, name=name)
@@ -566,18 +560,48 @@ class IOThreads:
t.start()
def stop(self, wait=False):
lib.dc_context_shutdown(self._dc_context)
self._thread_quitflag = True
lib.dc_interrupt_imap_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:
for name, thread in self._name2thread.items():
thread.join()
def dc_thread_run(self):
self._log_event("py-bindings-info", 0, "DC THREAD START")
lib.dc_context_run(self._dc_context, lib.py_dc_callback)
def imap_thread_run(self):
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_imap_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_idle(self._dc_context)
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):
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
while not self._thread_quitflag:
lib.dc_perform_smtp_jobs(self._dc_context)
lib.dc_perform_smtp_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
class EventLogger:
_loglock = threading.RLock()

View File

@@ -109,30 +109,6 @@ class Chat(object):
# ------ 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):
""" send a text message and return the resulting Message instance.
@@ -154,12 +130,9 @@ class Chat(object):
:raises ValueError: if message can not be send/chat does not exist.
:returns: the resulting :class:`deltachat.message.Message` instance
"""
msg = Message.new_empty(self.account, view_type="file")
msg.set_file(path, mime_type)
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")
return Message.from_db(self.account, sent_id)
msg = self.prepare_message_file(path=path, mime_type=mime_type)
self.send_prepared(msg)
return msg
def send_image(self, path):
""" send an image message and return the resulting Message instance.
@@ -169,12 +142,9 @@ class Chat(object):
:returns: the resulting :class:`deltachat.message.Message` instance
"""
mime_type = mimetypes.guess_type(path)[0]
msg = Message.new_empty(self.account, view_type="image")
msg.set_file(path, mime_type)
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")
return Message.from_db(self.account, sent_id)
msg = self.prepare_message_file(path=path, mime_type=mime_type, view_type="image")
self.send_prepared(msg)
return msg
def prepare_message(self, msg):
""" create a new prepared message.

View File

@@ -68,6 +68,7 @@ DC_LP_SMTP_SOCKET_SSL = 0x20000
DC_LP_SMTP_SOCKET_PLAIN = 0x40000
DC_CERTCK_AUTO = 0
DC_CERTCK_STRICT = 1
DC_CERTCK_ACCEPT_INVALID_HOSTNAMES = 2
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3
DC_EMPTY_MVBOX = 0x01
DC_EMPTY_INBOX = 0x02

View File

@@ -47,13 +47,3 @@ class Contact(object):
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)
def get_profile_image(self):
"""Get contact profile image.
:returns: path to profile image, None if no profile image exists.
"""
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)

View File

@@ -174,7 +174,7 @@ class Message(object):
@property
def _msgstate(self):
if self.id == 0:
dc_msg = self._dc_msg
dc_msg = self.message._dc_msg
else:
# load message from db to get a fresh/current state
dc_msg = ffi.gc(

View File

@@ -85,21 +85,16 @@ class SessionLiveConfigFromFile:
class SessionLiveConfigFromURL:
def __init__(self, url, create_token):
self.configlist = []
self.url = url
self.create_token = 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)})
for i in range(2):
res = requests.post(url, json={"token_create_user": int(create_token)})
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
return config
def get(self, index):
return self.configlist[index]
def exists(self):
return bool(self.configlist)
@@ -182,7 +177,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
def get_online_configuring_account(self, mvbox=False, sentbox=False):
ac, configdict = self.get_online_config()
ac.configure(**configdict)
ac.start_threads()
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
return ac
def get_one_online_account(self):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -16,14 +16,6 @@ class TestOfflineAccountBasic:
with pytest.raises(ValueError):
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):
ac1 = acfactory.get_unconfigured_account()
d = ac1.get_info()
@@ -176,7 +168,6 @@ class TestOfflineChat:
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
ac1.start_threads()
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
ac1._evlogger.consume_events()
with pytest.raises(ValueError):
@@ -196,7 +187,6 @@ class TestOfflineChat:
assert not chat.is_promoted()
msg = chat.get_draft()
assert msg.text == "xyz title1"
ac1.stop_threads()
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
@@ -392,23 +382,6 @@ class TestOfflineChat:
assert not res.is_ask_verifygroup()
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:
def get_chat(self, ac1, ac2, both_created=False):
@@ -483,7 +456,10 @@ class TestOnlineAccount:
msg1 = Message.new_empty(ac1, "file")
msg1.set_text("withfile")
msg1.set_file(p)
chat.send_msg(msg1)
message = chat.prepare_message(msg1)
assert message.is_out_preparing()
assert message.text == "withfile"
chat.send_prepared(message)
lp.sec("ac2: receive message")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
@@ -642,9 +618,6 @@ class TestOnlineAccount:
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")
chat = self.get_chat(ac1, ac2)
@@ -662,7 +635,6 @@ class TestOnlineAccount:
msg_in = ac2.get_message_by_id(msg_out.id)
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")
chat2 = msg_in.chat
@@ -680,26 +652,14 @@ class TestOnlineAccount:
chat2b.mark_noticed()
assert chat2b.count_fresh_messages() == 0
ac2._evlogger.consume_events()
lp.sec("sending a second message from ac1 to ac2")
msg_out2 = chat.send_text("message2")
lp.sec("wait for ac2 to receive second message")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
assert ev[2] == msg_out2.id
msg_in2 = ac2.get_message_by_id(msg_out2.id)
lp.sec("mark messages as seen on ac2, wait for changes on ac1")
ac2.mark_seen_messages([msg_in, msg_in2])
lp.sec("mark message as seen on ac2, wait for changes on ac1")
ac2.mark_seen_messages([msg_in])
lp.step("1")
for i in range(2):
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_READ")
assert ev[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev[2] > const.DC_MSG_ID_LAST_SPECIAL
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_READ")
assert ev[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev[2] > const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
assert msg_out.is_out_mdn_received()
assert msg_out2.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()
@@ -709,41 +669,6 @@ class TestOnlineAccount:
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")
# MDN should be moved even though MDNs are already disabled
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
assert len(chat.get_messages()) == 1
# MDN is received even though MDNs are already disabled
assert msg_out.is_out_mdn_received()
def test_send_and_receive_will_encrypt_decrypt(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -772,12 +697,6 @@ class TestOnlineAccount:
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")))
@@ -843,21 +762,7 @@ class TestOnlineAccount:
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")
# Set unprepared and unencrypted draft to test that it is not
# taken into account when determining whether last message is
# encrypted.
msg_draft = Message.new_empty(ac1, "text")
msg_draft.set_text("message2 -- should be encrypted")
chat.set_draft(msg_draft)
# Get the draft, prepare and send it.
msg_draft = chat.get_draft()
msg_out = chat.prepare_message(msg_draft)
chat.send_prepared(msg_out)
chat.set_draft(None)
assert chat.get_draft() is None
chat.send_text("message2 -- should be encrypted")
lp.sec("wait for ac2 to receive message")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
@@ -1053,54 +958,7 @@ class TestOnlineAccount:
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):
def test_set_get_profile_image(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("create unpromoted group chat")
@@ -1202,104 +1060,6 @@ class TestOnlineAccount:
assert not locations3
class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):
lp.sec("creating and configuring five accounts")
accounts = [acfactory.get_online_configuring_account() for i in range(5)]
for acc in accounts:
wait_configuration_progress(acc, 1000)
ac1 = accounts.pop()
lp.sec("ac1: setting up contacts with 4 other members")
contacts = []
for acc, name in zip(accounts, list("äöüsr")):
contact = ac1.create_contact(acc.get_config("addr"), name=name)
contacts.append(contact)
# make sure we accept the "hi" message
ac1.create_chat_by_contact(contact)
# make sure the other side accepts our messages
c1 = acc.create_contact(ac1.get_config("addr"), "ä member")
chat1 = acc.create_chat_by_contact(c1)
# send a message to get the contact key via autocrypt header
chat1.send_text("hi")
msg = ac1.wait_next_incoming_message()
assert msg.text == "hi"
# Save fifth account for later
ac5 = accounts.pop()
contact5 = contacts.pop()
lp.sec("ac1: creating group chat with 3 other members")
chat = ac1.create_group_chat("title1")
for contact in contacts:
chat.add_contact(contact)
assert not chat.is_promoted()
lp.sec("ac1: send mesage to new group chat")
msg = chat.send_text("hello")
assert chat.is_promoted()
assert msg.is_encrypted()
gossiped_timestamp = chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp > 0
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("ac3: checking that 'ac4' is a known contact")
ac3 = accounts[1]
msg3 = ac3.wait_next_incoming_message()
assert msg3.text == "hello"
ac3_contacts = ac3.get_contacts()
assert len(ac3_contacts) == 3
ac4_contacts = ac3.get_contacts(query=accounts[2].get_config("addr"))
assert len(ac4_contacts) == 1
lp.sec("ac2: removing one contact")
to_remove = contacts[-1]
msg.chat.remove_contact(to_remove)
lp.sec("ac1: receiving system message about contact removal")
sysmsg = ac1.wait_next_incoming_message()
assert to_remove.addr in sysmsg.text
assert len(sysmsg.chat.get_contacts()) == 3
# Receiving message about removed contact does not reset gossip
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: sending another message to the chat")
chat.send_text("hello2")
msg = ac2.wait_next_incoming_message()
assert msg.text == "hello2"
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: adding fifth member to the chat")
chat.add_contact(contact5)
# Additng contact to chat resets gossiped_timestamp
assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp
lp.sec("ac2: receiving system message about contact addition")
sysmsg = ac2.wait_next_incoming_message()
assert contact5.addr in sysmsg.text
assert len(sysmsg.chat.get_contacts()) == 4
lp.sec("ac5: waiting for message about addition to the chat")
sysmsg = ac5.wait_next_incoming_message()
msg = sysmsg.chat.send_text("hello!")
# Message should be encrypted because keys of other members are gossiped
assert msg.is_encrypted()
class TestOnlineConfigureFails:
def test_invalid_password(self, acfactory):
ac1, configdict = acfactory.get_online_config()

View File

@@ -1,49 +1,10 @@
from __future__ import print_function
import os.path
import shutil
import pytest
from filecmp import cmp
from conftest import wait_configuration_progress, wait_msgs_changed
from deltachat import const
from conftest import wait_configuration_progress, wait_msgs_changed
class TestOnlineInCreation:
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):
ac1 = 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?
lp.sec("create a message with a file in creation")
orig = data.get_path("d.png")
path = os.path.join(ac1.get_blobdir(), 'd.png')
with open(path, "x") as fp:
fp.write("preparing")
path = data.get_path("d.png")
prepared_original = chat.prepare_message_file(path)
assert prepared_original.is_out_preparing()
wait_msgs_changed(ac1, chat.id, prepared_original.id)
@@ -80,7 +38,6 @@ class TestOnlineInCreation:
lp.sec("finish creating the file and send it")
assert prepared_original.is_out_preparing()
shutil.copyfile(orig, path)
chat.send_prepared(prepared_original)
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
wait_msgs_changed(ac1, chat.id, prepared_original.id)
@@ -102,11 +59,11 @@ class TestOnlineInCreation:
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
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")
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev2[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev2[1] != ev1[1]
received_copy = ac2.get_message_by_id(ev2[2])
assert cmp(received_copy.filename, orig, shallow=False)
assert cmp(received_copy.filename, path, False)

View File

@@ -1,48 +1,25 @@
from __future__ import print_function
import threading
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
from deltachat.capi import ffi
from deltachat.capi import lib
from deltachat.account import EventLogger
class EventThread(threading.Thread):
def __init__(self, dc_context):
self.dc_context = dc_context
super(EventThread, self).__init__()
self.setDaemon(1)
def run(self):
lib.dc_context_run(self.dc_context, lib.py_dc_callback)
def stop(self):
lib.dc_context_shutdown(self.dc_context)
def test_empty_context():
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL)
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
capi.lib.dc_close(ctx)
def test_callback_None2int():
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL)
set_context_callback(ctx, lambda *args: None)
capi.lib.dc_close(ctx)
clear_context_callback(ctx)
def test_start_stop_event_thread_basic():
print("1")
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
print("2")
ev_thread = EventThread(ctx)
print("3 -- starting event thread")
ev_thread.start()
print("4 -- stopping event thread")
ev_thread.stop()
def test_dc_close_events(tmpdir):
ctx = ffi.gc(
capi.lib.dc_context_new(ffi.NULL, ffi.NULL),
capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
evlog = EventLogger(ctx)
@@ -51,9 +28,6 @@ def test_dc_close_events(tmpdir):
ctx,
lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)
)
ev_thread = EventThread(ctx)
ev_thread.start()
p = tmpdir.join("hello.db")
lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL)
capi.lib.dc_close(ctx)
@@ -72,12 +46,11 @@ def test_dc_close_events(tmpdir):
find("disconnecting mvbox-thread")
find("disconnecting SMTP")
find("Database closed")
ev_thread.stop()
def test_wrong_db(tmpdir):
dc_context = ffi.gc(
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
p = tmpdir.join("hello.db")
@@ -89,7 +62,7 @@ def test_wrong_db(tmpdir):
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(ffi.NULL, ffi.NULL),
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("hello.db")
@@ -146,7 +119,7 @@ def test_provider_info_none():
def test_get_info_closed():
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, ffi.NULL),
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))
@@ -156,7 +129,7 @@ def test_get_info_closed():
def test_get_info_open(tmpdir):
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("test.db")
@@ -168,7 +141,7 @@ def test_get_info_open(tmpdir):
def test_is_open_closed():
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_is_open(ctx) == 0
@@ -176,7 +149,7 @@ def test_is_open_closed():
def test_is_open_actually_open(tmpdir):
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("test.db")

View File

@@ -8,7 +8,7 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
python tests/package_wheels.py {toxworkdir}/wheelhouse
# python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
DCC_RS_DEV
@@ -65,11 +65,11 @@ commands =
[pytest]
addopts = -v -ra
python_files = tests/test_*.py
addopts = -v -rs
python_files = tests/test_*.py
norecursedirs = .tox
xfail_strict=true
timeout = 90
timeout = 60
timeout_method = thread
[flake8]

View File

@@ -1 +1 @@
nightly-2020-01-26
nightly-2019-11-06

View File

@@ -55,9 +55,7 @@ if __name__ == "__main__":
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"])
subprocess.call(["cargo", "update", "-p", "deltachat"])
print("after commit make sure to: ")
print("")

32
spec.md
View File

@@ -1,6 +1,6 @@
# Chat-over-Email specification
Version 0.20.0
Version 0.19.0
This document describes how emails can be used
to implement typical messenger functions
@@ -153,8 +153,8 @@ The message-id MUST have the format `Gr.<group-id>.<unique data>`.
Hello group - this group contains three members
Messengers adding the member list in the form `Name <email-address>`
MUST take care only to distribute the names authorized by the contacts themselves.
Otherwise, names as _Daddy_ or _Honey_ may be distributed
MUST take care only to spread the names authorized by the contacts themselves.
Otherwise, names as _Daddy_ or _Honey_ may be spread
(this issue is also true for normal MUAs, however,
for more contact- and chat-centralized apps
such situations happen more frequently).
@@ -248,11 +248,11 @@ and the message SHOULD appear as a message or action from the sender.
A group MAY have a group-image.
To change or set the group-image,
the messenger MUST attach an image file to a message
and MUST add the header `Chat-Group-Avatar`
and MUST add the header `Chat-Group-Image`
with the value set to the image name.
To remove the group-image,
the messenger MUST add the header `Chat-Group-Avatar: 0`.
the messenger MUST add the header `Chat-Group-Image: 0`.
The messenger SHOULD send an explicit mail for each group image change.
The body of the message SHOULD contain
@@ -265,7 +265,7 @@ and the message SHOULD appear as a message or action from the sender.
Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ
Chat-Group-Name: Our Group
Chat-Group-Avatar: image.jpg
Chat-Group-Image: image.jpg
Message-ID: Gr.12345uvwxyZ.0005@domain
Subject: Chat: Our Group: Hello, ...
Content-Type: multipart/mixed; boundary="==break=="
@@ -283,25 +283,25 @@ and the message SHOULD appear as a message or action from the sender.
The image format SHOULD be image/jpeg or image/png.
To save data, it is RECOMMENDED
to add a `Chat-Group-Avatar` only on image changes.
to add a `Chat-Group-Image` only on image changes.
# Set profile image
A user MAY have a profile-image that MAY be distributed to their contacts.
A user MAY have a profile-image that MAY be spread to his contacts.
To change or set the profile-image,
the messenger MUST attach an image file to a message
and MUST add the header `Chat-User-Avatar`
and MUST add the header `Chat-Profile-Image`
with the value set to the image name.
To remove the profile-image,
the messenger MUST add the header `Chat-User-Avatar: 0`.
the messenger MUST add the header `Chat-Profile-Image: 0`.
To distribute the image,
To spread the image,
the messenger MAY send the profile image
together with the next mail to a given contact
(to do this only once,
the messenger has to keep a `user_avatar_update_state` somewhere).
the messenger has to keep a `profile_image_update_state` somewhere).
Alternatively, the messenger MAY send an explicit mail
for each profile-image change to all contacts using a compatible messenger.
The messenger SHOULD NOT send an explicit mail to normal MUAs.
@@ -309,7 +309,7 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Chat-User-Avatar: photo.jpg
Chat-Profile-Image: photo.jpg
Subject: Chat: Hello, ...
Content-Type: multipart/mixed; boundary="==break=="
@@ -325,10 +325,10 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
--==break==--
The image format SHOULD be image/jpeg or image/png.
Note that `Chat-User-Avatar` may appear together with all other headers,
eg. there may be a `Chat-User-Avatar` and a `Chat-Group-Avatar` header
Note that `Chat-Profile-Image` may appear together with all other headers,
eg. there may be a `Chat-Profile-Image` and a `Chat-Group-Image` header
in the same message.
To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
To save data, it is RECOMMENDED to add a `Chat-Profile-Image` header
only on image changes.

View File

@@ -6,13 +6,9 @@ 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
@@ -163,7 +159,7 @@ impl<'a> BlobObject<'a> {
/// 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(
pub fn create_from_path(
context: &Context,
src: impl AsRef<Path>,
) -> std::result::Result<BlobObject, BlobError> {
@@ -322,12 +318,13 @@ impl<'a> BlobObject<'a> {
let clean = sanitize_filename::sanitize_with_options(name, opts);
let mut iter = clean.splitn(2, '.');
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
let ext: String = iter.next().unwrap_or_default().chars().take(32).collect();
if ext.is_empty() {
(stem, "".to_string())
} else {
(stem, format!(".{}", ext).to_lowercase())
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()),
}
}
@@ -352,31 +349,6 @@ impl<'a> BlobObject<'a> {
}
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> {
@@ -410,13 +382,6 @@ pub enum BlobError {
cause: std::io::Error,
backtrace: failure::Backtrace,
},
RecodeFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
cause: image::ImageError,
backtrace: failure::Backtrace,
},
WrongBlobdir {
blobdir: PathBuf,
src: PathBuf,
@@ -464,9 +429,6 @@ impl fmt::Display for BlobError {
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 {}",
@@ -597,14 +559,14 @@ mod tests {
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();
let blob = BlobObject::create_from_path(&t.ctx, &src_ext).unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/external");
let data = fs::read(blob.to_abs_path()).unwrap();
assert_eq!(data, b"boo");
let src_int = t.ctx.get_blobdir().join("internal");
fs::write(&src_int, b"boo").unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_int).unwrap();
let blob = BlobObject::create_from_path(&t.ctx, &src_int).unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
let data = fs::read(blob.to_abs_path()).unwrap();
assert_eq!(data, b"boo");
@@ -614,7 +576,7 @@ mod tests {
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();
let blob = BlobObject::create_from_path(&t.ctx, &src_ext).unwrap();
assert_eq!(
blob.as_name(),
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
@@ -630,11 +592,4 @@ mod tests {
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
}
#[test]
fn test_sanitise_name() {
let (_, ext) =
BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
assert_eq!(ext, ".txt");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@ use crate::stock::StockMessage;
#[derive(Debug)]
pub struct Chatlist {
/// Stores pairs of `chat_id, message_id`
ids: Vec<(ChatId, MsgId)>,
ids: Vec<(u32, MsgId)>,
}
impl Chatlist {
@@ -91,7 +91,7 @@ impl Chatlist {
let mut add_archived_link_item = false;
let process_row = |row: &rusqlite::Row| {
let chat_id: ChatId = row.get(0)?;
let chat_id: u32 = row.get(0)?;
let msg_id: MsgId = row.get(1).unwrap_or_default();
Ok((chat_id, msg_id))
};
@@ -162,12 +162,6 @@ impl Chatlist {
let query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query");
// allow searching over special names that may change at any time
// when the ui calls set_stock_translation()
if let Err(err) = update_special_chat_names(context) {
warn!(context, "cannot update special chat names: {:?}", err)
}
let str_like_cmd = format!("%{}%", query);
context.sql.query_map(
"SELECT c.id, m.id
@@ -211,10 +205,7 @@ impl Chatlist {
)?;
if 0 == listflags & DC_GCL_NO_SPECIALS {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) {
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
}
add_archived_link_item = true;
}
@@ -223,9 +214,9 @@ impl Chatlist {
if add_archived_link_item && dc_get_archived_cnt(context) > 0 {
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
}
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
}
Ok(Chatlist { ids })
@@ -244,9 +235,9 @@ impl Chatlist {
/// Get a single chat ID of a chatlist.
///
/// To get the message object from the message ID, use dc_get_chat().
pub fn get_chat_id(&self, index: usize) -> ChatId {
pub fn get_chat_id(&self, index: usize) -> u32 {
if index >= self.ids.len() {
return ChatId::new(0);
return 0;
}
self.ids[index].0
}
@@ -310,7 +301,7 @@ impl Chatlist {
None
};
if chat.id.is_archived_link() {
if chat.id == DC_CHAT_ID_ARCHIVED_LINK {
ret.text2 = None;
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
{
@@ -323,7 +314,7 @@ impl Chatlist {
}
}
/// Returns the number of archived chats
/// Get the number of archived chats
pub fn dc_get_archived_cnt(context: &Context) -> u32 {
context
.sql
@@ -358,6 +349,7 @@ fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
mod tests {
use super::*;
use crate::chat;
use crate::test_utils::*;
#[test]
@@ -377,7 +369,7 @@ mod tests {
// drafts are sorted to the top
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
chat_id2.set_draft(&t.ctx, Some(&mut msg));
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);
@@ -388,31 +380,8 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
assert_eq!(chats.len(), 0);
chat_id1.set_archived(&t.ctx, true).ok();
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);
}
#[test]
fn test_search_special_chat_names() {
let t = dummy_context();
t.ctx.update_device_chats().unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap();
assert_eq!(chats.len(), 0);
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
assert_eq!(chats.len(), 0);
t.ctx
.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap();
assert_eq!(chats.len(), 1);
t.ctx
.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
assert_eq!(chats.len(), 1);
}
}

View File

@@ -8,9 +8,7 @@ use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::job::*;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage;
use rusqlite::NO_PARAMS;
/// The available configuration keys.
#[derive(
@@ -99,7 +97,7 @@ impl Context {
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", 24 * 1024 * 1024 / 4 * 3)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(self, key),
};
@@ -129,23 +127,13 @@ impl Context {
/// 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<()> {
match key {
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", NO_PARAMS)?;
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::Selfavatar if value.is_some() => {
let blob = BlobObject::create_from_path(&self, value.unwrap())?;
self.sql.set_raw_config(self, key, Some(blob.as_name()))
}
Config::InboxWatch => {
let ret = self.sql.set_raw_config(self, key, value);
interrupt_inbox_idle(self);
interrupt_inbox_idle(self, true);
ret
}
Config::SentboxWatch => {
@@ -191,11 +179,7 @@ mod tests {
use std::str::FromStr;
use std::string::ToString;
use crate::constants::AVATAR_SIZE;
use crate::test_utils::*;
use image::GenericImageView;
use std::fs::File;
use std::io::Write;
#[test]
fn test_to_string() {
@@ -215,55 +199,30 @@ mod tests {
}
#[test]
fn test_selfavatar_outside_blobdir() {
fn test_selfavatar() -> 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)
.unwrap()
.write_all(avatar_bytes)
.unwrap();
std::fs::write(&avatar_src, b"avatar")?;
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists());
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.unwrap();
.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);
assert_eq!(std::fs::read(&avatar_blob)?, b"avatar");
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
assert_eq!(img.width(), 1000);
assert_eq!(img.height(), 1000);
let img = image::open(avatar_blob).unwrap();
assert_eq!(img.width(), AVATAR_SIZE);
assert_eq!(img.height(), AVATAR_SIZE);
Ok(())
}
#[test]
fn test_selfavatar_in_blobdir() {
fn test_selfavatar_in_blobdir() -> failure::Fallible<()> {
let t = dummy_context();
let avatar_src = t.ctx.get_blobdir().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
File::create(&avatar_src)
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let img = image::open(&avatar_src).unwrap();
assert_eq!(img.width(), 900);
assert_eq!(img.height(), 900);
let avatar_src = t.ctx.get_blobdir().join("avatar.jpg");
std::fs::write(&avatar_src, b"avatar")?;
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.unwrap();
.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()));
let img = image::open(avatar_src).unwrap();
assert_eq!(img.width(), AVATAR_SIZE);
assert_eq!(img.height(), AVATAR_SIZE);
Ok(())
}
}

View File

@@ -6,16 +6,15 @@ mod read_url;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use async_std::task;
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::job;
use crate::login_param::{CertificateChecks, LoginParam};
use crate::job::*;
use crate::login_param::LoginParam;
use crate::oauth2::*;
use crate::param::Params;
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
@@ -30,19 +29,34 @@ macro_rules! progress {
};
}
// connect
pub fn configure(context: &Context) {
if context.has_ongoing() {
warn!(context, "There is already another ongoing process running.",);
return;
}
job_kill_action(context, Action::ConfigureImap);
job_add(context, Action::ConfigureImap, 0, Params::new(), 0);
}
/// Check if the context is already configured.
pub fn dc_is_configured(context: &Context) -> bool {
context.sql.get_raw_config_bool(context, "configured")
}
/*******************************************************************************
* Configure JOB
******************************************************************************/
#[allow(non_snake_case, unused_must_use, clippy::cognitive_complexity)]
pub fn JobConfigureImap(context: &Context) -> job::Status {
#[allow(non_snake_case, unused_must_use)]
pub fn JobConfigureImap(context: &Context) {
if !context.sql.is_open() {
error!(context, "Cannot configure, database not opened.",);
progress!(context, 0);
return job::Status::Finished(Err(format_err!("Database not opened")));
return;
}
if !context.alloc_ongoing() {
progress!(context, 0);
return job::Status::Finished(Err(format_err!("Cannot allocated ongoing process")));
return;
}
let mut success = false;
let mut imap_connected_here = false;
@@ -78,11 +92,9 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
let mut param_domain = "undefined.undefined".to_owned();
let mut param_addr_urlencoded: String =
"Internal Error: this value should never be used".to_owned();
let mut keep_flags = 0;
const STEP_12_USE_AUTOCONFIG: u8 = 12;
const STEP_13_AFTER_AUTOCONFIG: u8 = 13;
let mut keep_flags = std::i32::MAX;
const STEP_3_INDEX: u8 = 13;
let mut step_counter: u8 = 0;
while !context.shall_stop_ongoing() {
step_counter += 1;
@@ -98,7 +110,7 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
}
// Step 1: Load the parameters and check email-address and password
2 => {
if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 {
if 0 != param.server_flags & 0x2 {
// the used oauth2 addr may differ, check this.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation,
// just use the given one.
@@ -133,7 +145,6 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
// Step 2: Autoconfig
4 => {
progress!(context, 200);
if param.mail_server.is_empty()
&& param.mail_port == 0
/*&&param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then */
@@ -141,18 +152,12 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
&& param.send_port == 0
&& param.send_user.is_empty()
/*&&param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for autoconfig or not */
&& (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0
&& param.server_flags & !0x2 == 0
{
// no advanced parameters entered by the user: query provider-database or do Autoconfig
keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2;
if let Some(new_param) = get_offline_autoconfig(context, &param) {
// got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting
param_autoconfig = Some(new_param);
step_counter = STEP_12_USE_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop
}
keep_flags = param.server_flags & 0x2;
} else {
// advanced parameters entered by the user: skip Autoconfig
step_counter = STEP_13_AFTER_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop
// Autoconfig is not needed so skip it.
step_counter = STEP_3_INDEX - 1;
}
true
}
@@ -184,7 +189,10 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
7 => {
progress!(context, 310);
if param_autoconfig.is_none() {
let url = format!("https://{}/autodiscover/autodiscover.xml", param_domain);
let url = format!(
"https://{}{}/autodiscover/autodiscover.xml",
"", param_domain
);
param_autoconfig = outlk_autodiscover(context, &url, &param).ok();
}
true
@@ -234,10 +242,8 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
}
true
}
/* C. Do we have any autoconfig result?
If you change the match-number here, also update STEP_12_COPY_AUTOCONFIG above
*/
STEP_12_USE_AUTOCONFIG => {
/* C. Do we have any result? */
12 => {
progress!(context, 500);
if let Some(ref cfg) = param_autoconfig {
info!(context, "Got autoconfig: {}", &cfg);
@@ -250,15 +256,15 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
param.send_port = cfg.send_port;
param.send_user = cfg.send_user.clone();
param.server_flags = cfg.server_flags;
/* although param_autoconfig's data are no longer needed from,
it is used to later to prevent trying variations of port/server/logins */
/* although param_autoconfig's data are no longer needed from, it is important to keep the object as
we may enter "deep guessing" if we could not read a configuration */
}
param.server_flags |= keep_flags;
true
}
// Step 3: Fill missing fields with defaults
// If you change the match-number here, also update STEP_13_AFTER_AUTOCONFIG above
STEP_13_AFTER_AUTOCONFIG => {
13 => {
// if you move this, don't forget to update STEP_3_INDEX, too
if param.mail_server.is_empty() {
param.mail_server = format!("imap.{}", param_domain,)
}
@@ -422,43 +428,6 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
context.free_ongoing();
progress!(context, if success { 1000 } else { 0 });
job::Status::Finished(Ok(()))
}
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
// XXX we don't have https://github.com/deltachat/provider-db APIs
// integrated yet but we'll already add nauta as a first use case, also
// showing what we need from provider-db in the future.
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if param.addr.ends_with("@nauta.cu") {
let mut p = LoginParam::new();
p.addr = param.addr.clone();
p.mail_server = "imap.nauta.cu".to_string();
p.mail_user = param.addr.clone();
p.mail_pw = param.mail_pw.clone();
p.mail_port = 143;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.send_server = "smtp.nauta.cu".to_string();
p.send_user = param.addr.clone();
p.send_pw = param.mail_pw.clone();
p.send_port = 25;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags = DC_LP_AUTH_NORMAL as i32
| DC_LP_IMAP_SOCKET_STARTTLS as i32
| DC_LP_SMTP_SOCKET_STARTTLS as i32;
info!(context, "found offline autoconfig: {}", p);
Some(p)
} else {
info!(context, "no offline autoconfig found");
None
}
}
fn try_imap_connections(
@@ -516,22 +485,17 @@ fn try_imap_connection(
fn try_imap_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
let inf = format!(
"imap: {}@{}:{} flags=0x{:x} certificate_checks={}",
param.mail_user,
param.mail_server,
param.mail_port,
param.server_flags,
param.imap_certificate_checks
"imap: {}@{}:{} flags=0x{:x}",
param.mail_user, param.mail_server, param.mail_port, param.server_flags
);
info!(context, "Trying: {}", inf);
if task::block_on(
context
.inbox_thread
.read()
.unwrap()
.imap
.connect(context, &param),
) {
if context
.inbox_thread
.read()
.unwrap()
.imap
.connect(context, &param)
{
info!(context, "success: {}", inf);
return Some(true);
}
@@ -603,7 +567,6 @@ fn try_smtp_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::*;
use crate::configure::JobConfigureImap;
use crate::test_utils::*;
@@ -617,19 +580,4 @@ mod tests {
t.ctx.set_config(Config::MailPw, Some("123456")).unwrap();
JobConfigureImap(&t.ctx);
}
#[test]
fn test_get_offline_autoconfig() {
let context = dummy_context().ctx;
let mut params = LoginParam::new();
params.addr = "someone123@example.org".to_string();
assert!(get_offline_autoconfig(&context, &params).is_none());
let mut params = LoginParam::new();
params.addr = "someone123@nauta.cu".to_string();
let found_params = get_offline_autoconfig(&context, &params).unwrap();
assert_eq!(found_params.mail_server, "imap.nauta.cu".to_string());
assert_eq!(found_params.send_server, "smtp.nauta.cu".to_string());
}
}

View File

@@ -11,10 +11,10 @@ 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::blocking::Client::new()
match reqwest::Client::new()
.get(url)
.send()
.and_then(|res| res.text())
.and_then(|mut res| res.text())
{
Ok(res) => Ok(res),
Err(err) => {

View File

@@ -1,5 +1,5 @@
//! # Constants
#![allow(dead_code)]
#![allow(non_camel_case_types, dead_code)]
use deltachat_derive::*;
use lazy_static::lazy_static;
@@ -49,8 +49,6 @@ pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
pub(crate) const DC_FROM_HANDSHAKE: i32 = 0x01;
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
@@ -60,8 +58,9 @@ pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
pub const DC_GCL_VERIFIED_ONLY: usize = 0x01;
pub const DC_GCL_ADD_SELF: usize = 0x02;
// unchanged user avatars are resent to the recipients every some days
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
// values for DC_PARAM_FORCE_PLAINTEXT
pub(crate) const DC_FP_NO_AUTOCRYPT_HEADER: i32 = 2;
pub(crate) const DC_FP_ADD_AUTOCRYPT_HEADER: i32 = 1;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
@@ -112,7 +111,7 @@ pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// approx. max. length returned by dc_msg_get_text()
const DC_MAX_GET_TEXT_LEN: usize = 30000;
/// approx. max. length returned by dc_get_msg_info()
const DC_MAX_GET_INFO_LEN: usize = 100_000;
const DC_MAX_GET_INFO_LEN: usize = 100000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
@@ -120,10 +119,6 @@ pub const DC_CONTACT_ID_INFO: u32 = 2;
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
// decorative address that is used for DC_CONTACT_ID_DEVICE
// 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;
@@ -182,14 +177,10 @@ pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
pub const DC_BOB_ERROR: i32 = 0;
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)]
#[repr(i32)]
pub enum Viewtype {
Unknown = 0,
/// Text message.
/// The text of the message is set using dc_msg_set_text()
/// and retrieved with dc_msg_get_text().

View File

@@ -1,5 +1,3 @@
//! Contacts module
use std::path::PathBuf;
use deltachat_derive::*;
@@ -7,19 +5,16 @@ use itertools::Itertools;
use rusqlite;
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::error::{Error, Result};
use crate::error::Result;
use crate::events::Event;
use crate::key::*;
use crate::login_param::LoginParam;
use crate::message::{MessageState, MsgId};
use crate::mimeparser::AvatarAction;
use crate::param::*;
use crate::peerstate::*;
use crate::sql;
use crate::stock::StockMessage;
@@ -49,28 +44,20 @@ pub struct Contact {
///
/// Normal contact IDs are larger than these special ones (larger than DC_CONTACT_ID_LAST_SPECIAL).
pub id: u32,
/// Contact name. It is recommended to use `Contact::get_name`,
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
/// May be empty, initially set to `authname`.
name: String,
/// 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`,
/// to access this field.
authname: String,
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr`` to access this field.
addr: String,
/// Blocked state. Use dc_contact_is_blocked to access this field.
pub blocked: bool,
blocked: bool,
/// The origin/source of the contact.
pub origin: Origin,
/// Parameters as Param::ProfileImage
pub param: Params,
}
/// Possible origins of a contact.
@@ -80,54 +67,38 @@ pub struct Contact {
#[repr(i32)]
pub enum Origin {
Unknown = 0,
/// From: of incoming messages of unknown sender
IncomingUnknownFrom = 0x10,
/// Cc: of incoming messages of unknown sender
IncomingUnknownCc = 0x20,
/// To: of incoming messages of unknown sender
IncomingUnknownTo = 0x40,
/// address scanned but not verified
UnhandledQrScan = 0x80,
/// Reply-To: of incoming message of known sender
IncomingReplyTo = 0x100,
/// Cc: of incoming message of known sender
IncomingCc = 0x200,
/// additional To:'s of incoming message of known sender
IncomingTo = 0x400,
/// a chat was manually created for this user, but no message yet sent
CreateChat = 0x800,
/// message sent by us
OutgoingBcc = 0x1000,
/// message sent by us
OutgoingCc = 0x2000,
/// message sent by us
OutgoingTo = 0x4000,
/// internal use
Internal = 0x40000,
/// address is in our address book
AdressBook = 0x80000,
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
SecurejoinInvited = 0x0100_0000,
SecurejoinInvited = 0x1000000,
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
SecurejoinJoined = 0x0200_0000,
/// contact added mannually by dc_create_contact(), this should be the largest origin as otherwise the user cannot modify the names
ManuallyCreated = 0x0400_0000,
SecurejoinJoined = 0x2000000,
/// contact added mannually by dc_create_contact(), this should be the largets origin as otherwise the user cannot modify the names
ManuallyCreated = 0x4000000,
}
impl Default for Origin {
@@ -137,11 +108,14 @@ impl Default for Origin {
}
impl Origin {
/// Contacts that are known, i. e. they came in via accepted contacts or
/// themselves an accepted contact. Known contacts are shown in the
/// contact list when one creates a chat and wants to add members etc.
pub fn is_known(self) -> bool {
self >= Origin::IncomingReplyTo
/// Contacts that are verified and known not to be spam.
pub fn is_verified(self) -> bool {
self as i32 >= 0x100
}
/// Contacts that are shown in the contact list.
pub fn include_in_contactlist(self) -> bool {
self as i32 >= DC_ORIGIN_MIN_CONTACT_LIST
}
}
@@ -165,10 +139,32 @@ pub enum VerifiedStatus {
impl Contact {
pub fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
let mut res = context.sql.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param
FROM contacts c
WHERE c.id=?;",
if contact_id == DC_CONTACT_ID_SELF {
let contact = Contact {
id: contact_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);
} else if contact_id == DC_CONTACT_ID_DEVICE {
let contact = Contact {
id: contact_id,
name: context.stock_str(StockMessage::DeviceMessages).into(),
authname: "".into(),
addr: "device@localhost".into(),
blocked: false,
origin: Origin::Unknown,
};
return Ok(contact);
}
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],
|row| {
let contact = Self {
@@ -178,21 +174,10 @@ impl Contact {
addr: row.get::<_, String>(1)?,
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
origin: row.get(2)?,
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
};
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.
@@ -263,7 +248,7 @@ impl Contact {
.is_ok()
{
context.call_cb(Event::MsgsChanged {
chat_id: ChatId::new(0),
chat_id: 0,
msg_id: MsgId::new(0),
});
}
@@ -411,30 +396,18 @@ impl Contact {
}
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 {
if origin == Origin::IncomingUnknownFrom {
update_authname = true;
}
if sql::execute(
context,
&context.sql,
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
params![
name.as_ref(),
addr,
origin,
if update_authname { name.as_ref() } else { "" }
],
)
.is_ok()
{
row_id = sql::get_rowid(context, &context.sql, "contacts", "addr", addr);
sth_modified = Modifier::Created;
info!(context, "added contact id={} addr={}", row_id, addr);
} else {
error!(context, "Cannot add contact.");
}
error!(context, "Cannot add contact.");
}
Ok((row_id, sth_modified))
@@ -518,7 +491,7 @@ impl Contact {
params![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
Origin::IncomingReplyTo,
0x100,
&s3str_like_cmd,
&s3str_like_cmd,
if flag_verified_only { 0 } else { 1 },
@@ -736,16 +709,6 @@ impl Contact {
Ok(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.
pub fn get_id(&self) -> u32 {
self.id
@@ -779,9 +742,6 @@ impl Contact {
if !self.name.is_empty() {
return &self.name;
}
if !self.authname.is_empty() {
return &self.authname;
}
&self.addr
}
@@ -817,11 +777,8 @@ impl Contact {
if let Some(p) = context.get_config(Config::Selfavatar) {
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
}
@@ -904,6 +861,22 @@ impl Contact {
.unwrap_or_default() as usize
}
pub fn get_origin_by_id(context: &Context, contact_id: u32, ret_blocked: &mut i32) -> Origin {
let mut ret = Origin::Unknown;
*ret_blocked = 0;
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
/* we could optimize this by loading only the needed fields */
if contact.blocked {
*ret_blocked = 1;
} else {
ret = contact.origin;
}
}
ret
}
pub fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
if !context.sql.is_open() || contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return false;
@@ -984,32 +957,6 @@ fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
}
}
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.
///
/// - Remove quotes (come from some bad MUA implementations)
@@ -1074,18 +1021,6 @@ 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 {
let norm1 = addr_normalize(addr1.as_ref()).to_lowercase();
let norm2 = addr_normalize(addr2.as_ref()).to_lowercase();
@@ -1093,6 +1028,15 @@ pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
norm1 == norm2
}
pub fn addr_equals_self(context: &Context, addr: impl AsRef<str>) -> bool {
if !addr.as_ref().is_empty() {
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr) {
return addr_cmp(addr, self_addr);
}
}
false
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
book.lines()
.chunks(2)
@@ -1176,18 +1120,6 @@ mod tests {
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()
@@ -1277,63 +1209,6 @@ mod tests {
assert!(!contact.is_blocked());
}
#[test]
fn test_remote_authnames() {
let t = dummy_context();
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob1",
"bob@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Created);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "bob1");
assert_eq!(contact.get_name(), "bob1");
assert_eq!(contact.get_display_name(), "bob1");
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob2",
"bob@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "bob2");
assert_eq!(contact.get_display_name(), "bob2");
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t.ctx, "bob3", "bob@example.org", Origin::ManuallyCreated)
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob4",
"bob@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "bob4");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
}
#[test]
fn test_addr_cmp() {
assert!(addr_cmp("AA@AA.ORG", "aa@aa.ORG"));

View File

@@ -1,15 +1,11 @@
//! Context module
//! Contacts module
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Condvar, Mutex, RwLock,
};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use crossbeam::channel::select;
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
use libc::uintptr_t;
use crate::chat::*;
use crate::config::Config;
@@ -18,12 +14,12 @@ use crate::contact::*;
use crate::error::*;
use crate::events::Event;
use crate::imap::*;
use crate::job;
use crate::job::*;
use crate::job_thread::JobThread;
use crate::key::*;
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId};
use crate::message::{self, Message, MsgId};
use crate::param::Params;
use crate::smtp::Smtp;
use crate::sql::Sql;
@@ -36,9 +32,14 @@ use crate::sql::Sql;
/// * `event` - One of the [Event] items.
/// * `data1` - Depends on the event parameter, see [Event].
/// * `data2` - Depends on the event parameter, see [Event].
pub type ContextCallback = dyn Fn(&Context, Event) -> () + Send + Sync;
///
/// # Returns
///
/// This callback must return 0 unless stated otherwise in the event
/// description at [Event].
pub type ContextCallback = dyn Fn(&Context, Event) -> uintptr_t + Send + Sync;
#[derive(Debug)]
#[derive(DebugStub)]
pub struct Context {
/// Database file path
dbfile: PathBuf,
@@ -53,21 +54,16 @@ pub struct Context {
pub smtp: Arc<Mutex<Smtp>>,
pub smtp_state: Arc<(Mutex<SmtpState>, Condvar)>,
pub oauth2_critical: Arc<Mutex<()>>,
#[debug_stub = "Callback"]
cb: Box<ContextCallback>,
pub os_name: Option<String>,
pub cmdline_sel_chat_id: Arc<RwLock<ChatId>>,
pub cmdline_sel_chat_id: Arc<RwLock<u32>>,
pub bob: Arc<RwLock<BobStatus>>,
pub last_smeared_timestamp: RwLock<i64>,
pub running_state: Arc<RwLock<RunningState>>,
/// Mutex to avoid generating the key for the user more than once.
pub generating_key_mutex: Mutex<()>,
pub translated_stockstrings: RwLock<HashMap<usize, String>>,
event_sender: Sender<Event>,
event_receiver: Receiver<Event>,
shutdown_sender: Sender<()>,
shutdown_receiver: Receiver<()>,
is_running: AtomicBool,
}
#[derive(Debug, PartialEq, Eq)]
@@ -86,24 +82,23 @@ 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("arch", (std::mem::size_of::<usize>() * 8).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
}
macro_rules! while_running {
($self:expr, $code:block) => {
if $self.is_running.load(Ordering::Relaxed) {
$code
} else {
break;
}
};
}
impl Context {
/// Creates new context.
pub fn new(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();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -111,22 +106,24 @@ impl Context {
if !blobdir.exists() {
std::fs::create_dir_all(&blobdir)?;
}
Context::with_blobdir(os_name, dbfile, blobdir)
Context::with_blobdir(cb, os_name, dbfile, blobdir)
}
pub fn with_blobdir(os_name: String, dbfile: PathBuf, blobdir: PathBuf) -> Result<Context> {
pub fn with_blobdir(
cb: Box<ContextCallback>,
os_name: String,
dbfile: PathBuf,
blobdir: PathBuf,
) -> Result<Context> {
ensure!(
blobdir.is_dir(),
"Blobdir does not exist: {}",
blobdir.display()
);
let (event_sender, event_receiver) = unbounded();
let (shutdown_sender, shutdown_receiver) = bounded(0);
let ctx = Context {
blobdir,
dbfile,
cb,
os_name: Some(os_name),
running_state: Arc::new(RwLock::new(Default::default())),
sql: Sql::new(),
@@ -135,7 +132,7 @@ impl Context {
oauth2_critical: Arc::new(Mutex::new(())),
bob: Arc::new(RwLock::new(Default::default())),
last_smeared_timestamp: RwLock::new(0),
cmdline_sel_chat_id: Arc::new(RwLock::new(ChatId::new(0))),
cmdline_sel_chat_id: Arc::new(RwLock::new(0)),
inbox_thread: Arc::new(RwLock::new(JobThread::new(
"INBOX",
"configured_inbox_folder",
@@ -155,11 +152,6 @@ impl Context {
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
event_sender,
event_receiver,
shutdown_sender,
shutdown_receiver,
is_running: Default::default(),
};
ensure!(
@@ -180,99 +172,8 @@ impl Context {
self.blobdir.as_path()
}
pub fn call_cb(&self, event: Event) {
self.event_sender.send(event).unwrap();
}
/// Start the run loop.
pub fn run<F>(&self, cb: F)
where
F: Fn(&Context, Event) -> () + Send + Sync,
{
if self.is_running.swap(true, Ordering::Relaxed) {
panic!("Run can only be called once");
}
crossbeam::scope(|s| {
let imap_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_inbox_jobs(self);
while_running!(self, {
job::perform_inbox_fetch(self);
while_running!(self, { job::perform_inbox_idle(self) });
});
});
});
let mvbox_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_mvbox_fetch(self);
while_running!(self, {
job::perform_mvbox_idle(self);
});
});
});
let sentbox_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_sentbox_fetch(self);
while_running!(self, {
job::perform_sentbox_idle(self);
});
});
});
let smtp_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_smtp_jobs(self);
while_running!(self, {
job::perform_smtp_idle(self);
});
});
});
loop {
select! {
recv(self.event_receiver) -> event => {
// This gurantees that the callback is always called from the thread
// that called `run`.
cb(self, event.unwrap())
},
recv(self.shutdown_receiver) -> _ => break,
}
}
imap_handle.join().unwrap();
mvbox_handle.join().unwrap();
sentbox_handle.join().unwrap();
smtp_handle.join().unwrap();
})
.unwrap()
}
/// Stop the run loop. Blocks until all threads have shutdown.
pub fn shutdown(&self) {
if !self.is_running.swap(false, Ordering::Relaxed) {
// already shutdown
return;
}
self.shutdown_sender.send(()).unwrap();
job::interrupt_inbox_idle(self);
job::interrupt_mvbox_idle(self);
job::interrupt_sentbox_idle(self);
job::interrupt_smtp_idle(self);
}
pub fn configure(&self) {
if self.has_ongoing() {
warn!(self, "There is already another ongoing process running.");
return;
}
job::job_kill_action(self, job::Action::ConfigureImap);
job::job_add(self, job::Action::ConfigureImap, 0, Params::new(), 0);
}
/// Check if the context is already configured.
pub fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured")
pub fn call_cb(&self, event: Event) -> uintptr_t {
(*self.cb)(self, event)
}
/*******************************************************************************
@@ -397,11 +298,6 @@ impl Context {
res.insert("database_version", dbversion.to_string());
res.insert("blobdir", self.get_blobdir().display().to_string());
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("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());
@@ -460,7 +356,7 @@ impl Context {
}
#[allow(non_snake_case)]
pub fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
pub fn search_msgs(&self, chat_id: u32, query: impl AsRef<str>) -> Vec<MsgId> {
let real_query = query.as_ref().trim();
if real_query.is_empty() {
return Vec::new();
@@ -468,7 +364,7 @@ impl Context {
let strLikeInText = format!("%{}%", real_query);
let strLikeBeg = format!("{}%", real_query);
let query = if !chat_id.is_unset() {
let query = if 0 != chat_id {
concat!(
"SELECT m.id AS id, m.timestamp AS timestamp",
" FROM msgs m",
@@ -500,7 +396,7 @@ impl Context {
self.sql
.query_map(
query,
params![chat_id, &strLikeInText, &strLikeBeg],
params![chat_id as i32, &strLikeInText, &strLikeBeg],
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
@@ -551,17 +447,15 @@ impl Context {
return;
}
match msg.is_dc_message {
MessengerMessage::No => {}
MessengerMessage::Yes | MessengerMessage::Reply => {
job::job_add(
self,
job::Action::MoveMsg,
msg.id.to_u32() as i32,
Params::new(),
0,
);
}
// 1 = dc message, 2 = reply to dc message
if 0 != msg.is_dc_message {
job_add(
self,
Action::MoveMsg,
msg.id.to_u32() as i32,
Params::new(),
0,
);
}
}
}
@@ -634,7 +528,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new("FakeOs".into(), dbfile);
let res = Context::new(Box::new(|_, _| 0), "FakeOs".into(), dbfile);
assert!(res.is_err());
}
@@ -649,7 +543,7 @@ mod tests {
fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new("FakeOS".into(), dbfile).unwrap();
Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile).unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
@@ -660,7 +554,7 @@ mod tests {
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new("FakeOS".into(), dbfile);
let res = Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile);
assert!(res.is_err());
}
@@ -670,7 +564,7 @@ mod tests {
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new("FakeOS".into(), dbfile).unwrap();
Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile).unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
@@ -680,7 +574,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir("FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}
@@ -689,7 +583,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir("FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}

View File

@@ -2,6 +2,7 @@ use crate::location::Location;
/* * the structure behind dc_array_t */
#[derive(Debug, Clone)]
#[allow(non_camel_case_types)]
pub enum dc_array_t {
Locations(Vec<Location>),
Uint(Vec<u32>),

File diff suppressed because it is too large Load Diff

273
src/dc_simplify.rs Normal file
View File

@@ -0,0 +1,273 @@
use crate::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, &line) in lines.iter().enumerate() {
// quoted-printable may encode `-- ` to `-- =20` which is converted
// back to `-- `
match line {
"-- " | "-- " => return (ix, false),
"--" | "---" | "----" => return (ix, true),
_ => (),
}
}
(lines.len(), false)
}
impl Simplify {
pub fn new() -> Self {
Simplify {
is_forwarded: false,
}
}
/// Simplify and normalise text: Remove quotes, signatures, unnecessary
/// 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 {
dehtml(input)
} else {
input.to_string()
};
out.retain(|c| c != '\r');
out = self.simplify_plain_text(&out, is_msgrmsg);
out.retain(|c| c != '\r');
out
}
/**
* Simplify Plain Text
*/
#[allow(non_snake_case, clippy::mut_range_bound)]
fn simplify_plain_text(&mut self, buf_terminated: &str, is_msgrmsg: bool) -> String {
/* This function ...
... 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 `>`
... remove a non-empty line before the removed quote (contains sth. like "On 2.9.2016, Bjoern wrote:" in different formats and lanugages) */
/* split the given buffer into lines */
let lines: Vec<_> = buf_terminated.split('\n').collect();
let mut l_first: usize = 0;
let mut is_cut_at_begin = false;
let (mut l_last, mut is_cut_at_end) = find_message_footer(&lines);
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
}
}
for l in l_first..l_last {
let line = lines[l];
if line == "-----"
|| line == "_____"
|| line == "====="
|| line == "*****"
|| line == "~~~~~"
{
l_last = l;
is_cut_at_end = true;
/* done */
break;
}
}
if !is_msgrmsg {
let mut l_lastQuotedLine = None;
for l in (l_first..l_last).rev() {
let line = lines[l];
if is_plain_quote(line) {
l_lastQuotedLine = Some(l)
} else if !is_empty_line(line) {
break;
}
}
if let Some(last_quoted_line) = l_lastQuotedLine {
l_last = last_quoted_line;
is_cut_at_end = true;
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
l_last -= 1
}
if l_last > 1 {
let line = lines[l_last - 1];
if is_quoted_headline(line) {
l_last -= 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 = 0;
let mut content_lines_added = 0;
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
}
}
/**
* Tools
*/
fn is_empty_line(buf: &str) -> bool {
// XXX: can it be simplified to buf.chars().all(|c| c.is_whitespace())?
//
// Strictly speaking, it is not equivalent (^A is not whitespace, but less than ' '),
// but having control sequences in email body?!
//
// See discussion at: https://github.com/deltachat/deltachat-core-rust/pull/402#discussion_r317062392
for c in buf.chars() {
if c > ' ' {
return false;
}
}
true
}
fn is_quoted_headline(buf: &str) -> bool {
/* This function may be called for the line _directly_ before a quote.
The function checks if the line contains sth. like "On 01.02.2016, xy@z wrote:" in various languages.
- Currently, we simply check if the last character is a ':'.
- Checking for the existence of an email address may fail (headlines may show the user's name instead of the address) */
buf.len() <= 80 && buf.ends_with(':')
}
fn is_plain_quote(buf: &str) -> bool {
buf.starts_with('>')
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
// proptest does not support [[:graphical:][:space:]] regex.
fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") {
let output = Simplify::new().simplify_plain_text(&input, true);
assert!(output.split('\n').all(|s| s != "-- "));
}
}
#[test]
fn test_simplify_trim() {
let mut simplify = Simplify::new();
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "line1\nline2");
}
#[test]
fn test_simplify_parse_href() {
let mut simplify = Simplify::new();
let html = "<a href=url>text</a";
let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "[text](url)");
}
#[test]
fn test_simplify_bold_text() {
let mut simplify = Simplify::new();
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "text *bold*<>");
}
#[test]
fn test_simplify_html_encoded() {
let mut simplify = Simplify::new();
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = simplify.simplify(html, true, false);
assert_eq!(
plain,
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
);
}
#[test]
fn test_simplify_utilities() {
assert!(is_empty_line(" \t"));
assert!(is_empty_line(""));
assert!(is_empty_line(" \r"));
assert!(!is_empty_line(" x"));
assert!(is_plain_quote("> hello world"));
assert!(is_plain_quote(">>"));
assert!(!is_plain_quote("Life is pain"));
assert!(!is_plain_quote(""));
}
}

View File

@@ -49,8 +49,8 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Co
/// - harmonize together while being different enough
/// (therefore, we cannot just use random rgb colors :)
const COLORS: [u32; 16] = [
0xe5_65_55, 0xf2_8c_48, 0x8e_85_ee, 0x76_c8_4d, 0x5b_b6_cc, 0x54_9c_dd, 0xd2_5c_99, 0xb3_78_00,
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
0xe56555, 0xf28c48, 0x8e85ee, 0x76c84d, 0x5bb6cc, 0x549cdd, 0xd25c99, 0xb37800, 0xf23030,
0x39b249, 0xbb243b, 0x964078, 0x66874f, 0x308ab9, 0x127ed0, 0xbe450c,
];
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
@@ -59,7 +59,7 @@ pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
let bytes = str_lower.as_bytes();
for (i, byte) in bytes.iter().enumerate() {
checksum += (i + 1) * *byte as usize;
checksum %= 0x00ff_ffff;
checksum %= 0xffffff;
}
let color_index = checksum % COLORS.len();
@@ -182,6 +182,22 @@ fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
String::from_utf8(wrapped_writer).unwrap()
}
pub(crate) fn dc_create_incoming_rfc724_mid(
message_timestamp: i64,
contact_id_from: u32,
contact_ids_to: &[u32],
) -> Option<String> {
/* create a deterministic rfc724_mid from input such that
repeatedly calling it with the same input results in the same Message-id */
let largest_id_to = contact_ids_to.iter().max().copied().unwrap_or_default();
let result = format!(
"{}-{}-{}@stub",
message_timestamp, contact_id_from, largest_id_to
);
Some(result)
}
/// Function generates a Message-ID that can be used for a new outgoing message.
/// - this function is called for all outgoing messages.
/// - the message ID should be globally unique
@@ -222,7 +238,45 @@ pub(crate) fn dc_extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
None
}
// Function returns a sanitized basename that does not contain
// win/linux path separators and also not any non-ascii chars
fn get_safe_basename(filename: &str) -> String {
// return the (potentially mangled) basename of the input filename
// this might be a path that comes in from another operating system
let mut index: usize = 0;
if let Some(unix_index) = filename.rfind('/') {
index = unix_index + 1;
}
if let Some(win_index) = filename.rfind('\\') {
index = max(index, win_index + 1);
}
if index >= filename.len() {
"nobasename".to_string()
} else {
// we don't allow any non-ascii to be super-safe
filename[index..].replace(|c: char| !c.is_ascii() || c == ':', "-")
}
}
pub fn dc_derive_safe_stem_ext(filename: &str) -> (String, String) {
let basename = get_safe_basename(&filename);
let (mut stem, mut ext) = if let Some(index) = basename.rfind('.') {
(
basename[0..index].to_string(),
basename[index..].to_string(),
)
} else {
(basename, "".to_string())
};
// limit length of stem and ext
stem.truncate(32);
ext.truncate(32);
(stem, ext)
}
// the returned suffix is lower-case
#[allow(non_snake_case)]
pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
Path::new(path_filename.as_ref())
.extension()
@@ -343,14 +397,11 @@ pub(crate) fn dc_copy_file(
}
}
pub(crate) fn dc_create_folder(
context: &Context,
path: impl AsRef<std::path::Path>,
) -> Result<(), std::io::Error> {
pub(crate) fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() {
match fs::create_dir_all(path_abs) {
Ok(_) => Ok(()),
Ok(_) => true,
Err(err) => {
warn!(
context,
@@ -358,11 +409,11 @@ pub(crate) fn dc_create_folder(
path.as_ref().display(),
err
);
Err(err)
false
}
}
} else {
Ok(())
true
}
}
@@ -448,7 +499,7 @@ pub(crate) fn dc_get_next_backup_path(
pub(crate) fn time() -> i64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.unwrap()
.as_secs() as i64
}
@@ -460,7 +511,7 @@ pub(crate) fn time() -> i64 {
///
/// ```
/// use deltachat::dc_tools::EmailAddress;
/// let email = match "someone@example.com".parse::<EmailAddress>() {
/// let email = match EmailAddress::new("someone@example.com") {
/// Ok(addr) => addr,
/// Err(e) => panic!("Error parsing address, error was {}", e),
/// };
@@ -474,6 +525,12 @@ pub struct EmailAddress {
pub domain: String,
}
impl EmailAddress {
pub fn new(input: &str) -> Result<Self, Error> {
input.parse::<EmailAddress>()
}
}
impl fmt::Display for EmailAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}@{}", self.local, self.domain)
@@ -610,6 +667,7 @@ mod tests {
);
}
#[test]
#[test]
fn test_dc_extract_grpid_from_rfc724_mid() {
// Should return None if we pass invalid mid
@@ -664,29 +722,29 @@ mod tests {
#[test]
fn test_emailaddress_parse() {
assert_eq!("".parse::<EmailAddress>().is_ok(), false);
assert_eq!(EmailAddress::new("").is_ok(), false);
assert_eq!(
"user@domain.tld".parse::<EmailAddress>().unwrap(),
EmailAddress::new("user@domain.tld").unwrap(),
EmailAddress {
local: "user".into(),
domain: "domain.tld".into(),
}
);
assert_eq!("uuu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("dd.tt".parse::<EmailAddress>().is_ok(), false);
assert_eq!("tt.dd@uu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d.".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d.t".parse::<EmailAddress>().is_ok(), false);
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
assert_eq!(EmailAddress::new("tt.dd@uu").is_ok(), false);
assert_eq!(EmailAddress::new("u@d").is_ok(), false);
assert_eq!(EmailAddress::new("u@d.").is_ok(), false);
assert_eq!(EmailAddress::new("u@d.t").is_ok(), false);
assert_eq!(
"u@d.tt".parse::<EmailAddress>().unwrap(),
EmailAddress::new("u@d.tt").unwrap(),
EmailAddress {
local: "u".into(),
domain: "d.tt".into(),
}
);
assert_eq!("u@tt".parse::<EmailAddress>().is_ok(), false);
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
assert_eq!(EmailAddress::new("u@.tt").is_ok(), false);
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
}
use proptest::prelude::*;
@@ -722,6 +780,27 @@ mod tests {
}
}
#[test]
fn test_dc_create_incoming_rfc724_mid() {
let res = dc_create_incoming_rfc724_mid(123, 45, &[6, 7]);
assert_eq!(res, Some("123-45-7@stub".into()));
let res = dc_create_incoming_rfc724_mid(123, 45, &[]);
assert_eq!(res, Some("123-45-0@stub".into()));
}
#[test]
fn test_file_get_safe_basename() {
assert_eq!(get_safe_basename("12312/hello"), "hello");
assert_eq!(get_safe_basename("12312\\hello"), "hello");
assert_eq!(get_safe_basename("//12312\\hello"), "hello");
assert_eq!(get_safe_basename("//123:12\\hello"), "hello");
assert_eq!(get_safe_basename("//123:12/\\\\hello"), "hello");
assert_eq!(get_safe_basename("//123:12//hello"), "hello");
assert_eq!(get_safe_basename("//123:12//"), "nobasename");
assert_eq!(get_safe_basename("//123:12/"), "nobasename");
assert!(get_safe_basename("123\x012.hello").ends_with(".hello"));
}
#[test]
fn test_file_handling() {
let t = dummy_context();
@@ -770,7 +849,7 @@ mod tests {
assert!(dc_delete_file(context, "$BLOBDIR/foobar"));
assert!(dc_delete_file(context, "$BLOBDIR/dada"));
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder").is_ok());
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder"));
assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",));
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder"));

View File

@@ -139,12 +139,13 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
dehtml.add_text = AddText::YesPreserveLineEnds;
}
"a" => {
if let Some(href) = event
.html_attributes()
.filter_map(|attr| attr.ok())
.find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "href")
{
if let Some(href) = event.html_attributes().find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "href")
.unwrap_or_default()
}) {
let href = href
.unwrap()
.unescape_and_decode_value(reader)
.unwrap_or_default()
.to_lowercase();
@@ -188,41 +189,4 @@ mod tests {
assert_eq!(dehtml(input), output);
}
}
#[test]
fn test_dehtml_parse_br() {
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
let plain = dehtml(html);
assert_eq!(plain, "line1\n\r\r\rline2");
}
#[test]
fn test_dehtml_parse_href() {
let html = "<a href=url>text</a";
let plain = dehtml(html);
assert_eq!(plain, "[text](url)");
}
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = dehtml(html);
assert_eq!(plain, "text *bold*<>");
}
#[test]
fn test_dehtml_html_encoded() {
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = dehtml(html);
assert_eq!(
plain,
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
);
}
}

View File

@@ -2,7 +2,7 @@
use std::collections::HashSet;
use mailparse::{MailHeaderMap, ParsedMail};
use mailparse::MailHeaderMap;
use num_traits::FromPrimitive;
use crate::aheader::*;
@@ -14,6 +14,7 @@ use crate::keyring::*;
use crate::peerstate::*;
use crate::pgp;
use crate::securejoin::handle_degrade_event;
use crate::wrapmime;
#[derive(Debug)]
pub struct EncryptHelper {
@@ -117,7 +118,7 @@ impl EncryptHelper {
pub fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
mail: &mailparse::ParsedMail<'_>,
message_time: i64,
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
let from = mail
@@ -231,36 +232,9 @@ fn load_or_generate_self_public_key(context: &Context, self_addr: impl AsRef<str
}
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
ensure!(
mail.ctype.mimetype == "multipart/encrypted",
"Not a multipart/encrypted message: {}",
mail.ctype.mimetype
);
ensure!(
mail.subparts.len() == 2,
"Invalid Autocrypt Level 1 Mime Parts"
);
ensure!(
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
mail.subparts[0].ctype,
);
ensure!(
mail.subparts[1].ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
mail.subparts[1].ctype
);
Ok(&mail.subparts[1])
}
fn decrypt_if_autocrypt_message<'a>(
context: &Context,
mail: &ParsedMail<'a>,
mail: &mailparse::ParsedMail<'a>,
private_keyring: &Keyring,
public_keyring_for_validate: &Keyring,
ret_valid_signatures: &mut HashSet<String>,
@@ -272,7 +246,7 @@ fn decrypt_if_autocrypt_message<'a>(
//
// Errors are returned for failures related to decryption of AC-messages.
let encrypted_data_part = match get_autocrypt_mime(mail) {
let encrypted_data_part = match wrapmime::get_autocrypt_mime(mail) {
Err(_) => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
@@ -293,7 +267,7 @@ fn decrypt_if_autocrypt_message<'a>(
/// Returns Ok(None) if nothing encrypted was found.
fn decrypt_part(
_context: &Context,
mail: &ParsedMail<'_>,
mail: &mailparse::ParsedMail<'_>,
private_keyring: &Keyring,
public_keyring_for_validate: &Keyring,
ret_valid_signatures: &mut HashSet<String>,
@@ -339,7 +313,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
/// However, Delta Chat itself has no problem with encrypted multipart/report
/// parts and MUAs should be encouraged to encrpyt multipart/reports as well so
/// that we could use the normal Autocrypt processing.
fn contains_report(mail: &ParsedMail<'_>) -> bool {
fn contains_report(mail: &mailparse::ParsedMail<'_>) -> bool {
mail.ctype.mimetype == "multipart/report"
}

View File

@@ -6,54 +6,34 @@ use lettre_email::mime;
pub enum Error {
#[fail(display = "{:?}", _0)]
Failure(failure::Error),
#[fail(display = "SQL error: {:?}", _0)]
SqlError(#[cause] crate::sql::Error),
#[fail(display = "{:?}", _0)]
Io(std::io::Error),
#[fail(display = "{:?}", _0)]
Message(String),
#[fail(display = "{:?}", _0)]
Image(image_meta::ImageError),
#[fail(display = "{:?}", _0)]
Utf8(std::str::Utf8Error),
#[fail(display = "PGP: {:?}", _0)]
Pgp(pgp::errors::Error),
#[fail(display = "Base64Decode: {:?}", _0)]
Base64Decode(base64::DecodeError),
#[fail(display = "{:?}", _0)]
FromUtf8(std::string::FromUtf8Error),
#[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)]
#[fail(display = "Inalid Email: {:?}", _0)]
MailParseError(#[cause] mailparse::MailParseError),
#[fail(display = "Building invalid Email: {:?}", _0)]
LettreError(#[cause] lettre_email::error::Error),
#[fail(display = "SMTP error: {:?}", _0)]
SmtpError(#[cause] async_smtp::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>;

View File

@@ -4,7 +4,6 @@ use std::path::PathBuf;
use strum::EnumProperty;
use crate::chat::ChatId;
use crate::message::MsgId;
impl Event {
@@ -22,38 +21,56 @@ pub enum Event {
/// The library-user may write an informational string to the log.
/// Passed to the callback given to dc_context_new().
/// This event should not be reported to the end-user using a popup or something like that.
///
/// @return 0
#[strum(props(id = "100"))]
Info(String),
/// Emitted when SMTP connection is established and login was successful.
///
/// @return 0
#[strum(props(id = "101"))]
SmtpConnected(String),
/// Emitted when IMAP connection is established and login was successful.
///
/// @return 0
#[strum(props(id = "102"))]
ImapConnected(String),
/// Emitted when a message was successfully sent to the SMTP server.
///
/// @return 0
#[strum(props(id = "103"))]
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),
@@ -61,6 +78,8 @@ pub enum Event {
/// Passed to the callback given to dc_context_new().
///
/// This event should not be reported to the end-user using a popup or something like that.
///
/// @return 0
#[strum(props(id = "300"))]
Warning(String),
@@ -75,6 +94,8 @@ pub enum Event {
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then.
///
/// @return
#[strum(props(id = "400"))]
Error(String),
@@ -91,6 +112,8 @@ pub enum Event {
/// Moreover, if the UI detects that the device is offline,
/// it is probably more useful to report this to the user
/// instead of the string from data2.
///
/// @return 0
#[strum(props(id = "401"))]
ErrorNetwork(String),
@@ -99,6 +122,8 @@ pub enum Event {
/// dc_set_chat_name(), dc_set_chat_profile_image(),
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
/// dc_send_text_msg() or another sending function.
///
/// @return 0
#[strum(props(id = "410"))]
ErrorSelfNotInGroup(String),
@@ -107,41 +132,54 @@ pub enum Event {
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
/// @return 0
#[strum(props(id = "2000"))]
MsgsChanged { chat_id: ChatId, msg_id: MsgId },
MsgsChanged { chat_id: u32, msg_id: MsgId },
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
///
/// @return 0
#[strum(props(id = "2005"))]
IncomingMsg { chat_id: ChatId, msg_id: MsgId },
IncomingMsg { chat_id: u32, msg_id: MsgId },
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
///
/// @return 0
#[strum(props(id = "2010"))]
MsgDelivered { chat_id: ChatId, msg_id: MsgId },
MsgDelivered { chat_id: u32, msg_id: MsgId },
/// 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().
///
/// @return 0
#[strum(props(id = "2012"))]
MsgFailed { chat_id: ChatId, msg_id: MsgId },
MsgFailed { chat_id: u32, msg_id: MsgId },
/// 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().
///
/// @return 0
#[strum(props(id = "2015"))]
MsgRead { chat_id: ChatId, msg_id: MsgId },
MsgRead { chat_id: u32, msg_id: MsgId },
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
/// @return 0
#[strum(props(id = "2020"))]
ChatModified(ChatId),
ChatModified(u32),
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
/// @return 0
#[strum(props(id = "2030"))]
ContactsChanged(Option<u32>),
@@ -150,12 +188,14 @@ pub enum Event {
/// @param data1 (u32) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
/// @return 0
#[strum(props(id = "2035"))]
LocationChanged(Option<u32>),
/// Inform about the configuration progress started by configure().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @return 0
#[strum(props(id = "2041"))]
ConfigureProgress(usize),
@@ -163,6 +203,7 @@ pub enum Event {
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
/// @return 0
#[strum(props(id = "2051"))]
ImexProgress(usize),
@@ -173,6 +214,7 @@ pub enum Event {
/// services.
///
/// @param data2 0
/// @return 0
#[strum(props(id = "2052"))]
ImexFileWritten(PathBuf),
@@ -188,6 +230,7 @@ pub enum Event {
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
/// @return 0
#[strum(props(id = "2060"))]
SecurejoinInviterProgress { contact_id: u32, progress: usize },
@@ -199,12 +242,14 @@ pub enum Event {
/// @param data2 (int) Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// @return 0
#[strum(props(id = "2061"))]
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
/// This event is sent out to the inviter when a joiner successfully joined a group.
/// @param data1 (int) chat_id
/// @param data2 (int) contact_id
/// @return 0
#[strum(props(id = "2062"))]
SecurejoinMemberAdded { chat_id: ChatId, contact_id: u32 },
SecurejoinMemberAdded { chat_id: u32, contact_id: u32 },
}

View File

@@ -1,62 +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,
/// Delta Chat extension for message IDs in combined MDNs
AdditionalMessageIds,
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");
}
}

View File

@@ -18,9 +18,6 @@ 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,
@@ -80,34 +77,21 @@ impl Imap {
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
Ok(IdleResponse::NewData(_)) => {
IdleResponse::NewData(_) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
Ok(IdleResponse::Timeout) => {
IdleResponse::Timeout => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(IdleResponse::ManualInterrupt) => {
IdleResponse::ManualInterrupt => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
match handle.done().await {
Ok(session) => {
*self.session.lock().await = Some(Session::Secure(session));
}
@@ -137,34 +121,21 @@ impl Imap {
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
Ok(IdleResponse::NewData(_)) => {
IdleResponse::NewData(_) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
Ok(IdleResponse::Timeout) => {
IdleResponse::Timeout => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(IdleResponse::ManualInterrupt) => {
IdleResponse::ManualInterrupt => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
match handle.done().await {
Ok(session) => {
*self.session.lock().await = Some(Session::Insecure(session));
}
@@ -247,7 +218,7 @@ impl Imap {
"IMAP-fake-IDLE done after {:.4}s",
SystemTime::now()
.duration_since(fake_idle_start_time)
.unwrap_or_default()
.unwrap()
.as_millis() as f64
/ 1000.,
);

View File

@@ -7,7 +7,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use async_imap::{
error::Result as ImapResult,
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
types::{Fetch, Flag, Mailbox, Name, NameAttribute},
};
use async_std::sync::{Mutex, RwLock};
use async_std::task;
@@ -23,6 +23,7 @@ use crate::message::{self, update_server_uid};
use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::stock::StockMessage;
use crate::wrapmime;
mod idle;
pub mod select_folder;
@@ -63,9 +64,6 @@ pub enum Error {
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[fail(display = "No mailbox selected, folder: {:?}", _0)]
NoMailbox(String),
#[fail(display = "IMAP other error: {:?}", _0)]
Other(String),
}
@@ -331,7 +329,9 @@ impl Imap {
/// Connects to imap account using already-configured parameters.
pub fn connect_configured(&self, context: &Context) -> Result<()> {
if async_std::task::block_on(self.is_connected()) && !self.should_reconnect() {
if async_std::task::block_on(async move {
self.is_connected().await && !self.should_reconnect()
}) {
return Ok(());
}
if !context.sql.get_raw_config_bool(context, "configured") {
@@ -341,7 +341,7 @@ impl Imap {
let param = LoginParam::from_database(context, "configured_");
// the trailing underscore is correct
if task::block_on(self.connect(context, &param)) {
if self.connect(context, &param) {
self.ensure_configured_folders(context, true)
} else {
Err(Error::ConnectionFailed(format!("{}", param)))
@@ -350,80 +350,77 @@ impl Imap {
/// tries connecting to imap account using the specific login
/// parameters
pub async fn connect(&self, context: &Context, lp: &LoginParam) -> bool {
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
return false;
}
pub fn connect(&self, context: &Context, lp: &LoginParam) -> bool {
task::block_on(async move {
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
return false;
}
{
let addr = &lp.addr;
let imap_server = &lp.mail_server;
let imap_port = lp.mail_port as u16;
let imap_user = &lp.mail_user;
let imap_pw = &lp.mail_pw;
let server_flags = lp.server_flags as usize;
{
let addr = &lp.addr;
let imap_server = &lp.mail_server;
let imap_port = lp.mail_port as u16;
let imap_user = &lp.mail_user;
let imap_pw = &lp.mail_pw;
let server_flags = lp.server_flags as usize;
let mut config = self.config.write().await;
config.addr = addr.to_string();
config.imap_server = imap_server.to_string();
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
config.certificate_checks = lp.imap_certificate_checks;
config.server_flags = server_flags;
}
let mut config = self.config.write().await;
config.addr = addr.to_string();
config.imap_server = imap_server.to_string();
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
config.certificate_checks = lp.imap_certificate_checks;
config.server_flags = server_flags;
}
if let Err(err) = self.setup_handle_if_needed(context).await {
warn!(context, "failed to setup imap handle: {}", err);
self.free_connect_params().await;
return false;
}
if let Err(err) = self.setup_handle_if_needed(context).await {
warn!(context, "failed to setup imap handle: {}", err);
self.free_connect_params().await;
return false;
}
let teardown = match &mut *self.session.lock().await {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open() {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,);
true
} else {
let can_idle = caps.has_str("IDLE");
let has_xlist = caps.has_str("XLIST");
let caps_list = caps.iter().fold(String::new(), |s, c| {
if let Capability::Atom(x) = c {
s + &format!(" {}", x)
} else {
s + &format!(" {:?}", c)
}
});
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
Event::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.mail_user, caps_list,
))
);
false
let teardown = match &mut *self.session.lock().await {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open() {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,);
true
} else {
let can_idle = caps.has_str("IDLE");
let has_xlist = caps.has_str("XLIST");
let caps_list = caps
.iter()
.fold(String::new(), |s, c| s + &format!(" {:?}", c));
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
Event::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.mail_user, caps_list,
))
);
false
}
}
}
Err(err) => {
info!(context, "CAPABILITY command error: {}", err);
true
}
},
None => true,
};
Err(err) => {
info!(context, "CAPABILITY command error: {}", err);
true
}
},
None => true,
};
if teardown {
self.disconnect(context);
if teardown {
self.disconnect(context);
false
} else {
true
}
false
} else {
true
}
})
}
pub fn disconnect(&self, context: &Context) {
@@ -481,10 +478,7 @@ impl Imap {
let (uid_validity, last_seen_uid) = self.get_config_last_seen_uid(context, &folder);
let config = self.config.read().await;
let mailbox = config
.selected_mailbox
.as_ref()
.ok_or_else(|| Error::NoMailbox(folder.to_string()))?;
let mailbox = config.selected_mailbox.as_ref().expect("just selected");
let new_uid_validity = match mailbox.uid_validity {
Some(v) => v,
@@ -719,17 +713,7 @@ impl Imap {
if !is_deleted && msg.body().is_some() {
let body = msg.body().unwrap_or_default();
if let Err(err) =
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32)
{
warn!(
context,
"dc_receive_imf failed for imap-message {}/{}: {:?}",
folder.as_ref(),
server_uid,
err
);
}
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32);
}
}
@@ -957,8 +941,8 @@ impl Imap {
remote_message_id,
message_id,
);
*uid = 0;
}
*uid = 0;
}
Err(err) => {
warn!(
@@ -1228,18 +1212,6 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
}
}
fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> {
let value = std::str::from_utf8(message_id)?;
let addrs = mailparse::addrparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
if let Some(info) = addrs.extract_single_info() {
return Ok(info.addr);
}
bail!("could not parse message_id: {}", value);
}
fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
if prefetch_msg.envelope().is_none() {
return Err(Error::Other(
@@ -1252,22 +1224,5 @@ fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
return Err(Error::Other("prefetch: No message ID found".to_string()));
}
parse_message_id(&message_id.unwrap()).map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_message_id() {
assert_eq!(
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
assert_eq!(
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
}
wrapmime::parse_message_id(&message_id.unwrap()).map_err(Into::into)
}

View File

@@ -4,11 +4,12 @@ use async_imap::{
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 async_std::sync::Arc;
use async_tls::client::TlsStream;
use crate::login_param::{dc_build_tls, CertificateChecks};
use crate::login_param::{dc_build_tls_config, CertificateChecks};
#[derive(Debug)]
pub(crate) enum Client {
@@ -35,8 +36,9 @@ impl Client {
certificate_checks: CertificateChecks,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks);
let tls_stream = tls.connect(domain.as_ref(), stream).await?;
let tls_config = dc_build_tls_config(certificate_checks);
let tls_connector: async_tls::TlsConnector = Arc::new(tls_config).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;
@@ -45,7 +47,7 @@ impl Client {
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
.expect("failed to read greeting");
Ok(Client::Secure(client))
}
@@ -60,7 +62,7 @@ impl Client {
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
.expect("failed to read greeting");
Ok(Client::Insecure(client))
}
@@ -72,8 +74,10 @@ impl Client {
) -> ImapResult<Client> {
match self {
Client::Insecure(client) => {
let tls = dc_build_tls(certificate_checks);
let client_sec = client.secure(domain, tls).await?;
let tls_config = dc_build_tls_config(certificate_checks);
let tls: async_tls::TlsConnector = Arc::new(tls_config).into();
let client_sec = client.secure(domain, &tls).await?;
Ok(Client::Secure(client_sec))
}

View File

@@ -8,8 +8,8 @@ use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
use crate::chat;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::config::Config;
use crate::configure::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
@@ -33,19 +33,16 @@ pub enum ImexMode {
/// 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.
@@ -136,16 +133,14 @@ fn do_initiate_key_transfer(context: &Context) -> Result<String> {
let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF)?;
msg = Message::default();
msg.viewtype = Viewtype::File;
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,
ForcePlaintext::NoAutocryptHeader as i32,
);
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)?;
@@ -385,7 +380,7 @@ pub fn JobImexImap(context: &Context, job: &Job) -> Result<()> {
context.free_ongoing();
bail!("Cannot create private key or private key not available.");
} else {
dc_create_folder(context, &param)?;
dc_create_folder(context, &param);
}
}
let path = Path::new(param);
@@ -422,7 +417,7 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
);
ensure!(
!context.is_configured(),
!dc_is_configured(context),
"Cannot import backups to accounts in use."
);
context.sql.close(&context);
@@ -443,8 +438,6 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
"could not re-open db"
);
delete_and_reset_all_device_msgs(&context)?;
let total_files_cnt = context
.sql
.query_get_value::<_, isize>(context, "SELECT COUNT(*) FROM backup_blobs;", params![])

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,18 @@
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
#![allow(clippy::match_bool)]
// for now we hide warnings to not clutter/hide errors during "cargo clippy"
#![allow(
clippy::type_complexity,
clippy::cognitive_complexity,
clippy::too_many_arguments,
clippy::block_in_if_condition_stmt,
clippy::large_enum_variant
)]
#![allow(
clippy::unreadable_literal,
clippy::needless_range_loop,
clippy::match_bool
)]
#![feature(ptr_wrapping_offset_from)]
#[macro_use]
extern crate failure_derive;
@@ -16,12 +29,10 @@ extern crate strum_macros;
extern crate debug_stub_derive;
#[macro_use]
pub mod log;
mod log;
#[macro_use]
pub mod error;
pub mod headerdef;
pub(crate) mod events;
pub use events::*;
@@ -38,7 +49,6 @@ mod e2ee;
mod imap;
mod imap_client;
pub mod imex;
#[macro_use]
pub mod job;
mod job_thread;
pub mod key;
@@ -55,15 +65,17 @@ pub mod peerstate;
pub mod pgp;
pub mod qr;
pub mod securejoin;
mod simplify;
mod smtp;
pub mod sql;
pub mod stock;
mod token;
#[macro_use]
mod wrapmime;
mod dehtml;
pub mod dc_array;
pub mod dc_receive_imf;
mod dc_simplify;
pub mod dc_tools;
/// if set imap/incoming and smtp/outgoing MIME messages will be printed

View File

@@ -4,14 +4,14 @@ use bitflags::bitflags;
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::chat::{self, ChatId};
use crate::chat;
use crate::config::Config;
use crate::constants::*;
use crate::context::*;
use crate::dc_tools::*;
use crate::error::Error;
use crate::events::Event;
use crate::job::{self, job_action_exists, job_add, Job};
use crate::job::*;
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
@@ -28,7 +28,7 @@ pub struct Location {
pub timestamp: i64,
pub contact_id: u32,
pub msg_id: u32,
pub chat_id: ChatId,
pub chat_id: u32,
pub marker: Option<String>,
pub independent: u32,
}
@@ -193,9 +193,9 @@ impl Kml {
}
// location streaming
pub fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) {
pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
let now = time();
if !(seconds < 0 || chat_id.is_special()) {
if !(seconds < 0 || chat_id <= DC_CHAT_ID_LAST_SPECIAL) {
let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id);
if sql::execute(
context,
@@ -207,7 +207,7 @@ pub fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64)
params![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id,
chat_id as i32,
],
)
.is_ok()
@@ -228,8 +228,8 @@ pub fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64)
schedule_MAYBE_SEND_LOCATIONS(context, false);
job_add(
context,
job::Action::MaybeSendLocationsEnded,
chat_id.to_u32() as i32,
Action::MaybeSendLocationsEnded,
chat_id as i32,
Params::new(),
seconds + 1,
);
@@ -240,23 +240,17 @@ pub fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64)
#[allow(non_snake_case)]
fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, force_schedule: bool) {
if force_schedule || !job_action_exists(context, job::Action::MaybeSendLocations) {
job_add(
context,
job::Action::MaybeSendLocations,
0,
Params::new(),
60,
);
if force_schedule || !job_action_exists(context, Action::MaybeSendLocations) {
job_add(context, Action::MaybeSendLocations, 0, Params::new(), 60);
};
}
pub fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> bool {
pub fn is_sending_locations_to_chat(context: &Context, chat_id: u32) -> bool {
context
.sql
.exists(
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
params![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
params![if chat_id == 0 { 1 } else { 0 }, chat_id as i32, time()],
)
.unwrap_or_default()
}
@@ -302,7 +296,7 @@ pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> b
pub fn get_range(
context: &Context,
chat_id: ChatId,
chat_id: u32,
contact_id: u32,
timestamp_from: i64,
mut timestamp_to: i64,
@@ -320,8 +314,8 @@ pub fn get_range(
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
params![
if chat_id.is_unset() { 1 } else { 0 },
chat_id,
if chat_id == 0 { 1 } else { 0 },
chat_id as i32,
if contact_id == 0 { 1 } else { 0 },
contact_id as i32,
timestamp_from,
@@ -371,7 +365,7 @@ pub fn delete_all(context: &Context) -> Result<(), Error> {
Ok(())
}
pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> {
pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error> {
let mut last_added_location_id = 0;
let self_addr = context
@@ -380,7 +374,7 @@ pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Erro
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
params![chat_id], |row| {
params![chat_id as i32], |row| {
let send_begin: i64 = row.get(0)?;
let send_until: i64 = row.get(1)?;
let last_sent: i64 = row.get(2)?;
@@ -465,14 +459,14 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String
pub fn set_kml_sent_timestamp(
context: &Context,
chat_id: ChatId,
chat_id: u32,
timestamp: i64,
) -> Result<(), Error> {
sql::execute(
context,
&context.sql,
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
params![timestamp, chat_id],
params![timestamp, chat_id as i32],
)?;
Ok(())
@@ -495,12 +489,12 @@ pub fn set_msg_location_id(
pub fn save(
context: &Context,
chat_id: ChatId,
chat_id: u32,
contact_id: u32,
locations: &[Location],
independent: bool,
) -> Result<u32, Error> {
ensure!(!chat_id.is_special(), "Invalid chat id");
ensure!(chat_id > DC_CHAT_ID_LAST_SPECIAL, "Invalid chat id");
context
.sql
.prepare2(
@@ -520,7 +514,7 @@ pub fn save(
stmt_insert.execute(params![
location.timestamp,
contact_id as i32,
chat_id,
chat_id as i32,
location.latitude,
location.longitude,
location.accuracy,
@@ -548,7 +542,7 @@ pub fn save(
}
#[allow(non_snake_case)]
pub fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
pub fn JobMaybeSendLocations(context: &Context, _job: &Job) {
let now = time();
let mut continue_streaming = false;
info!(
@@ -562,7 +556,7 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
WHERE locations_send_until>?;",
params![now],
|row| {
let chat_id: ChatId = row.get(0)?;
let chat_id: i32 = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?;
continue_streaming = true;
@@ -629,46 +623,44 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
for (chat_id, mut msg) in msgs.into_iter() {
// TODO: better error handling
chat::send_msg(context, chat_id, &mut msg).unwrap_or_default();
chat::send_msg(context, chat_id as u32, &mut msg).unwrap_or_default();
}
}
if continue_streaming {
schedule_MAYBE_SEND_LOCATIONS(context, true);
}
job::Status::Finished(Ok(()))
}
#[allow(non_snake_case)]
pub fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status {
pub fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) {
// this function is called when location-streaming _might_ have ended for a chat.
// the function checks, if location-streaming is really ended;
// if so, a device-message is added if not yet done.
let chat_id = ChatId::new(job.foreign_id);
let chat_id = job.foreign_id;
let (send_begin, send_until) = job_try!(context.sql.query_row(
if let Ok((send_begin, send_until)) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
params![chat_id],
params![chat_id as i32],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
));
if !(send_begin != 0 && time() <= send_until) {
// still streaming -
// may happen as several calls to dc_send_locations_to_chat()
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
if !(send_begin == 0 && send_until == 0) {
// not streaming, device-message already sent
job_try!(context.sql.execute(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
params![chat_id],
));
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_info_msg(context, chat_id, stock_str);
context.call_cb(Event::ChatModified(chat_id));
) {
if !(send_begin != 0 && time() <= send_until) {
// still streaming -
// may happen as several calls to dc_send_locations_to_chat()
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
if !(send_begin == 0 && send_until == 0) {
// not streaming, device-message already sent
if context.sql.execute(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
params![chat_id as i32],
).is_ok() {
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_info_msg(context, chat_id, stock_str);
context.call_cb(Event::ChatModified(chat_id));
}
}
}
}
job::Status::Finished(Ok(()))
}
#[cfg(test)]

View File

@@ -7,13 +7,7 @@ macro_rules! info {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
let thread = ::std::thread::current();
let full = format!("{thid:?} {file}:{line}: {msg}",
thid = thread.id(),
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::Event::Info(full));
emit_event!($ctx, $crate::Event::Info(formatted));
}};
}
@@ -24,13 +18,7 @@ macro_rules! warn {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
let thread = ::std::thread::current();
let full = format!("{thid:?} {file}:{line}: {msg}",
thid = thread.id(),
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::Event::Warning(full));
emit_event!($ctx, $crate::Event::Warning(formatted));
}};
}

View File

@@ -4,6 +4,11 @@ use std::borrow::Cow;
use std::fmt;
use crate::context::Context;
use crate::error::Error;
use async_std::sync::Arc;
use rustls;
use webpki;
use webpki_roots;
#[derive(Copy, Clone, Debug, Display, FromPrimitive)]
#[repr(i32)]
@@ -11,11 +16,7 @@ use crate::context::Context;
pub enum CertificateChecks {
Automatic = 0,
Strict = 1,
/// Same as AcceptInvalidCertificates
/// Previously known as AcceptInvalidHostnames, now deprecated.
AcceptInvalidCertificates2 = 2,
AcceptInvalidHostnames = 2,
AcceptInvalidCertificates = 3,
}
@@ -129,7 +130,7 @@ impl LoginParam {
&self,
context: &Context,
prefix: impl AsRef<str>,
) -> crate::sql::Result<()> {
) -> Result<(), Error> {
let prefix = prefix.as_ref();
let sql = &context.sql;
@@ -258,22 +259,49 @@ fn get_readable_flags(flags: i32) -> String {
res
}
pub fn dc_build_tls(certificate_checks: CertificateChecks) -> async_native_tls::TlsConnector {
let tls_builder = async_native_tls::TlsConnector::new();
pub struct NoCertificateVerification {}
impl rustls::ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
&self,
_roots: &rustls::RootCertStore,
_presented_certs: &[rustls::Certificate],
_dns_name: webpki::DNSNameRef<'_>,
_ocsp: &[u8],
) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
Ok(rustls::ServerCertVerified::assertion())
}
}
pub fn dc_build_tls_config(certificate_checks: CertificateChecks) -> rustls::ClientConfig {
let mut config = rustls::ClientConfig::new();
config
.root_store
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
match certificate_checks {
CertificateChecks::Strict => {}
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)
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
}
CertificateChecks::AcceptInvalidCertificates => {
// TODO: only accept invalid certs
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
}
CertificateChecks::AcceptInvalidHostnames => {
// TODO: only accept invalid hostnames
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
}
CertificateChecks::Strict => tls_builder,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true),
}
config
}
#[cfg(test)]
@@ -285,8 +313,8 @@ mod tests {
use std::string::ToString;
assert_eq!(
"accept_invalid_certificates".to_string(),
CertificateChecks::AcceptInvalidCertificates.to_string()
"accept_invalid_hostnames".to_string(),
CertificateChecks::AcceptInvalidHostnames.to_string()
);
}
}

View File

@@ -73,28 +73,20 @@ pub enum LotState {
// Qr States
/// id=contact
QrAskVerifyContact = 200,
/// text1=groupname
QrAskVerifyGroup = 202,
/// id=contact
QrFprOk = 210,
/// id=contact
QrFprMissmatch = 220,
/// test1=formatted fingerprint
QrFprWithoutAddr = 230,
/// id=contact
QrAddr = 320,
/// text1=text
QrText = 330,
/// text1=URL
QrUrl = 332,
/// text1=error string
QrError = 400,

View File

@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use failure::Fail;
use crate::chat::{self, Chat, ChatId};
use crate::chat::{self, Chat};
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
@@ -47,7 +47,10 @@ impl MsgId {
///
/// This kind of message ID can not be used for real messages.
pub fn is_special(self) -> bool {
self.0 <= DC_MSG_ID_LAST_SPECIAL
match self.0 {
0..=DC_MSG_ID_LAST_SPECIAL => true,
_ => false,
}
}
/// Whether the message ID is unset.
@@ -145,22 +148,6 @@ impl rusqlite::types::FromSql for MsgId {
#[fail(display = "Invalid Message ID.")]
pub struct InvalidMsgId;
#[derive(Debug, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum MessengerMessage {
No = 0,
Yes = 1,
/// No, but reply to messenger message.
Reply = 2,
}
impl Default for MessengerMessage {
fn default() -> Self {
Self::No
}
}
/// An object representing a single message in memory.
/// The message object is not updated.
/// If you want an update, you have to recreate the object.
@@ -173,8 +160,8 @@ pub struct Message {
pub(crate) id: MsgId,
pub(crate) from_id: u32,
pub(crate) to_id: u32,
pub(crate) chat_id: ChatId,
pub(crate) viewtype: Viewtype,
pub(crate) chat_id: u32,
pub(crate) type_0: Viewtype,
pub(crate) state: MessageState,
pub(crate) hidden: bool,
pub(crate) timestamp_sort: i64,
@@ -185,7 +172,8 @@ pub struct Message {
pub(crate) in_reply_to: Option<String>,
pub(crate) server_folder: Option<String>,
pub(crate) server_uid: u32,
pub(crate) is_dc_message: MessengerMessage,
// TODO: enum
pub(crate) is_dc_message: u32,
pub(crate) starred: bool,
pub(crate) chat_blocked: Blocked,
pub(crate) location_id: u32,
@@ -195,7 +183,7 @@ pub struct Message {
impl Message {
pub fn new(viewtype: Viewtype) -> Self {
let mut msg = Message::default();
msg.viewtype = viewtype;
msg.type_0 = viewtype;
msg
}
@@ -203,8 +191,7 @@ impl Message {
pub fn load_from_db(context: &Context, id: MsgId) -> Result<Message, Error> {
ensure!(
!id.is_special(),
"Can not load special message IDs from DB. ({:?})",
id
"Can not load special message IDs from DB."
);
context
.sql
@@ -249,7 +236,7 @@ impl Message {
msg.timestamp_sort = row.get("timestamp")?;
msg.timestamp_sent = row.get("timestamp_sent")?;
msg.timestamp_rcvd = row.get("timestamp_rcvd")?;
msg.viewtype = row.get("type")?;
msg.type_0 = row.get("type")?;
msg.state = row.get("state")?;
msg.is_dc_message = row.get("msgrmsg")?;
@@ -325,10 +312,10 @@ impl Message {
}
pub fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<(), Error> {
if chat::msgtype_has_file(self.viewtype) {
if chat::msgtype_has_file(self.type_0) {
let file_param = self.param.get_path(Param::File, context)?;
if let Some(path_and_filename) = file_param {
if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
if (self.type_0 == Viewtype::Image || self.type_0 == Viewtype::Gif)
&& !self.param.exists(Param::Width)
{
self.param.set_int(Param::Width, 0);
@@ -398,16 +385,16 @@ impl Message {
self.from_id
}
pub fn get_chat_id(&self) -> ChatId {
pub fn get_chat_id(&self) -> u32 {
if self.chat_blocked != Blocked::Not {
ChatId::new(DC_CHAT_ID_DEADDROP)
1
} else {
self.chat_id
}
}
pub fn get_viewtype(&self) -> Viewtype {
self.viewtype
self.type_0
}
pub fn get_state(&self) -> MessageState {
@@ -487,7 +474,7 @@ impl Message {
pub fn get_summarytext(&self, context: &Context, approx_characters: usize) -> String {
get_summarytext_by_raw(
self.viewtype,
self.type_0,
self.text.as_ref(),
&self.param,
approx_characters,
@@ -531,11 +518,11 @@ impl Message {
/// copied to the blobdir. Thus those attachments need to be
/// created immediately in the blobdir with a valid filename.
pub fn is_increation(&self) -> bool {
chat::msgtype_has_file(self.viewtype) && self.state == MessageState::OutPreparing
chat::msgtype_has_file(self.type_0) && self.state == MessageState::OutPreparing
}
pub fn is_setupmessage(&self) -> bool {
if self.viewtype != Viewtype::File {
if self.type_0 != Viewtype::File {
return false;
}
@@ -608,48 +595,18 @@ impl Message {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[repr(i32)]
pub enum MessageState {
Undefined = 0,
/// Incoming *fresh* message. Fresh messages are neither noticed
/// nor seen and are typically shown in notifications.
InFresh = 10,
/// Incoming *noticed* message. E.g. chat opened but message not
/// yet read - noticed messages are not counted as unread but did
/// not marked as read nor resulted in MDNs.
InNoticed = 13,
/// Incoming message, really *seen* by the user. Marked as read on
/// IMAP and MDN may be sent.
InSeen = 16,
/// For files which need time to be prepared before they can be
/// sent, the message enters this state before
/// OutPending.
OutPreparing = 18,
/// Message saved as draft.
OutDraft = 19,
/// The user has pressed the "send" button but the message is not
/// yet sent and is pending in some way. Maybe we're offline (no
/// checkmark).
OutPending = 20,
/// *Unrecoverable* error (*recoverable* errors result in pending
/// messages).
OutFailed = 24,
/// Outgoing message successfully delivered to server (one
/// checkmark). Note, that already delivered messages may get into
/// the OutFailed state if we get such a hint from the server.
OutDelivered = 26,
/// Outgoing message read by the recipient (two checkmarks; this
/// requires goodwill on the receiver's side)
OutMdnRcvd = 28,
}
@@ -659,27 +616,6 @@ impl Default for MessageState {
}
}
impl std::fmt::Display for MessageState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Undefined => "Undefined",
Self::InFresh => "Fresh",
Self::InNoticed => "Noticed",
Self::InSeen => "Seen",
Self::OutPreparing => "Preparing",
Self::OutDraft => "Draft",
Self::OutPending => "Pending",
Self::OutFailed => "Failed",
Self::OutDelivered => "Delivered",
Self::OutMdnRcvd => "Read",
}
)
}
}
impl From<MessageState> for LotState {
fn from(s: MessageState) -> Self {
use MessageState::*;
@@ -735,7 +671,7 @@ impl Lot {
self.text1 = None;
self.text1_meaning = Meaning::None;
} else {
if chat.id.is_deaddrop() {
if chat.id == DC_CHAT_ID_DEADDROP {
if let Some(contact) = contact {
self.text1 = Some(contact.get_display_name().into());
} else {
@@ -751,7 +687,7 @@ impl Lot {
}
self.text2 = Some(get_summarytext_by_raw(
msg.viewtype,
msg.type_0,
msg.text.as_ref(),
&msg.param,
SUMMARY_CHARACTERS,
@@ -784,7 +720,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
return ret;
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = dc_truncate(rawtxt.trim(), 100_000, false);
let rawtxt = dc_truncate(rawtxt.trim(), 100000, false);
let fts = dc_timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {}", fts);
@@ -834,7 +770,19 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
}
}
ret += &format!("State: {}", msg.state);
ret += "State: ";
use MessageState::*;
match msg.state {
InFresh => ret += "Fresh",
InNoticed => ret += "Noticed",
InSeen => ret += "Seen",
OutDelivered => ret += "Delivered",
OutFailed => ret += "Failed",
OutMdnRcvd => ret += "Read",
OutPending => ret += "Pending",
OutPreparing => ret += "Preparing",
_ => ret += &format!("{}", msg.state),
}
if msg.has_location() {
ret += ", Location sent";
@@ -860,9 +808,9 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes);
}
if msg.viewtype != Viewtype::Text {
if msg.type_0 != Viewtype::Text {
ret += "Type: ";
ret += &format!("{}", msg.viewtype);
ret += &format!("{}", msg.type_0);
ret += "\n";
ret += &format!("Mimetype: {}\n", &msg.get_filemime().unwrap_or_default());
}
@@ -896,7 +844,6 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
"mp3" => (Viewtype::Audio, "audio/mpeg"),
"aac" => (Viewtype::Audio, "audio/aac"),
"mp4" => (Viewtype::Video, "video/mp4"),
"webm" => (Viewtype::Video, "video/webm"),
"jpg" => (Viewtype::Image, "image/jpeg"),
"jpeg" => (Viewtype::Image, "image/jpeg"),
"jpe" => (Viewtype::Image, "image/jpeg"),
@@ -927,7 +874,7 @@ pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
delete_poi_location(context, msg.location_id);
}
}
update_msg_chat_id(context, *msg_id, ChatId::new(DC_CHAT_ID_TRASH));
update_msg_chat_id(context, *msg_id, DC_CHAT_ID_TRASH);
job_add(
context,
Action::DeleteMsgOnImap,
@@ -939,7 +886,7 @@ pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
if !msg_ids.is_empty() {
context.call_cb(Event::MsgsChanged {
chat_id: ChatId::new(0),
chat_id: 0,
msg_id: MsgId::new(0),
});
job_kill_action(context, Action::Housekeeping);
@@ -947,12 +894,12 @@ pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
};
}
fn update_msg_chat_id(context: &Context, msg_id: MsgId, chat_id: ChatId) -> bool {
fn update_msg_chat_id(context: &Context, msg_id: MsgId, chat_id: u32) -> bool {
sql::execute(
context,
&context.sql,
"UPDATE msgs SET chat_id=? WHERE id=?;",
params![chat_id, msg_id],
params![chat_id as i32, msg_id],
)
.is_ok()
}
@@ -1031,7 +978,7 @@ pub fn markseen_msgs(context: &Context, msg_ids: &[MsgId]) -> bool {
if send_event {
context.call_cb(Event::MsgsChanged {
chat_id: ChatId::new(0),
chat_id: 0,
msg_id: MsgId::new(0),
});
}
@@ -1142,14 +1089,14 @@ pub fn exists(context: &Context, msg_id: MsgId) -> bool {
return false;
}
let chat_id: Option<ChatId> = context.sql.query_get_value(
let chat_id: Option<u32> = context.sql.query_get_value(
context,
"SELECT chat_id FROM msgs WHERE id=?;",
params![msg_id],
);
if let Some(chat_id) = chat_id {
!chat_id.is_trash()
chat_id != DC_CHAT_ID_TRASH
} else {
false
}
@@ -1187,7 +1134,7 @@ pub fn mdn_from_ext(
from_id: u32,
rfc724_mid: &str,
timestamp_sent: i64,
) -> Option<(ChatId, MsgId)> {
) -> Option<(u32, MsgId)> {
if from_id <= DC_MSG_ID_LAST_SPECIAL || rfc724_mid.is_empty() {
return None;
}
@@ -1207,7 +1154,7 @@ pub fn mdn_from_ext(
|row| {
Ok((
row.get::<_, MsgId>("msg_id")?,
row.get::<_, ChatId>("chat_id")?,
row.get::<_, u32>("chat_id")?,
row.get::<_, Chattype>("type")?,
row.get::<_, MessageState>("state")?,
))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -154,7 +154,7 @@ pub fn dc_get_oauth2_access_token(
}
// ... and POST
let response = reqwest::blocking::Client::new()
let response = reqwest::Client::new()
.post(post_url)
.form(&post_param)
.send();
@@ -165,7 +165,7 @@ pub fn dc_get_oauth2_access_token(
);
return None;
}
let response = response.unwrap();
let mut response = response.unwrap();
if !response.status().is_success() {
warn!(
context,
@@ -271,8 +271,7 @@ impl Oauth2 {
{
match domain {
"gmail.com" | "googlemail.com" => Some(OAUTH2_GMAIL),
"yandex.com" | "yandex.by" | "yandex.kz" | "yandex.ru" | "yandex.ua" | "ya.ru"
| "narod.ru" => Some(OAUTH2_YANDEX),
"yandex.com" | "yandex.ru" | "yandex.ua" => Some(OAUTH2_YANDEX),
_ => None,
}
} else {
@@ -291,12 +290,12 @@ impl Oauth2 {
// "verified_email": true,
// "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg"
// }
let response = reqwest::blocking::Client::new().get(&userinfo_url).send();
let response = reqwest::Client::new().get(&userinfo_url).send();
if response.is_err() {
warn!(context, "Error getting userinfo: {:?}", response);
return None;
}
let response = response.unwrap();
let mut response = response.unwrap();
if !response.status().is_success() {
warn!(context, "Error getting userinfo: {:?}", response.status());
return None;
@@ -311,19 +310,14 @@ impl Oauth2 {
return None;
}
if let Ok(response) = parsed {
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
// but serde_json::Value.to_string() does not!
if let Some(addr) = response.get("email") {
if let Some(s) = addr.as_str() {
Some(s.to_string())
} else {
warn!(context, "E-mail in userinfo is not a string: {}", addr);
None
}
} else {
// serde_json::Value.as_str() removes the quotes of json-strings
let addr = response.get("email");
if addr.is_none() {
warn!(context, "E-mail missing in userinfo.");
None
return None;
}
let addr = addr.unwrap().as_str();
addr.map(|addr| addr.to_string())
} else {
warn!(context, "Failed to parse userinfo.");
None

View File

@@ -8,7 +8,6 @@ use num_traits::FromPrimitive;
use crate::blob::{BlobError, BlobObject};
use crate::context::Context;
use crate::error;
use crate::message::MsgId;
use crate::mimeparser::SystemMessage;
/// Available param keys.
@@ -17,57 +16,38 @@ use crate::mimeparser::SystemMessage;
pub enum Param {
/// For messages and jobs
File = b'f',
/// For Messages
Width = b'w',
/// For Messages
Height = b'h',
/// For Messages
Duration = b'd',
/// For Messages
MimeType = b'm',
/// For Messages: message is encrypted, outgoing: guarantee E2EE or the message is not send
/// For Messages: message is encryoted, outgoing: guarantee E2EE or the message is not send
GuaranteeE2ee = b'c',
/// For Messages: decrypted with validation errors or without mutual set, if neither
/// 'c' nor 'e' are preset, the messages is only transport encrypted.
ErroneousE2ee = b'e',
/// For Messages: force unencrypted message, either `ForcePlaintext::AddAutocryptHeader` (1),
/// `ForcePlaintext::NoAutocryptHeader` (2) or 0.
ForcePlaintext = b'u',
/// For Messages
WantsMdn = b'r',
/// For Messages
Forwarded = b'a',
/// For Messages
Cmd = b'S',
/// For Messages
Arg = b'E',
/// For Messages
Arg2 = b'F',
/// For Messages
Arg3 = b'G',
/// For Messages
Arg4 = b'H',
/// For Messages
Error = b'L',
/// For Messages
AttachGroupImage = b'A',
/// For Messages: space-separated list of messaged IDs of forwarded copies.
///
/// This is used when a [crate::message::Message] is in the
@@ -78,48 +58,32 @@ pub enum Param {
/// When the original message is then finally sent this parameter
/// is used to also send all the forwarded messages.
PrepForwards = b'P',
/// For Jobs
SetLatitude = b'l',
/// For Jobs
SetLongitude = b'n',
/// For Jobs
ServerFolder = b'Z',
/// For Jobs
ServerUid = b'z',
/// For Jobs
AlsoMove = b'M',
/// For Jobs: space-separated list of message recipients
Recipients = b'R',
/// For Groups
// For Groups
Unpromoted = b'U',
/// For Groups and Contacts
// For Groups and Contacts
ProfileImage = b'i',
/// For Chats
// For Chats
Selftalk = b'K',
/// For Chats
// For Chats
Devicetalk = b'D',
/// For QR
// For QR
Auth = b's',
/// For QR
// For QR
GroupId = b'x',
/// For QR
// For QR
GroupName = b'g',
/// For MDN-sending job
MsgId = b'I',
}
/// Possible values for `Param::ForcePlaintext`.
@@ -228,11 +192,6 @@ impl Params {
self.get(key).and_then(|s| s.parse().ok())
}
/// Get the given parameter and parse as `bool`.
pub fn get_bool(&self, key: Param) -> Option<bool> {
self.get_int(key).map(|v| v != 0)
}
/// Get the parameter behind `Param::Cmd` interpreted as `SystemMessage`.
pub fn get_cmd(&self) -> SystemMessage {
self.get_int(Param::Cmd)
@@ -291,7 +250,7 @@ impl Params {
let file = ParamsFile::from_param(context, val)?;
let blob = match file {
ParamsFile::FsPath(path) => match create {
true => BlobObject::new_from_path(context, path)?,
true => BlobObject::create_from_path(context, path)?,
false => BlobObject::from_path(context, path)?,
},
ParamsFile::Blob(blob) => blob,
@@ -316,12 +275,6 @@ impl Params {
Ok(Some(path))
}
pub fn get_msg_id(&self) -> Option<MsgId> {
self.get(Param::MsgId)
.and_then(|x| x.parse::<u32>().ok())
.map(MsgId::new)
}
/// Set the given paramter to the passed in `i32`.
pub fn set_int(&mut self, key: Param, value: i32) -> &mut Self {
self.set(key, format!("{}", value));

View File

@@ -5,6 +5,7 @@ use std::fmt;
use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::chat::*;
use crate::constants::*;
use crate::context::Context;
use crate::key::*;
@@ -94,7 +95,6 @@ pub enum ToSave {
pub enum DegradeEvent {
/// Recoverable by an incoming encrypted mail.
EncryptionPaused = 0x01,
/// Recoverable by a new verify.
FingerprintChanged = 0x02,
}
@@ -417,6 +417,7 @@ impl<'a> Peerstate<'a> {
&self.addr,
],
)?;
reset_gossiped_timestamp(self.context, 0);
} else if self.to_save == Some(ToSave::Timestamps) {
sql::execute(
self.context,

View File

@@ -16,7 +16,7 @@ use pgp::types::{
};
use rand::{thread_rng, CryptoRng, Rng};
use crate::error::Result;
use crate::error::Error;
use crate::key::*;
use crate::keyring::*;
@@ -88,7 +88,9 @@ impl<'a> PublicKeyTrait for SignedPublicKeyOrSubkey<'a> {
/// Split data from PGP Armored Data as defined in https://tools.ietf.org/html/rfc4880#section-6.2.
///
/// Returns (type, headers, base64 encoded body).
pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>)> {
pub fn split_armored_data(
buf: &[u8],
) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>), Error> {
use std::io::Read;
let cursor = Cursor::new(buf);
@@ -192,7 +194,7 @@ pub fn pk_encrypt(
plain: &[u8],
public_keys_for_encryption: &Keyring,
private_key_for_signing: Option<&Key>,
) -> Result<String> {
) -> Result<String, Error> {
let lit_msg = Message::new_literal_bytes("", plain);
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
.keys()
@@ -234,7 +236,7 @@ pub fn pk_decrypt(
private_keys_for_decryption: &Keyring,
public_keys_for_validation: &Keyring,
ret_signature_fingerprints: Option<&mut HashSet<String>>,
) -> Result<Vec<u8>> {
) -> Result<Vec<u8>, Error> {
let (msg, _) = Message::from_armor_single(Cursor::new(ctext))?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption
.keys()
@@ -246,7 +248,7 @@ pub fn pk_decrypt(
.collect();
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
let msgs = decryptor.collect::<Result<Vec<_>, _>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
let dec_msg = &msgs[0];
@@ -278,7 +280,7 @@ pub fn pk_decrypt(
}
/// Symmetric encryption.
pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String, Error> {
let mut rng = thread_rng();
let lit_msg = Message::new_literal_bytes("", plain);
@@ -295,11 +297,11 @@ pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
pub fn symm_decrypt<T: std::io::Read + std::io::Seek>(
passphrase: &str,
ctext: T,
) -> Result<Vec<u8>> {
) -> Result<Vec<u8>, Error> {
let (enc_msg, _) = Message::from_armor_single(ctext)?;
let decryptor = enc_msg.decrypt_with_password(|| passphrase.into())?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
let msgs = decryptor.collect::<Result<Vec<_>, _>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
match msgs[0].get_content()? {

View File

@@ -69,7 +69,7 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
(fp, &rest[1..])
}) {
Some(pair) => pair,
None => (payload, ""),
None => return format_err!("Invalid OPENPGP4FPR found").into(),
};
// replace & with \n to match expected param format
@@ -83,11 +83,11 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
let addr = if let Some(addr) = param.get(Param::Forwarded) {
match normalize_address(addr) {
Ok(addr) => Some(addr),
Ok(addr) => addr,
Err(err) => return err.into(),
}
} else {
None
return format_err!("Missing address").into();
};
// what is up with that param name?
@@ -157,7 +157,7 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
lot.state = LotState::QrFprWithoutAddr;
lot.text1 = Some(dc_format_fingerprint(&fingerprint));
}
} else if let Some(addr) = addr {
} else {
if grpid.is_some() && grpname.is_some() {
lot.state = LotState::QrAskVerifyGroup;
lot.text1 = grpname;
@@ -172,8 +172,6 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
lot.fingerprint = Some(fingerprint);
lot.invitenumber = invitenumber;
lot.auth = auth;
} else {
return format_err!("Missing address").into();
}
lot
@@ -473,24 +471,4 @@ mod tests {
assert_eq!(contact.get_addr(), "cli@deltachat.de");
assert_eq!(contact.get_name(), "Jörn P. P.");
}
#[test]
fn test_decode_openpgp_without_addr() {
let ctx = dummy_context();
let res = check_qr(
&ctx.ctx,
"OPENPGP4FPR:1234567890123456789012345678901234567890",
);
assert_eq!(res.get_state(), LotState::QrFprWithoutAddr);
assert_eq!(
res.get_text1().unwrap(),
"1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890"
);
assert_eq!(res.get_id(), 0);
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890");
assert_eq!(res.get_state(), LotState::QrError);
assert_eq!(res.get_id(), 0);
}
}

View File

@@ -3,7 +3,7 @@
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId};
use crate::chat::{self, Chat};
use crate::config::*;
use crate::constants::*;
use crate::contact::*;
@@ -11,7 +11,6 @@ use crate::context::Context;
use crate::e2ee::*;
use crate::error::Error;
use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::key::*;
use crate::lot::LotState;
use crate::message::Message;
@@ -65,18 +64,15 @@ macro_rules! get_qr_attr {
};
}
pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<String> {
/*=======================================================
pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: u32) -> Option<String> {
/* =========================================================
==== Alice - the inviter side ====
==== Step 1 in "Setup verified contact" protocol ====
=======================================================*/
========================================================= */
let fingerprint: String;
ensure_secret_key_exists(context).ok();
// invitenumber will be used to allow starting the handshake,
// auth will be used to verify the fingerprint
let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group_chat_id);
let auth = token::lookup_or_new(context, token::Namespace::Auth, group_chat_id);
let self_addr = match context.get_config(Config::ConfiguredAddr) {
@@ -101,8 +97,7 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<
let self_name_urlencoded =
utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
let qr = if !group_chat_id.is_unset() {
// parameters used: a=g=x=i=s=
let qr = if 0 != group_chat_id {
if let Ok(chat) = Chat::load_from_db(context, group_chat_id) {
let group_name = chat.get_name();
let group_name_urlencoded =
@@ -122,7 +117,6 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<
return None;
}
} else {
// parameters used: a=n=i=s=
Some(format!(
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
fingerprint, self_addr_urlencoded, self_name_urlencoded, &invitenumber, &auth,
@@ -143,41 +137,36 @@ fn get_self_fingerprint(context: &Context) -> Option<String> {
None
}
/// Take a scanned QR-code and do the setup-contact/join-group handshake.
/// See the ffi-documentation for more details.
pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
pub fn dc_join_securejoin(context: &Context, qr: &str) -> u32 {
let cleanup =
|context: &Context, contact_chat_id: ChatId, ongoing_allocated: bool, join_vg: bool| {
|context: &Context, contact_chat_id: u32, ongoing_allocated: bool, join_vg: bool| {
let mut bob = context.bob.write().unwrap();
bob.expects = 0;
let ret_chat_id: ChatId = if bob.status == DC_BOB_SUCCESS {
let ret_chat_id = if bob.status == DC_BOB_SUCCESS {
if join_vg {
chat::get_chat_id_by_grpid(
context,
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
)
.unwrap_or((ChatId::new(0), false, Blocked::Not))
.0
} else {
contact_chat_id
}
} else {
ChatId::new(0)
0
};
bob.qr_scan = None;
if ongoing_allocated {
context.free_ongoing();
}
ret_chat_id
ret_chat_id as u32
};
/*========================================================
/* ==========================================================
==== Bob - the joiner's side =====
==== Step 2 in "Setup verified contact" protocol =====
========================================================*/
let mut contact_chat_id = ChatId::new(0);
========================================================== */
let mut contact_chat_id: u32 = 0;
let mut join_vg: bool = false;
info!(context, "Requesting secure-join ...",);
@@ -191,13 +180,11 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
error!(context, "Unknown QR code.",);
return cleanup(&context, contact_chat_id, true, join_vg);
}
contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id) {
Ok(chat_id) => chat_id,
Err(_) => {
error!(context, "Unknown contact.");
return cleanup(&context, contact_chat_id, true, join_vg);
}
};
contact_chat_id = chat::create_by_contact_id(context, qr_scan.id).unwrap_or_default();
if contact_chat_id == 0 {
error!(context, "Unknown contact.",);
return cleanup(&context, contact_chat_id, true, join_vg);
}
if context.shall_stop_ongoing() {
return cleanup(&context, contact_chat_id, true, join_vg);
}
@@ -221,14 +208,10 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
.unwrap(),
contact_chat_id,
) {
// the scanned fingerprint matches Alice's key,
// we can proceed to step 4b) directly and save two mails
info!(context, "Taking protocol shortcut.");
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM;
joiner_progress!(context, chat_id_2_contact_id(context, contact_chat_id), 400);
let own_fingerprint = get_self_fingerprint(context).unwrap_or_default();
// Bob -> Alice
send_handshake_msg(
context,
contact_chat_id,
@@ -247,8 +230,6 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
);
} else {
context.bob.write().unwrap().expects = DC_VC_AUTH_REQUIRED;
// Bob -> Alice
send_handshake_msg(
context,
contact_chat_id,
@@ -259,30 +240,24 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
);
}
if join_vg {
// for a group-join, wait until the secure-join is done and the group is created
while !context.shall_stop_ongoing() {
std::thread::sleep(std::time::Duration::from_millis(200));
}
cleanup(&context, contact_chat_id, true, join_vg)
} else {
// for a one-to-one-chat, the chat is already known, return the chat-id,
// the verification runs in background
context.free_ongoing();
contact_chat_id
// Bob -> Alice
while !context.shall_stop_ongoing() {
// Don't sleep too long, the user is waiting.
std::thread::sleep(std::time::Duration::from_millis(200));
}
cleanup(&context, contact_chat_id, true, join_vg)
}
fn send_handshake_msg(
context: &Context,
contact_chat_id: ChatId,
contact_chat_id: u32,
step: &str,
param2: impl AsRef<str>,
fingerprint: Option<String>,
grpid: impl AsRef<str>,
) {
let mut msg = Message::default();
msg.viewtype = Viewtype::Text;
msg.type_0 = Viewtype::Text;
msg.text = Some(format!("Secure-Join: {}", step));
msg.hidden = true;
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
@@ -312,7 +287,7 @@ fn send_handshake_msg(
chat::send_msg(context, contact_chat_id, &mut msg).unwrap_or_default();
}
fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
fn chat_id_2_contact_id(context: &Context, contact_chat_id: u32) -> u32 {
let contacts = chat::get_chat_contacts(context, contact_chat_id);
if contacts.len() == 1 {
contacts[0]
@@ -324,7 +299,7 @@ fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
fn fingerprint_equals_sender(
context: &Context,
fingerprint: impl AsRef<str>,
contact_chat_id: ChatId,
contact_chat_id: u32,
) -> bool {
let contacts = chat::get_chat_contacts(context, contact_chat_id);
@@ -343,115 +318,80 @@ fn fingerprint_equals_sender(
}
false
}
#[derive(Fail, Debug)]
pub(crate) enum HandshakeError {
#[fail(display = "Can not be called with special contact ID")]
SpecialContactId,
#[fail(display = "Not a Secure-Join message")]
NotSecureJoinMsg,
#[fail(
display = "Failed to look up or create chat for contact #{}",
contact_id
)]
NoChat {
contact_id: u32,
#[cause]
cause: Error,
},
#[fail(display = "Chat for group {} not found", group)]
ChatNotFound { group: String },
#[fail(display = "No configured self address found")]
NoSelfAddr,
pub(crate) struct HandshakeMessageStatus {
pub(crate) hide_this_msg: bool,
pub(crate) delete_this_msg: bool,
pub(crate) stop_ongoing_process: bool,
pub(crate) bob_securejoin_success: Option<bool>,
}
/// What to do with a Secure-Join handshake message after it was handled.
pub(crate) enum HandshakeMessage {
/// The message has been fully handled and should be removed/delete.
Done,
/// The message should be ignored/hidden, but not removed/deleted.
Ignore,
/// The message should be further processed by incoming message handling.
Propagate,
impl Default for HandshakeMessageStatus {
fn default() -> Self {
Self {
hide_this_msg: true,
delete_this_msg: false,
stop_ongoing_process: false,
bob_securejoin_success: None,
}
}
}
/// Handle incoming secure-join handshake.
///
/// This function will update the securejoin state in [Context::bob]
/// and also terminate the ongoing process using
/// [Context::stop_ongoing] as required by the protocol.
///
/// A message which results in [Err] will be hidden from the user but
/// not deleted, it may be a valid message for something else we are
/// not aware off. E.g. it could be part of a handshake performed by
/// another DC app on the same account.
///
/// When handle_securejoin_handshake() is called,
/// the message is not yet filed in the database;
/// this is done by receive_imf() later on as needed.
pub(crate) fn handle_securejoin_handshake(
context: &Context,
mime_message: &MimeMessage,
mimeparser: &MimeParser,
contact_id: u32,
) -> Result<HandshakeMessage, HandshakeError> {
) -> Result<HandshakeMessageStatus, Error> {
let own_fingerprint: String;
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return Err(HandshakeError::SpecialContactId);
}
let step = mime_message
.get(HeaderDef::SecureJoin)
.ok_or(HandshakeError::NotSecureJoinMsg)?;
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"handle_securejoin_handshake(): called with special contact id"
);
let step = mimeparser
.lookup_field("Secure-Join")
.ok_or_else(|| format_err!("This message is not a Secure-Join message"))?;
info!(
context,
">>>>>>>>>>>>>>>>>>>>>>>>> secure-join message \'{}\' received", step,
);
let contact_chat_id =
match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not) {
Ok((chat_id, blocked)) => {
if blocked != Blocked::Not {
chat_id.unblock(context);
}
chat_id
}
Err(err) => {
return Err(HandshakeError::NoChat {
contact_id,
cause: err,
});
}
};
let (contact_chat_id, contact_chat_id_blocked) =
chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not).unwrap_or_default();
if contact_chat_id_blocked != Blocked::Not {
chat::unblock(context, contact_chat_id);
}
let join_vg = step.starts_with("vg-");
let mut ret = HandshakeMessageStatus::default();
match step.as_str() {
"vg-request" | "vc-request" => {
/*=======================================================
/* =========================================================
==== Alice - the inviter side ====
==== Step 3 in "Setup verified contact" protocol ====
=======================================================*/
// this message may be unencrypted (Bob, the joiner and the sender, might not have Alice's key yet)
========================================================= */
// this message may be unencrypted (Bob, the joinder and the sender, might not have Alice's key yet)
// it just ensures, we have Bobs key now. If we do _not_ have the key because eg. MitM has removed it,
// send_message() will fail with the error "End-to-end-encryption unavailable unexpectedly.", so, there is no additional check needed here.
// verify that the `Secure-Join-Invitenumber:`-header matches invitenumber written to the QR code
let invitenumber = match mime_message.get(HeaderDef::SecureJoinInvitenumber) {
let invitenumber = match mimeparser.lookup_field("Secure-Join-Invitenumber") {
Some(n) => n,
None => {
warn!(context, "Secure-join denied (invitenumber missing)");
return Ok(HandshakeMessage::Ignore);
warn!(context, "Secure-join denied (invitenumber missing).",);
return Ok(ret);
}
};
if !token::exists(context, token::Namespace::InviteNumber, &invitenumber) {
warn!(context, "Secure-join denied (bad invitenumber).");
return Ok(HandshakeMessage::Ignore);
warn!(context, "Secure-join denied (bad invitenumber).",);
return Ok(ret);
}
info!(context, "Secure-join requested.",);
inviter_progress!(context, contact_id, 300);
// Alice -> Bob
send_handshake_msg(
context,
contact_chat_id,
@@ -460,15 +400,8 @@ pub(crate) fn handle_securejoin_handshake(
None,
"",
);
Ok(HandshakeMessage::Done)
}
"vg-auth-required" | "vc-auth-required" => {
/*========================================================
==== Bob - the joiner's side =====
==== Step 4 in "Setup verified contact" protocol =====
========================================================*/
// verify that Alice's Autocrypt key and fingerprint matches the QR-code
let cond = {
let bob = context.bob.read().unwrap();
let scan = bob.qr_scan.as_ref();
@@ -478,26 +411,26 @@ pub(crate) fn handle_securejoin_handshake(
};
if cond {
warn!(context, "auth-required message out of sync.");
warn!(context, "auth-required message out of sync.",);
// no error, just aborted somehow or a mail from another handshake
return Ok(HandshakeMessage::Ignore);
return Ok(ret);
}
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
let auth = get_qr_attr!(context, auth).to_string();
if !encrypted_and_signed(mime_message, &scanned_fingerprint_of_alice) {
if !encrypted_and_signed(mimeparser, &scanned_fingerprint_of_alice) {
could_not_establish_secure_connection(
context,
contact_chat_id,
if mime_message.was_encrypted() {
if mimeparser.encrypted {
"No valid signature."
} else {
"Not encrypted."
},
);
context.bob.write().unwrap().status = 0; // secure-join failed
context.stop_ongoing();
return Ok(HandshakeMessage::Ignore);
ret.stop_ongoing_process = true;
ret.bob_securejoin_success = Some(false);
return Ok(ret);
}
if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id) {
could_not_establish_secure_connection(
@@ -505,16 +438,15 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on joiner-side.",
);
context.bob.write().unwrap().status = 0; // secure-join failed
context.stop_ongoing();
return Ok(HandshakeMessage::Ignore);
ret.stop_ongoing_process = true;
ret.bob_securejoin_success = Some(false);
return Ok(ret);
}
info!(context, "Fingerprint verified.",);
own_fingerprint = get_self_fingerprint(context).unwrap();
joiner_progress!(context, contact_id, 400);
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM;
// Bob -> Alice
send_handshake_msg(
context,
contact_chat_id,
@@ -527,17 +459,15 @@ pub(crate) fn handle_securejoin_handshake(
"".to_string()
},
);
Ok(HandshakeMessage::Done)
}
"vg-request-with-auth" | "vc-request-with-auth" => {
/*==========================================================
/* ============================================================
==== Alice - the inviter side ====
==== Steps 5+6 in "Setup verified contact" protocol ====
==== Step 6 in "Out-of-band verified groups" protocol ====
==========================================================*/
============================================================ */
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
let fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint) {
let fingerprint = match mimeparser.lookup_field("Secure-Join-Fingerprint") {
Some(fp) => fp,
None => {
could_not_establish_secure_connection(
@@ -545,16 +475,16 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint not provided.",
);
return Ok(HandshakeMessage::Ignore);
return Ok(ret);
}
};
if !encrypted_and_signed(mime_message, &fingerprint) {
if !encrypted_and_signed(mimeparser, &fingerprint) {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Auth not encrypted.",
);
return Ok(HandshakeMessage::Ignore);
return Ok(ret);
}
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id) {
could_not_establish_secure_connection(
@@ -562,11 +492,11 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
);
return Ok(HandshakeMessage::Ignore);
return Ok(ret);
}
info!(context, "Fingerprint verified.",);
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
let auth_0 = match mime_message.get(HeaderDef::SecureJoinAuth) {
let auth_0 = match mimeparser.lookup_field("Secure-Join-Auth") {
Some(auth) => auth,
None => {
could_not_establish_secure_connection(
@@ -574,12 +504,12 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Auth not provided.",
);
return Ok(HandshakeMessage::Ignore);
return Ok(ret);
}
};
if !token::exists(context, token::Namespace::Auth, &auth_0) {
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.");
return Ok(HandshakeMessage::Ignore);
return Ok(ret);
}
if mark_peer_as_verified(context, fingerprint).is_err() {
could_not_establish_secure_connection(
@@ -587,7 +517,7 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
);
return Ok(HandshakeMessage::Ignore);
return Ok(ret);
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited);
info!(context, "Auth verified.",);
@@ -595,76 +525,49 @@ pub(crate) fn handle_securejoin_handshake(
emit_event!(context, Event::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
if join_vg {
// the vg-member-added message is special:
// this is a normal Chat-Group-Member-Added message
// with an additional Secure-Join header
let field_grpid = match mime_message.get(HeaderDef::SecureJoinGroup) {
Some(s) => s.as_str(),
None => {
warn!(context, "Missing Secure-Join-Group header");
return Ok(HandshakeMessage::Ignore);
}
};
match chat::get_chat_id_by_grpid(context, field_grpid) {
Ok((group_chat_id, _, _)) => {
if let Err(err) =
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
{
error!(context, "failed to add contact: {}", err);
}
}
Err(err) => {
error!(context, "Chat {} not found: {}", &field_grpid, err);
return Err(HandshakeError::ChatNotFound {
group: field_grpid.to_string(),
});
}
let field_grpid = mimeparser
.lookup_field("Secure-Join-Group")
.map(|s| s.as_str())
.unwrap_or_else(|| "");
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, field_grpid);
if group_chat_id == 0 {
error!(context, "Chat {} not found.", &field_grpid);
return Ok(ret);
} else if let Err(err) =
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
{
error!(context, "failed to add contact: {}", err);
}
} else {
// Alice -> Bob
send_handshake_msg(context, contact_chat_id, "vc-contact-confirm", "", None, "");
inviter_progress!(context, contact_id, 1000);
}
Ok(HandshakeMessage::Done)
}
"vg-member-added" | "vc-contact-confirm" => {
/*=======================================================
==== Bob - the joiner's side ====
==== Step 7 in "Setup verified contact" protocol ====
=======================================================*/
let abort_retval = if join_vg {
HandshakeMessage::Propagate
} else {
HandshakeMessage::Ignore
};
if join_vg {
ret.hide_this_msg = false;
}
if context.bob.read().unwrap().expects != DC_VC_CONTACT_CONFIRM {
info!(context, "Message belongs to a different handshake.",);
return Ok(abort_retval);
return Ok(ret);
}
let cond = {
let bob = context.bob.read().unwrap();
let scan = bob.qr_scan.as_ref();
scan.is_none() || (join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup)
scan.is_none() || join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup
};
if cond {
warn!(
context,
"Message out of sync or belongs to a different handshake.",
);
return Ok(abort_retval);
return Ok(ret);
}
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
let vg_expect_encrypted = if join_vg {
let group_id = get_qr_attr!(context, text2).to_string();
// This is buggy, is_verified_group will always be
// false since the group is created by receive_imf by
// the very handshake message we're handling now. But
// only after we have returned. It does not impact
// the security invariants of secure-join however.
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, &group_id)
.unwrap_or((ChatId::new(0), false, Blocked::Not));
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, group_id);
// when joining a non-verified group
// the vg-member-added message may be unencrypted
// when not all group members have keys or prefer encryption.
@@ -675,15 +578,15 @@ pub(crate) fn handle_securejoin_handshake(
true
};
if vg_expect_encrypted
&& !encrypted_and_signed(mime_message, &scanned_fingerprint_of_alice)
&& !encrypted_and_signed(mimeparser, &scanned_fingerprint_of_alice)
{
could_not_establish_secure_connection(
context,
contact_chat_id,
"Contact confirm message not encrypted.",
);
context.bob.write().unwrap().status = 0;
return Ok(abort_retval);
ret.bob_securejoin_success = Some(false);
return Ok(ret);
}
if mark_peer_as_verified(context, &scanned_fingerprint_of_alice).is_err() {
@@ -692,26 +595,21 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on joiner-side.",
);
return Ok(abort_retval);
return Ok(ret);
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined);
emit_event!(context, Event::ContactsChanged(None));
let cg_member_added = mime_message
.get(HeaderDef::ChatGroupMemberAdded)
let cg_member_added = mimeparser
.lookup_field("Chat-Group-Member-Added")
.map(|s| s.as_str())
.unwrap_or_else(|| "");
if join_vg
&& !context
.is_self_addr(cg_member_added)
.map_err(|_| HandshakeError::NoSelfAddr)?
{
if join_vg && !addr_equals_self(context, cg_member_added) {
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
return Ok(abort_retval);
return Ok(ret);
}
secure_connection_established(context, contact_chat_id);
context.bob.write().unwrap().expects = 0;
if join_vg {
// Bob -> Alice
send_handshake_msg(
context,
contact_chat_id,
@@ -721,56 +619,48 @@ pub(crate) fn handle_securejoin_handshake(
"",
);
}
context.bob.write().unwrap().status = 1;
context.stop_ongoing();
Ok(if join_vg {
HandshakeMessage::Propagate
} else {
HandshakeMessage::Done
})
ret.stop_ongoing_process = true;
ret.bob_securejoin_success = Some(true);
}
"vg-member-added-received" => {
/*==========================================================
/* ============================================================
==== Alice - the inviter side ====
==== Step 8 in "Out-of-band verified groups" protocol ====
==========================================================*/
============================================================ */
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
if contact.is_verified(context) == VerifiedStatus::Unverified {
warn!(context, "vg-member-added-received invalid.",);
return Ok(HandshakeMessage::Ignore);
return Ok(ret);
}
inviter_progress!(context, contact_id, 800);
inviter_progress!(context, contact_id, 1000);
let field_grpid = mime_message
.get(HeaderDef::SecureJoinGroup)
let field_grpid = mimeparser
.lookup_field("Secure-Join-Group")
.map(|s| s.as_str())
.unwrap_or_else(|| "");
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid)
.map_err(|err| {
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
HandshakeError::ChatNotFound {
group: field_grpid.to_string(),
}
})?;
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid);
context.call_cb(Event::SecurejoinMemberAdded {
chat_id: group_chat_id,
contact_id,
});
Ok(HandshakeMessage::Done)
} else {
warn!(context, "vg-member-added-received invalid.",);
Ok(HandshakeMessage::Ignore)
return Ok(ret);
}
}
_ => {
warn!(context, "invalid step: {}", step);
Ok(HandshakeMessage::Ignore)
}
}
if ret.hide_this_msg {
ret.delete_this_msg = true;
}
Ok(ret)
}
fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
fn secure_connection_established(context: &Context, contact_chat_id: u32) {
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id);
let contact = Contact::get_by_id(context, contact_id);
let addr = if let Ok(ref contact) = contact {
@@ -783,11 +673,7 @@ fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
emit_event!(context, Event::ChatModified(contact_chat_id));
}
fn could_not_establish_secure_connection(
context: &Context,
contact_chat_id: ChatId,
details: &str,
) {
fn could_not_establish_secure_connection(context: &Context, contact_chat_id: u32, details: &str) {
let contact_id = chat_id_2_contact_id(context, contact_chat_id);
let contact = Contact::get_by_id(context, contact_id);
let msg = context.stock_string_repl_str(
@@ -830,8 +716,8 @@ fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Res
* Tools: Misc.
******************************************************************************/
fn encrypted_and_signed(mimeparser: &MimeMessage, expected_fingerprint: impl AsRef<str>) -> bool {
if !mimeparser.was_encrypted() {
fn encrypted_and_signed(mimeparser: &MimeParser, expected_fingerprint: impl AsRef<str>) -> bool {
if !mimeparser.encrypted {
warn!(mimeparser.context, "Message not encrypted.",);
false
} else if mimeparser.signatures.is_empty() {

View File

@@ -1,251 +0,0 @@
/// 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
// back to `-- `
match line {
"-- " | "-- " => return &lines[..ix],
_ => (),
}
}
lines
}
/// Remove nonstandard footer and a boolean indicating whether such
/// footer was removed.
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
for (ix, &line) in lines.iter().enumerate() {
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> {
buf.split('\n').collect()
}
/// Simplify message text for chat display.
/// Remove quotes, signatures, trailing empty lines etc.
pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool) {
input.retain(|c| c != '\r');
let lines = split_lines(&input);
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;
for (l, line) in lines.iter().enumerate().rev() {
if is_plain_quote(line) {
last_quoted_line = Some(l)
} else if !is_empty_line(line) {
break;
}
}
if let Some(mut l_last) = last_quoted_line {
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
l_last -= 1
}
if l_last > 1 {
let line = lines[l_last - 1];
if is_quoted_headline(line) {
l_last -= 1
}
}
(&lines[..l_last], true)
} else {
(lines, false)
}
}
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
let mut last_quoted_line = None;
let mut has_quoted_headline = false;
for (l, line) in lines.iter().enumerate() {
if is_plain_quote(line) {
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;
}
}
}
if let Some(last_quoted_line) = last_quoted_line {
(&lines[last_quoted_line + 1..], true)
} else {
(lines, false)
}
}
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 {
ret += "[...]";
}
/* we write empty lines only in case and non-empty line follows */
let mut pending_linebreaks = 0;
let mut empty_body = true;
for line in lines {
if is_empty_line(line) {
pending_linebreaks += 1
} else {
if !empty_body {
if pending_linebreaks > 2 {
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_cut_at_end && (!is_cut_at_begin || !empty_body) {
ret += " [...]";
}
ret
}
/**
* Tools
*/
fn is_empty_line(buf: &str) -> bool {
// XXX: can it be simplified to buf.chars().all(|c| c.is_whitespace())?
//
// Strictly speaking, it is not equivalent (^A is not whitespace, but less than ' '),
// but having control sequences in email body?!
//
// See discussion at: https://github.com/deltachat/deltachat-core-rust/pull/402#discussion_r317062392
for c in buf.chars() {
if c > ' ' {
return false;
}
}
true
}
fn is_quoted_headline(buf: &str) -> bool {
/* This function may be called for the line _directly_ before a quote.
The function checks if the line contains sth. like "On 01.02.2016, xy@z wrote:" in various languages.
- Currently, we simply check if the last character is a ':'.
- Checking for the existence of an email address may fail (headlines may show the user's name instead of the address) */
buf.len() <= 80 && buf.ends_with(':')
}
fn is_plain_quote(buf: &str) -> bool {
buf.starts_with('>')
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
// proptest does not support [[:graphical:][:space:]] regex.
fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") {
let (output, _is_forwarded) = simplify(input, true);
assert!(output.split('\n').all(|s| s != "-- "));
}
}
#[test]
fn test_simplify_trim() {
let input = "line1\n\r\r\rline2".to_string();
let (plain, is_forwarded) = simplify(input, false);
assert_eq!(plain, "line1\nline2");
assert!(!is_forwarded);
}
#[test]
fn test_simplify_forwarded_message() {
let input = "---------- Forwarded message ----------\r\nFrom: test@example.com\r\n\r\nForwarded message\r\n-- \r\nSignature goes here".to_string();
let (plain, is_forwarded) = simplify(input, false);
assert_eq!(plain, "Forwarded message");
assert!(is_forwarded);
}
#[test]
fn test_simplify_utilities() {
assert!(is_empty_line(" \t"));
assert!(is_empty_line(""));
assert!(is_empty_line(" \r"));
assert!(!is_empty_line(" x"));
assert!(is_plain_quote("> hello world"));
assert!(is_plain_quote(">>"));
assert!(!is_plain_quote("Life is pain"));
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);
}
}

View File

@@ -2,49 +2,31 @@
pub mod send;
use std::time::Duration;
use async_smtp::smtp::client::net::*;
use async_smtp::*;
use lettre::smtp::client::net::*;
use lettre::*;
use crate::constants::*;
use crate::context::Context;
use crate::events::Event;
use crate::login_param::{dc_build_tls, LoginParam};
use crate::login_param::{dc_build_tls_config, LoginParam};
use crate::oauth2::*;
/// SMTP write and read timeout in seconds.
const SMTP_TIMEOUT: u64 = 30;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Bad parameters")]
BadParameters,
#[fail(display = "Invalid login address {}: {}", address, error)]
InvalidLoginAddress {
address: String,
#[cause]
error: error::Error,
error: lettre::error::Error,
},
#[fail(display = "SMTP failed to connect: {:?}", _0)]
ConnectionFailure(#[cause] smtp::error::Error),
ConnectionFailure(#[cause] lettre::smtp::error::Error),
#[fail(display = "SMTP: failed to setup connection {:?}", _0)]
ConnectionSetupFailure(#[cause] smtp::error::Error),
ConnectionSetupFailure(#[cause] lettre::smtp::error::Error),
#[fail(display = "SMTP: oauth2 error {:?}", _0)]
Oauth2Error { address: String },
#[fail(display = "TLS error")]
Tls(#[cause] async_native_tls::Error),
}
impl From<async_native_tls::Error> for Error {
fn from(err: async_native_tls::Error) -> Error {
Error::Tls(err)
}
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -52,7 +34,8 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Default, DebugStub)]
pub struct Smtp {
#[debug_stub(some = "SmtpTransport")]
transport: Option<smtp::SmtpTransport>,
transport: Option<lettre::smtp::SmtpTransport>,
transport_connected: bool,
/// Email address we are sending from.
from: Option<EmailAddress>,
}
@@ -65,25 +48,22 @@ impl Smtp {
/// Disconnect the SMTP transport and drop it entirely.
pub fn disconnect(&mut self) {
if let Some(mut transport) = self.transport.take() {
async_std::task::block_on(transport.close()).ok();
if self.transport.is_none() || !self.transport_connected {
return;
}
let mut transport = self.transport.take().unwrap();
transport.close();
self.transport_connected = false;
}
/// Check whether we are connected.
/// Check if a connection already exists.
pub fn is_connected(&self) -> bool {
self.transport
.as_ref()
.map(|t| t.is_connected())
.unwrap_or_default()
self.transport.is_some()
}
/// Connect using the provided login params.
/// Connect using the provided login params
pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> {
async_std::task::block_on(self.inner_connect(context, lp))
}
async fn inner_connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> {
if self.is_connected() {
warn!(context, "SMTP already connected.");
return Ok(());
@@ -104,7 +84,7 @@ impl Smtp {
let domain = &lp.send_server;
let port = lp.send_port as u16;
let tls_config = dc_build_tls(lp.smtp_certificate_checks);
let tls_config = dc_build_tls_config(lp.smtp_certificate_checks);
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls_config);
let (creds, mechanism) = if 0 != lp.server_flags & (DC_LP_AUTH_OAUTH2 as i32) {
@@ -119,21 +99,21 @@ impl Smtp {
}
let user = &lp.send_user;
(
smtp::authentication::Credentials::new(
lettre::smtp::authentication::Credentials::new(
user.to_string(),
access_token.unwrap_or_default(),
),
vec![smtp::authentication::Mechanism::Xoauth2],
vec![lettre::smtp::authentication::Mechanism::Xoauth2],
)
} else {
// plain
let user = lp.send_user.clone();
let pw = lp.send_pw.clone();
(
smtp::authentication::Credentials::new(user, pw),
lettre::smtp::authentication::Credentials::new(user, pw),
vec![
smtp::authentication::Mechanism::Plain,
smtp::authentication::Mechanism::Login,
lettre::smtp::authentication::Mechanism::Plain,
lettre::smtp::authentication::Mechanism::Login,
],
)
};
@@ -141,31 +121,28 @@ impl Smtp {
let security = if 0
!= lp.server_flags & (DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_PLAIN) as i32
{
smtp::ClientSecurity::Opportunistic(tls_parameters)
lettre::smtp::ClientSecurity::Opportunistic(tls_parameters)
} else {
smtp::ClientSecurity::Wrapper(tls_parameters)
lettre::smtp::ClientSecurity::Wrapper(tls_parameters)
};
let client = smtp::SmtpClient::with_security((domain.as_str(), port), security)
.await
let client = lettre::smtp::SmtpClient::new((domain.as_str(), port), security)
.map_err(Error::ConnectionSetupFailure)?;
let client = client
.smtp_utf8(true)
.credentials(creds)
.authentication_mechanism(mechanism)
.connection_reuse(smtp::ConnectionReuseParameters::ReuseUnlimited)
.timeout(Some(Duration::from_secs(SMTP_TIMEOUT)));
let mut trans = client.into_transport();
trans.connect().await.map_err(Error::ConnectionFailure)?;
.connection_reuse(lettre::smtp::ConnectionReuseParameters::ReuseUnlimited);
let mut trans = client.transport();
trans.connect().map_err(Error::ConnectionFailure)?;
self.transport = Some(trans);
self.transport_connected = true;
context.call_cb(Event::SmtpConnected(format!(
"SMTP-LOGIN as {} ok",
lp.send_user,
)));
Ok(())
}
}

View File

@@ -1,7 +1,7 @@
//! # SMTP message sending
use super::Smtp;
use async_smtp::*;
use lettre::*;
use crate::context::Context;
use crate::events::Event;
@@ -11,11 +11,9 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Envelope error: {}", _0)]
EnvelopeError(#[cause] async_smtp::error::Error),
EnvelopeError(#[cause] lettre::error::Error),
#[fail(display = "Send error: {}", _0)]
SendError(#[cause] async_smtp::smtp::error::Error),
SendError(#[cause] lettre::smtp::error::Error),
#[fail(display = "SMTP has no transport")]
NoTransport,
}
@@ -23,7 +21,7 @@ pub enum Error {
impl Smtp {
/// Send a prepared mail to recipients.
/// On successful send out Ok() is returned.
pub async fn send(
pub fn send(
&mut self,
context: &Context,
recipients: Vec<EmailAddress>,
@@ -38,21 +36,22 @@ impl Smtp {
.collect::<Vec<String>>()
.join(",");
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
message,
);
if let Some(ref mut transport) = self.transport {
transport.send(mail).await.map_err(Error::SendError)?;
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
message,
);
transport.send(mail).map_err(Error::SendError)?;
context.call_cb(Event::SmtpMessageSent(format!(
"Message len={} was smtp-sent to {}",
message_len, recipients_display
)));
self.transport_connected = true;
Ok(())
} else {
warn!(

View File

@@ -7,7 +7,7 @@ use std::time::Duration;
use rusqlite::{Connection, OpenFlags, Statement, NO_PARAMS};
use thread_local_object::ThreadLocal;
use crate::chat::{update_device_icon, update_saved_messages_icon};
use crate::chat::update_saved_messages_icon;
use crate::constants::ShowEmails;
use crate::context::Context;
use crate::dc_tools::*;
@@ -365,7 +365,6 @@ fn table_exists(conn: &Connection, name: impl AsRef<str>) -> Result<bool> {
Ok(exists)
}
#[allow(clippy::cognitive_complexity)]
fn open(
context: &Context,
sql: &Sql,
@@ -545,7 +544,7 @@ fn open(
// --------------------------------------------------------------------
let mut dbversion = dbversion_before_update;
let mut recalc_fingerprints = false;
let mut recalc_fingerprints = 0;
let mut update_icons = false;
if dbversion < 1 {
@@ -688,7 +687,7 @@ fn open(
"CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);",
params![],
)?;
recalc_fingerprints = true;
recalc_fingerprints = 1;
dbversion = 34;
sql.set_raw_config_int(context, "dbversion", 34)?;
}
@@ -849,6 +848,7 @@ fn open(
if exists_before_update && sql.get_raw_config_int(context, "bcc_self").is_none() {
sql.set_raw_config_int(context, "bcc_self", 1)?;
}
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 59)?;
}
if dbversion < 60 {
@@ -859,21 +859,12 @@ fn open(
)?;
sql.set_raw_config_int(context, "dbversion", 60)?;
}
if dbversion < 61 {
info!(context, "[migration] v61");
sql.execute(
"ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;",
NO_PARAMS,
)?;
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 61)?;
}
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)
// --------------------------------------------------------------------
if recalc_fingerprints {
if 0 != recalc_fingerprints {
info!(context, "[migration] recalc fingerprints");
sql.query_map(
"SELECT addr FROM acpeerstates;",
@@ -893,7 +884,6 @@ fn open(
}
if update_icons {
update_saved_messages_icon(context)?;
update_device_icon(context)?;
}
}

View File

@@ -185,9 +185,6 @@ pub enum StockMessage {
Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
WelcomeMessage = 71,
#[strum(props(fallback = "Unknown Sender for this chat. See 'info' for more details."))]
UnknownSenderForChat = 72,
}
/*
@@ -547,8 +544,8 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 2);
chats.get_chat_id(0).delete(&t.ctx).ok();
chats.get_chat_id(1).delete(&t.ctx).ok();
chat::delete(&t.ctx, chats.get_chat_id(0)).ok();
chat::delete(&t.ctx, chats.get_chat_id(1)).ok();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 0);

View File

@@ -2,8 +2,7 @@
//!
//! This module is only compiled for test runs.
use std::sync::Arc;
use libc::uintptr_t;
use tempfile::{tempdir, TempDir};
use crate::config::Config;
@@ -17,7 +16,7 @@ use crate::key;
/// The temporary directory can be used to store the SQLite database,
/// see e.g. [test_context] which does this.
pub struct TestContext {
pub ctx: Arc<Context>,
pub ctx: Context,
pub dir: TempDir,
}
@@ -32,15 +31,9 @@ pub fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
let dbfile = dir.path().join("db.sqlite");
let cb: Box<ContextCallback> = match callback {
Some(cb) => cb,
None => Box::new(|_, _| ()),
None => Box::new(|_, _| 0),
};
let ctx = Arc::new(Context::new("FakeOs".into(), dbfile).unwrap());
let ctx_1 = ctx.clone();
std::thread::spawn(move || {
ctx_1.run(|context, event| (*cb)(context, event));
});
let ctx = Context::new(cb, "FakeOs".into(), dbfile).unwrap();
TestContext { ctx, dir }
}
@@ -53,13 +46,14 @@ pub fn dummy_context() -> TestContext {
test_context(None)
}
pub fn logging_cb(_ctx: &Context, evt: Event) {
pub fn logging_cb(_ctx: &Context, evt: Event) -> uintptr_t {
match evt {
Event::Info(msg) => println!("I: {}", msg),
Event::Warning(msg) => println!("W: {}", msg),
Event::Error(msg) => println!("E: {}", msg),
_ => (),
}
0
}
/// Creates Alice with a pre-generated keypair.

View File

@@ -6,7 +6,6 @@
use deltachat_derive::*;
use crate::chat::ChatId;
use crate::context::Context;
use crate::dc_tools::*;
use crate::sql;
@@ -28,28 +27,28 @@ impl Default for Namespace {
/// Creates a new token and saves it into the database.
/// Returns created token.
pub fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String {
pub fn save(context: &Context, namespace: Namespace, foreign_id: u32) -> String {
// foreign_id may be 0
let token = dc_create_id();
sql::execute(
context,
&context.sql,
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
params![namespace, foreign_id, &token, time()],
params![namespace, foreign_id as i32, &token, time()],
)
.ok();
token
}
pub fn lookup(context: &Context, namespace: Namespace, foreign_id: ChatId) -> Option<String> {
pub fn lookup(context: &Context, namespace: Namespace, foreign_id: u32) -> Option<String> {
context.sql.query_get_value::<_, String>(
context,
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
params![namespace, foreign_id],
params![namespace, foreign_id as i32],
)
}
pub fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String {
pub fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: u32) -> String {
lookup(context, namespace, foreign_id).unwrap_or_else(|| save(context, namespace, foreign_id))
}

40
src/top_evil_rs.py Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
from pathlib import Path
import os
import re
if __name__ == "__main__":
if Path('src/top_evil_rs.py').exists():
os.chdir('src')
filestats = []
for fn in Path(".").glob("**/*.rs"):
s = fn.read_text()
s = re.sub(r"(?m)///.*$", "", s) # remove comments
unsafe = s.count("unsafe")
free = s.count("free(")
unsafe_fn = s.count("unsafe fn")
chars = s.count("c_char") + s.count("CStr")
libc = s.count("libc")
filestats.append((fn, unsafe, free, unsafe_fn, chars, libc))
sum_unsafe, sum_free, sum_unsafe_fn, sum_chars, sum_libc = 0, 0, 0, 0, 0
for fn, unsafe, free, unsafe_fn, chars, libc in reversed(sorted(filestats, key=lambda x: sum(x[1:]))):
if unsafe + free + unsafe_fn + chars + libc == 0:
continue
print("{0: <25} unsafe: {1: >3} free: {2: >3} unsafe-fn: {3: >3} chars: {4: >3} libc: {5: >3}".format(str(fn), unsafe, free, unsafe_fn, chars, libc))
sum_unsafe += unsafe
sum_free += free
sum_unsafe_fn += unsafe_fn
sum_chars += chars
sum_libc += libc
print()
print("total unsafe:", sum_unsafe)
print("total free:", sum_free)
print("total unsafe-fn:", sum_unsafe_fn)
print("total c_chars:", sum_chars)
print("total libc:", sum_libc)

59
src/wrapmime.rs Normal file
View File

@@ -0,0 +1,59 @@
use mailparse::ParsedMail;
use crate::error::Error;
pub fn parse_message_id(message_id: &[u8]) -> Result<String, Error> {
let value = std::str::from_utf8(message_id)?;
let addrs = mailparse::addrparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
if let Some(info) = addrs.extract_single_info() {
return Ok(info.addr);
}
bail!("could not parse message_id: {}", value);
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
pub fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>, Error> {
ensure!(
mail.ctype.mimetype == "multipart/encrypted",
"Not a multipart/encrypted message: {}",
mail.ctype.mimetype
);
ensure!(
mail.subparts.len() == 2,
"Invalid Autocrypt Level 1 Mime Parts"
);
ensure!(
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
mail.subparts[0].ctype,
);
ensure!(
mail.subparts[1].ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
mail.subparts[1].ctype
);
Ok(&mail.subparts[1])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_message_id() {
assert_eq!(
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
assert_eq!(
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -9,6 +9,6 @@ message-id: <2dfdbde7@example.org>
Date: Sat, 14 Sep 2019 19:00:13 +0200
From: lmn <x@tux.org>
To: abc <abc@bcd.com>
CC: def <Def@def.de>
CC: def <def@def.de>
hi

View File

@@ -1,55 +0,0 @@
Chat-Group-ID: WVnDtF5azch
Chat-Group-Name: =?utf-8?q?testgr1?=
Chat-Group-Avatar: group-image.png
Chat-User-Avatar: avatar.png
Subject: =?utf-8?q?Chat=3A_testgr1=3A_hi!_?=
Date: Thu, 12 Dec 2019 17:24:03 +0000
X-Mailer: Delta Chat Core 1.0.0-beta.15/CLI
Chat-Version: 1.0
Message-ID: <Gr.WVnDtF5azch.c6vUZfnnXYx@testrun.org>
To: <bpetersen@b44t.com>
From: =?utf-8?q??= <tunis4@testrun.org>
Content-Type: multipart/mixed; boundary="LV8nfXkpyyn39fsVyoB1b29PKDMeb5"
--LV8nfXkpyyn39fsVyoB1b29PKDMeb5
Content-Type: text/plain; charset=utf-8
hi!
--
Sent with my Delta Chat Messenger: https://delta.chat
--LV8nfXkpyyn39fsVyoB1b29PKDMeb5
Content-Type: image/png
Content-Disposition: attachment; filename="group-image.png"
Content-Transfer-Encoding: base64
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT
1Iw0AYht+mSqVUHCwi4pChOlkQFXHUKhShQqgVWnUwufQPmjQkKS6OgmvBwZ/FqoOLs64OroIg+APi
4uqk6CIlfpcUWsR4x3EP733vy913gNCoMM3qGgc03TbTyYSYza2KoVdEEMYATUFmljEnSSn4jq97BP
h+F+dZ/nV/jl41bzEgIBLPMsO0iTeIpzdtg/M+cZSVZJX4nHjMpAsSP3Jd8fiNc9FlgWdGzUx6njhK
LBY7WOlgVjI14inimKrplC9kPVY5b3HWKjXWuid/YSSvryxzndYwkljEEiSIUFBDGRXYiNOuk2IhTe
cJH/+Q65fIpZCrDEaOBVShQXb94H/wu7dWYXLCS4okgO4Xx/kYAUK7QLPuON/HjtM8AYLPwJXe9lcb
wMwn6fW2FjsC+raBi+u2puwBlzvA4JMhm7IrBWkJhQLwfkbflAP6b4Hwmte31jlOH4AM9Sp1AxwcAq
NFyl73eXdPZ9/+rWn17wcR7HKATfSiTAAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+MMChYX
Fh+1IOwAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAGElEQVQoz2P858hAEm
BiYBjVMKphuGoAAAO8AV+n297RAAAAAElFTkSuQmCC
--LV8nfXkpyyn39fsVyoB1b29PKDMeb5
Content-Type: image/png
Content-Disposition: attachment; filename="avatar.png"
Content-Transfer-Encoding: base64
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT
1Iw0AcxV9TpUUqIhYRcchQnSyIFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi
4uqk6CIl/q8ptIjx4Lgf7+497t4BQr3MNKtrAtB020wl4mImuyoGXhFCEIOIoV9mljEnSUl4jq97+P
h6F+VZ3uf+HL1qzmKATySeZYZpE28QT2/aBud94jAryirxOfG4SRckfuS64vIb50KTBZ4ZNtOpeeIw
sVjoYKWDWdHUiKeII6qmU76QcVnlvMVZK1dZ6578haGcvrLMdZojSGARS5AgQkEVJZRhI0qrToqFFO
3HPfzDTb9ELoVcJTByLKACDXLTD/4Hv7u18rFJNykUB7pfHOdjFAjsAo2a43wfO07jBPA/A1d621+p
AzOfpNfaWuQI6NsGLq7bmrIHXO4AQ0+GbMpNyU9TyOeB9zP6piwwcAv0rLm9tfZx+gCkqavkDXBwCI
wVKHvd493Bzt7+PdPq7wd6nHKqMKZUTAAAAANQTFRF/sYAhYATyAAAAAlwSFlzAAAuIwAALiMBeKU/
dgAAAAd0SU1FB+MMCBY0D29+N8YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAA
AADElEQVQI12NgIA0AAAAwAAHHqoWOAAAAAElFTkSuQmCC
--LV8nfXkpyyn39fsVyoB1b29PKDMeb5--

View File

@@ -1,37 +0,0 @@
Chat-User-Avatar: avatar.png
Subject: =?utf-8?q?Chat=3A_this_is_a_message_with_a_=2E=2E=2E?=
Message-ID: Mr.wOBwZNbBTVt.NZpmQDwWoNk@example.org
In-Reply-To: Mr.ETXqza5-WpB.zDEYOLECxAw@example.org
Date: Sun, 08 Dec 2019 23:12:55 +0000
X-Mailer: Delta Chat Core 1.0.0-beta.12/CLI
Chat-Version: 1.0
To: <tunis3@example.org>
From: "=?utf-8?q??=" <tunis4@example.org>
Content-Type: multipart/mixed; boundary="luTiGu6GBoVLCvTkzVtmZmwsmhkNMw"
--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw
Content-Type: text/plain; charset=utf-8
this is a message with a profile-image attached
--
Sent with my Delta Chat Messenger: https://delta.chat
--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw
Content-Type: image/png
Content-Disposition: attachment; filename="avatar.png"
Content-Transfer-Encoding: base64
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT
1Iw0AcxV9TpUUqIhYRcchQnSyIFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi
4uqk6CIl/q8ptIjx4Lgf7+497t4BQr3MNKtrAtB020wl4mImuyoGXhFCEIOIoV9mljEnSUl4jq97+P
h6F+VZ3uf+HL1qzmKATySeZYZpE28QT2/aBud94jAryirxOfG4SRckfuS64vIb50KTBZ4ZNtOpeeIw
sVjoYKWDWdHUiKeII6qmU76QcVnlvMVZK1dZ6578haGcvrLMdZojSGARS5AgQkEVJZRhI0qrToqFFO
3HPfzDTb9ELoVcJTByLKACDXLTD/4Hv7u18rFJNykUB7pfHOdjFAjsAo2a43wfO07jBPA/A1d621+p
AzOfpNfaWuQI6NsGLq7bmrIHXO4AQ0+GbMpNyU9TyOeB9zP6piwwcAv0rLm9tfZx+gCkqavkDXBwCI
wVKHvd493Bzt7+PdPq7wd6nHKqMKZUTAAAAANQTFRF/sYAhYATyAAAAAlwSFlzAAAuIwAALiMBeKU/
dgAAAAd0SU1FB+MMCBY0D29+N8YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAA
AADElEQVQI12NgIA0AAAAwAAHHqoWOAAAAAElFTkSuQmCC
--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw--

View File

@@ -1,14 +0,0 @@
Content-Type: text/plain; charset=utf-8
Chat-User-Avatar: 0
Subject: =?utf-8?q?Chat=3A_profile_image_deleted?=
Message-ID: Mr.tsgoJgn-cBf.0TkFWKJzeSp@example.org
Date: Sun, 08 Dec 2019 23:28:30 +0000
X-Mailer: Delta Chat Core 1.0.0-beta.12/CLI
Chat-Version: 1.0
To: <tunis3@example>
From: "=?utf-8?q??=" <tunis4@example.org>
profile image deleted
--
Sent with my Delta Chat Messenger: https://delta.chat

View File

@@ -1,9 +1,10 @@
//! Stress some functions for testing; if used as a lib, this file is obsolete.
use std::collections::HashSet;
use std::sync::Arc;
use deltachat::chat::{self, Chat};
use deltachat::config;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::keyring::*;
use deltachat::pgp;
@@ -204,24 +205,20 @@ fn test_encryption_decryption() {
assert_eq!(plain, original_text);
}
fn cb(_context: &Context, _event: Event) {}
fn cb(_context: &Context, _event: Event) -> libc::uintptr_t {
0
}
#[allow(dead_code)]
struct TestContext {
ctx: Arc<Context>,
ctx: Context,
dir: TempDir,
}
fn create_test_context() -> TestContext {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let ctx = Arc::new(Context::new("FakeOs".into(), dbfile).unwrap());
let ctx_1 = ctx.clone();
std::thread::spawn(move || {
ctx_1.run(cb);
});
let ctx = Context::new(Box::new(cb), "FakeOs".into(), dbfile).unwrap();
TestContext { ctx, dir }
}
@@ -230,3 +227,20 @@ fn test_stress_tests() {
let context = create_test_context();
stress_functions(&context.ctx);
}
#[test]
fn test_chat() {
let context = create_test_context();
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap();
assert_ne!(contact1, 0);
let chat_id = chat::create_by_contact_id(&context.ctx, contact1).unwrap();
assert!(chat_id > 9, "chat_id too small {}", chat_id);
let chat = Chat::load_from_db(&context.ctx, chat_id).unwrap();
let chat2_id = chat::create_by_contact_id(&context.ctx, contact1).unwrap();
assert_eq!(chat2_id, chat_id);
let chat2 = Chat::load_from_db(&context.ctx, chat2_id).unwrap();
assert_eq!(chat2.name, chat.name);
}