Compare commits

..

20 Commits

Author SHA1 Message Date
holger krekel
4ef3a091fe parallelize tox-runs somewhat 2019-09-19 12:11:45 +02:00
holger krekel
977e3e08d7 refine python test run further 2019-09-19 11:37:26 +02:00
holger krekel
bb23e1487d try optimize python run a little 2019-09-19 11:24:21 +02:00
holger krekel
30783adef2 shuffle files such that ci_scripts/ contains all the ci scripts 2019-09-19 11:17:13 +02:00
holger krekel
a2c585c7a5 think these two env var settings are not needed 2019-09-19 10:51:09 +02:00
holger krekel
ff331061a0 remove unused script 2019-09-19 10:30:34 +02:00
holger krekel
77cb0276a6 try let upload of docs and python wheels outside docker and cache semantics 2019-09-19 09:46:47 +02:00
holger krekel
2747939b52 pyenv only exist in machine-runs -- let's see if upload passes without it or if there is a python path missing (the docker image contains several python versions) 2019-09-19 09:39:42 +02:00
dignifiedquire
dfb2ebb533 skip release and ffi runs, they are check using python bindings 2019-09-19 09:39:42 +02:00
dignifiedquire
4c579e6cf6 remove .git from cache dir, no need 2019-09-19 09:39:42 +02:00
dignifiedquire
cc1d520580 persist .git 2019-09-19 09:39:42 +02:00
dignifiedquire
c7686e0a97 fixup 2019-09-19 09:39:42 +02:00
dignifiedquire
d41bcccd41 add more details 2019-09-19 09:39:42 +02:00
dignifiedquire
3c1a4ebfe0 use correct script 2019-09-19 09:39:42 +02:00
dignifiedquire
361f14bffe ... 2019-09-19 09:39:42 +02:00
dignifiedquire
939ca7f7d3 .. 2019-09-19 09:39:42 +02:00
dignifiedquire
cdacf6a40f try fix config 2019-09-19 09:39:42 +02:00
dignifiedquire
feb4dfc3af try sth 2019-09-19 09:39:42 +02:00
dignifiedquire
8c13771d6c fix executor 2019-09-19 09:39:42 +02:00
dignifiedquire
5c3e1a6593 improve python caching 2019-09-19 09:39:42 +02:00
156 changed files with 18833 additions and 25669 deletions

View File

@@ -4,9 +4,6 @@ executors:
docker:
- image: filecoin/rust:latest
working_directory: /mnt/crate
doxygen:
docker:
- image: hrektts/doxygen
restore-workspace: &restore-workspace
attach_workspace:
@@ -15,7 +12,7 @@ restore-workspace: &restore-workspace
restore-cache: &restore-cache
restore_cache:
keys:
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- cargo-v2-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- repo-source-{{ .Branch }}-{{ .Revision }}
commands:
@@ -36,13 +33,20 @@ 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 }}
- cargo-v2-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- run: rustup install $(cat rust-toolchain)
- run: rustup default $(cat rust-toolchain)
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
- run: rustup component add --toolchain $(cat rust-toolchain) clippy-preview
- run: cargo update
- run: cargo fetch
- run: rustc +stable --version
- run: rustc +$(cat rust-toolchain) --version
@@ -53,7 +57,7 @@ jobs:
paths:
- crate
- save_cache:
key: cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
key: cargo-v2-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
paths:
- "~/.cargo"
- "~/.rustup"
@@ -84,6 +88,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
@@ -108,25 +113,26 @@ jobs:
target: "aarch64-linux-android"
build_doxygen:
executor: doxygen
build_test_docs_wheel:
docker:
- image: deltachat/coredeps
environment:
TESTS: 1
DOCS: 1
working_directory: /mnt/crate
steps:
- checkout
- run: bash ci_scripts/run-doxygen.sh
- run: mkdir -p workspace/c-docs
- run: cp -av deltachat-ffi/{html,xml} workspace/c-docs/
- persist_to_workspace:
root: workspace
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
- *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:
@@ -134,28 +140,15 @@ jobs:
- py-docs
- wheelhouse
remote_tests_rust:
machine: true
steps:
- checkout
- run: ci_scripts/remote_tests_rust.sh
remote_tests_python:
machine: true
steps:
- checkout
- run: ci_scripts/remote_tests_python.sh
upload_docs_wheels:
machine: true
steps:
- 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
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse
clippy:
executor: default
@@ -164,7 +157,7 @@ jobs:
- *restore-cache
- run:
name: Run cargo clippy
command: cargo clippy
command: cargo clippy --all
workflows:
@@ -172,49 +165,24 @@ workflows:
test:
jobs:
# - cargo_fetch
- remote_tests_rust:
filters:
tags:
only: /.*/
- remote_tests_python:
filters:
tags:
only: /.*/
- remote_python_packaging:
requires:
- remote_tests_python
- remote_tests_rust
filters:
tags:
only: /.*/
- cargo_fetch
- build_test_docs_wheel:
requires:
- cargo_fetch
- upload_docs_wheels:
requires:
- remote_python_packaging
- build_doxygen
filters:
tags:
only: /.*/
# - rustfmt:
# requires:
# - cargo_fetch
# - clippy:
# requires:
# - cargo_fetch
- build_doxygen:
filters:
tags:
only: /.*/
- build_test_docs_wheel
- rustfmt:
requires:
- cargo_fetch
- clippy:
requires:
- cargo_fetch
# Linux Desktop 64bit
# - test_x86_64-unknown-linux-gnu:
# requires:
# - cargo_fetch
- test_x86_64-unknown-linux-gnu:
requires:
- cargo_fetch
# Linux Desktop 32bit
# - test_i686-unknown-linux-gnu:

View File

@@ -1,48 +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
args: --workspace --examples --tests --all-features
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2019-11-06
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2019-11-06
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features

3
.gitignore vendored
View File

@@ -16,12 +16,9 @@ python/.tox
*.egg-info
__pycache__
python/src/deltachat/capi*.so
python/.venv/
python/liveconfig*
# ignore doxgen generated files
deltachat-ffi/html
deltachat-ffi/xml
.rsynclist

View File

@@ -1,396 +0,0 @@
# Changelog
## 1.25.0
- save traffic by downloading only messages that are really displayed #1236
- change generated key type to Ed25519, these keys are much shorter
than RSA keys, which results in saving traffic and speed improvements #1287
- improve key handling #1237 #1240 #1242 #1247
- mute handling, apis are dc_set_chat_mute_duration()
dc_chat_is_muted() and dc_chat_get_remaining_mute_duration() #1143
- pinning chats, new apis are dc_set_chat_visibility() and
dc_chat_get_visibility() #1248
- add dc_provider_new_from_email() api that queries the new, integrated
provider-database #1207
- account creation by scanning a qr code
in the DCACCOUNT scheme (https://mailadm.readthedocs.io),
new api is dc_set_config_from_qr() #1249
- if possible, dc_join_securejoin(), returns the new chat-id immediately
and does the handshake in background #1225
- update imap and smtp dependencies #1115
- check for MOVE capability before using MOVE command #1263
- allow inline attachments from RFC 2183 #1280
- fix updating names from incoming mails #1298
- fix error messages shown on import #1234
- directly attempt to re-connect if the smtp connection is maybe stale #1296
- improve adding group members #1291
- improve rust-api #1261
- cleanup #1302 #1283 #1282 #1276 #1270-#1274 #1267 #1258-#1260
#1257 #1239 #1231 #1224
- update spec #1286 #1291
## 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
## 1.0.0-beta.12
- fix python bindings to use core for copying attachments to blobdir
and fix core to actually do it. @hpk42
## 1.0.0-beta.11
- trigger reconnect more often on imap error states. Should fix an
issue observed when trying to empty a folder. @hpk42
- un-split qr tests: we fixed qr-securejoin protocol flakyness
last weeks. @hpk42
## 1.0.0-beta.10
- fix grpid-determination from in-reply-to and references headers. @hpk42
- only send Autocrypt-gossip headers on encrypted messages. @dignifiedquire
- fix reply-to-encrypted message to also be encrypted. @hpk42
- remove last unsafe code from dc_receive_imf :) @hpk42
- add experimental new dc_chat_get_info_json FFI/API so that desktop devs
can play with using it. @jikstra
- fix encoding of subjects and attachment-filenames @hpk42
@dignifiedquire .
## 1.0.0-beta.9
- historic: we now use the mailparse crate and lettre-email to generate mime
messages. This got rid of mmime completely, the C2rust generated port of the libetpan
mime-parse -- IOW 22KLocs of cumbersome code removed! see
https://github.com/deltachat/deltachat-core-rust/pull/904#issuecomment-561163330
many thanks @dignifiedquire for making everybody's life easier
and @jonhoo (from rust-imap fame) for suggesting to use the mailparse crate :)
- lots of improvements and better error handling in many rust modules
thanks @link2xt @flub @r10s, @hpk42 and @dignifiedquire
- @r10s introduced a new device chat which has an initial
welcome message. See
https://c.delta.chat/classdc__context__t.html#a1a2aad98bd23c1d21ee42374e241f389
for the main new FFI-API.
- fix moving self-sent messages, thanks @r10s, @flub, @hpk42
- fix flakyness/sometimes-failing verified/join-protocols,
thanks @flub, @r10s, @hpk42
- fix reply-to-encrypted message to keep encryption
- new DC_EVENT_SECUREJOIN_MEMBER_ADDED event
- many little fixes and rustifications (@link2xt, @flub, @hpk42)
## 1.0.0-beta.8
- now uses async-email/async-imap as the new base
which makes imap-idle interruptible and thus fixes
several issues around the imap thread being in zombie state .
thanks @dignifiedquire, @hpk42 and @link2xt.
- fixes imap-protocol parsing bugs that lead to infinitely
repeated crashing while trying to receive messages with
a subjec that contained non-utf8. thanks @link2xt
- fixed logic to find encryption subkey -- previously
delta chat would use the primary key for encryption
(which works with RSA but not ECC). thanks @link2xt
- introduce a new device chat where core and UIs can
add "device" messages. Android uses it for an initial
welcome message. thanks @r10s
- fix time smearing (when two message are virtually send
in the same second, there would be misbehaviour because
we didn't persist smeared time). thanks @r10s
- fix double-dotted extensions like .html.zip or .tar.gz
to not mangle them when creating blobfiles. thanks @flub
- fix backup/exports where the wrong sql file would be modified,
leading to problems when exporting twice. thanks @hpk42
- several other little fixes and improvements
## 1.0.0-beta.7
- fix location-streaming #782
- fix display of messages that could not be decrypted #785
- fix smtp MAILER-DAEMON bug #786
- fix a logging of durations #783
- add more error logging #779
- do not panic on some bad utf-8 mime #776
## 1.0.0-beta.6
- fix chatlist.get_msg_id to return id, instead of wrongly erroring
## 1.0.0-beta.5
- fix dc_get_msg() to return empty messages when asked for special ones
## 1.0.0-beta.4
- fix more than one sending of autocrypt setup message
- fix recognition of mailto-address-qr-codes, add tests
- tune down error to warning when adding self to chat
## 1.0.0-beta.3
- add back `dc_empty_server()` #682
- if `show_emails` is set to `DC_SHOW_EMAILS_ALL`,
email-based contact requests are added to the chatlist directly
- fix IMAP hangs #717 and cleanups
- several rPGP fixes
- code streamlining and rustifications
## 1.0.0-beta.2
- https://c.delta.chat docs are now regenerated again through our CI
- several rPGP cleanups, security fixes and better multi-platform support
- reconnect on io errors and broken pipes (imap)
- probe SMTP with real connection not just setup
- various imap/smtp related fixes
- use to_string_lossy in most places instead of relying on valid utf-8
encodings
- rework, rustify and test autoconfig-reading and parsing
- some rustifications/boolifications of c-ints
## 1.0.0-beta.1
- first beta of the Delta Chat Rust core library. many fixes of crashes
and other issues compared to 1.0.0-alpha.5.
- Most code is now "rustified" and does not do manual memory allocation anymore.
- The `DC_EVENT_GET_STRING` event is not used anymore, removing the last
event where the core requested a return value from the event callback.
Please now use `dc_set_stock_translation()` API for core messages
to be properly localized.
- Deltachat FFI docs are automatically generated and available here:
https://c.delta.chat
- New events ImapMessageMoved and ImapMessageDeleted
For a full list of changes, please see our closed Pull Requests:
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed

2604
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,27 @@
[package]
name = "deltachat"
version = "1.25.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
version = "1.0.0-alpha.4"
authors = ["dignifiedquire <dignifiedquire@gmail.com>"]
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 }
hex = "0.4.0"
pgp = { version = "0.2", default-features = false }
hex = "0.3.2"
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"
phf = { git = "https://github.com/sfackler/rust-phf", rev = "0d00821", features = ["macros"] }
smallvec = "0.6.9"
reqwest = "0.9.15"
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"] }
base64 = "0.11"
native-tls = "0.2.3"
lettre = "0.9.0"
imap = { git = "https://github.com/jonhoo/rust-imap", rev = "281d2eb8ab50dc656ceff2ae749ca5045f334e15" }
mmime = { git = "https://github.com/dignifiedquire/mmime", rev = "bccd2c2" }
base64 = "0.10"
charset = "0.1"
percent-encoding = "2.0"
serde = { version = "1.0", features = ["derive"] }
@@ -34,32 +29,25 @@ 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"
strum = "0.15.0"
strum_macros = "0.15.0"
thread-local-object = "0.1.0"
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"
jetscii = "0.4.4"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.10.2"
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 }
[dev-dependencies]
tempfile = "3.0"
@@ -80,11 +68,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 = ["native-tls/vendored", "reqwest/default-tls-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

@@ -1,9 +1,11 @@
# Delta Chat Rust
> Deltachat-core written in Rust
> Project porting deltachat-core to rust
[![CircleCI build status][circle-shield]][circle] [![Appveyor build status][appveyor-shield]][appveyor]
Current commit on deltachat/deltachat-core: `12ef73c8e76185f9b78e844ea673025f56a959ab`.
## Installing Rust and Cargo
To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment:
@@ -14,7 +16,7 @@ curl https://sh.rustup.rs -sSf | sh
## Using the CLI client
Compile and run Delta Chat Core command line utility, using `cargo`:
Compile and run Delta Chat Core using `cargo`:
```
cargo run --example repl -- /path/to/db
@@ -87,15 +89,6 @@ $ cargo test --all
$ cargo build -p deltachat_ffi --release
```
## Debugging environment variables
- `DCC_IMAP_DEBUG`: if set IMAP protocol commands and responses will be
printed
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
### Expensive tests
Some tests are expensive and marked with `#[ignore]`, to run these
@@ -114,22 +107,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**

6
Xargo.toml Normal file
View File

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

View File

@@ -8,11 +8,12 @@ install:
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -vV
- cargo -vV
- cargo update
build: false
test_script:
- cargo test --release --all
- cargo test --release
cache:
- target

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:export-ydpi="409.60001"
inkscape:export-xdpi="409.60001"
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-device.png"
version="1.0"
width="60"
height="60"
viewBox="0 0 45 45"
preserveAspectRatio="xMidYMid meet"
id="svg4344"
sodipodi:docname="icon-device.svg"
inkscape:version="1.0beta1 (32d4812, 2019-09-19)">
<defs
id="defs4348" />
<sodipodi:namedview
inkscape:snap-global="false"
pagecolor="#ffffff"
bordercolor="#666666"
inkscape:document-rotation="0"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1600"
inkscape:window-height="1035"
id="namedview4346"
showgrid="false"
units="px"
inkscape:zoom="3.959798"
inkscape:cx="28.322498"
inkscape:cy="24.898474"
inkscape:window-x="45"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="svg4344" />
<metadata
id="metadata4336">
Created by potrace 1.15, written by Peter Selinger 2001-2017
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
y="-4.4408921e-16"
x="0"
height="45"
width="45"
id="rect860"
style="opacity:1;fill:#76868b;fill-opacity:1;stroke-width:0.819271" />
<g
fill="#000000"
stroke="none"
style="fill:#ffffff;fill-opacity:1"
transform="matrix(0.00255113,0,0,-0.00255113,5.586152,38.200477)"
id="g4342">
<path
style="fill:#ffffff;fill-opacity:1"
d="m 8175,12765 c -703,-114 -1248,-608 -1387,-1258 -17,-82 -21,-136 -22,-277 0,-202 15,-307 70,-470 149,-446 499,-733 1009,-828 142,-26 465,-23 619,6 691,131 1201,609 1328,1244 31,158 31,417 0,565 -114,533 -482,889 -1038,1004 -133,27 -448,35 -579,14 z"
id="path4338"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffff;fill-opacity:1"
d="m 7070,9203 c -212,-20 -275,-27 -397,-48 -691,-117 -1400,-444 -2038,-940 -182,-142 -328,-270 -585,-517 -595,-571 -911,-974 -927,-1181 -6,-76 11,-120 69,-184 75,-80 159,-108 245,-79 109,37 263,181 632,595 539,606 774,826 1035,969 135,75 231,105 341,106 82,1 94,-2 138,-27 116,-68 161,-209 122,-376 -9,-36 -349,-868 -757,-1850 -407,-982 -785,-1892 -838,-2021 -287,-694 -513,-1389 -615,-1889 -70,-342 -90,-683 -52,-874 88,-440 381,-703 882,-792 124,-23 401,-30 562,-16 783,69 1674,461 2561,1125 796,596 1492,1354 1607,1751 43,146 -33,308 -168,360 -61,23 -100,15 -173,-36 -105,-74 -202,-170 -539,-529 -515,-551 -762,-783 -982,-927 -251,-164 -437,-186 -543,-65 -56,64 -74,131 -67,247 13,179 91,434 249,815 135,324 1588,4102 1646,4280 106,325 151,561 159,826 9,281 -22,463 -112,652 -58,122 -114,199 -211,292 -245,233 -582,343 -1044,338 -91,-1 -181,-3 -200,-5 z"
id="path4340"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,71 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:export-ydpi="409.60001"
inkscape:export-xdpi="409.60001"
inkscape:export-filename="/home/kerle/test-icon.png"
version="1.0"
width="60"
height="60"
viewBox="0 0 45 45"
preserveAspectRatio="xMidYMid meet"
id="svg4344"
sodipodi:docname="icon-saved-messages.svg"
inkscape:version="1.0beta1 (32d4812, 2019-09-19)">
<defs
id="defs4348" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
inkscape:document-rotation="0"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1395"
inkscape:window-height="855"
id="namedview4346"
showgrid="false"
units="px"
inkscape:zoom="4"
inkscape:cx="29.308676"
inkscape:cy="49.03624"
inkscape:window-x="89"
inkscape:window-y="108"
inkscape:window-maximized="0"
inkscape:current-layer="svg4344"
inkscape:lockguides="false" />
<metadata
id="metadata4336">
Created by potrace 1.15, written by Peter Selinger 2001-2017
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
y="0"
x="0"
height="45"
width="45"
id="rect1420"
style="fill:#87aade;fill-opacity:1;stroke:none;stroke-width:0.968078" />
<path
id="rect846"
style="fill:#ffffff;stroke-width:0.58409804"
d="M 13.5,7.5 V 39 h 0.08654 L 22.533801,29.370239 31.482419,39 h 0.01758 V 7.5 Z m 9.004056,4.108698 1.879508,4.876388 5.039514,0.359779 -3.879358,3.363728 1.227764,5.095749 -4.276893,-2.796643 -4.280949,2.788618 1.237229,-5.093073 -3.873949,-3.371754 5.040866,-0.350417 z"
inkscape:connector-curvature="0" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -1,46 +1,52 @@
# Continuous Integration Scripts for Delta Chat
Continuous Integration, run through CircleCI and an own build machine.
## Description of scripts
- `../.circleci/config.yml` describing the build jobs that are run
by Circle-CI
- `remote_tests_python.sh` rsyncs to a build machine and runs
`run-python-test.sh` remotely on the build machine.
- `remote_tests_rust.sh` rsyncs to the build machine and runs
`run-rust-test.sh` remotely on the build machine.
- `doxygen/Dockerfile` specifies an image that contains
the doxygen tool which is used by `run-doxygen.sh`
to generate C-docs which are then uploaded
via `ci_upload.sh` to `https://c.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
(and the master branch is linked to https://c.delta.chat proper).
Continuous Integration is run through CircleCI
but is largely independent of it.
## Triggering runs on the build machine locally (fast!)
## Generating docker containers for performing build step work
There is experimental support for triggering a remote Python or Rust test run
from your local checkout/branch. You will need to be authorized to login to
the build machine (ask your friendly sysadmin on #deltachat freenode) to type::
All tests, docs and wheel building is run in docker containers:
ci_scripts/manual_remote_tests.sh rust
ci_scripts/manual_remote_tests.sh python
- **coredeps/Dockerfile** specifies an image that contains all
of Delta Chat's core dependencies as linkable libraries.
It also serves to run python tests and build wheels
(binary packages for Python).
This will **rsync** your current checkout to the remote build machine
(no need to commit before) and then run either rust or python tests.
- **doxygen/Dockerfile** specifies an image that contains
the doxygen tool which is used to generate C-docs.
# Outdated files (for later re-use)
To run tests locally you can pull existing images from "docker.io",
the hub for sharing Docker images::
`coredeps/Dockerfile` specifies an image that contains all
of Delta Chat's core dependencies. It used to run
python tests and build wheels (binary packages for Python)
docker pull deltachat/coredeps
docker pull deltachat/doxygen
You can build the docker images yourself locally
or you can build the docker images yourself locally
to avoid the relatively large download::
cd ci_scripts # where all CI things are
docker build -t deltachat/coredeps docker-coredeps
docker build -t deltachat/doxygen docker-doxygen
## ci_run.sh (main entrypoint called by circle-ci)
Once you have the docker images available
you can run python testing, documentation generation
and building binary wheels::
sh DOCS=1 TESTS=1 ci_scripts/ci_run.sh
## ci_upload.sh (uploading artifacts on success)
- python docs to `https://py.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
- doxygen docs to `https://c.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
- python wheels to `https://m.devpi.net/dc/<BRANCH>`
so that you install fully self-contained wheels like this:
`pip install -U -i https://m.devpi.net/dc/<BRANCH> deltachat`

View File

@@ -7,9 +7,9 @@ fi
set -xe
#DOXYDOCDIR=${1:?directory where doxygen docs to be found}
PYDOCDIR=${1:?directory with python docs}
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
@@ -17,18 +17,15 @@ 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}
# 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}
#rsync -avz \
# -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
# "$DOXYDOCDIR/html/" \
# delta@py.delta.chat:build-c/${BRANCH}
echo -----------------------
echo upload wheels
@@ -37,7 +34,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 +45,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,17 +5,21 @@ 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
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
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
# Install python tools (auditwheels,tox, ...)
ADD deps/build_python.sh /builder/build_python.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
# Install Rust nightly
# Install Rust nightly
ADD deps/build_rust.sh /builder/build_rust.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
# Install a recent Perl, needed to install OpenSSL
ADD deps/build_perl.sh /builder/build_perl.sh
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

View File

@@ -1,11 +1,11 @@
#!/bin/bash
PERL_VERSION=5.30.0
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
PERL_VERSION=5.28.0
PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
tar -xzf perl-${PERL_VERSION}.tar.gz
cd perl-${PERL_VERSION}
echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
tar xzf perl-${PERL_VERSION}.tar.gz
cd perl-${PERL_VERSION}
./Configure -de
make

View File

@@ -1,11 +1,11 @@
#!/bin/bash
set -e -x
set -e -x
# Install Rust
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-11-06 -y
# Install Rust
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-07-10 -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/
rm -rf /root/.rustup/toolchains/nightly-2019-07-10-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,9 +0,0 @@
#!/bin/bash
set -xe
export CIRCLE_JOB=remote_tests_${1:?need to specify 'rust' or 'python'}
export CIRCLE_BUILD_NUM=$USER
export CIRCLE_BRANCH=`git branch | grep \* | cut -d ' ' -f2`
export CIRCLE_PROJECT_REPONAME=$(basename `git rev-parse --show-toplevel`)
time bash ci_scripts/$CIRCLE_JOB.sh

View File

@@ -1,77 +0,0 @@
name: CI
on:
pull_request:
push:
env:
RUSTFLAGS: -Dwarnings
jobs:
build_and_test:
name: Build and test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [nightly]
steps:
- uses: actions/checkout@master
- name: Install ${{ matrix.rust }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- name: check
uses: actions-rs/cargo@v1
if: matrix.rust == 'nightly'
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all
- name: tests ignored
uses: actions-rs/cargo@v1
with:
command: test
args: --all --release -- --ignored
check_fmt:
name: Checking fmt and docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
components: rustfmt
- name: fmt
run: cargo fmt --all -- --check
# clippy_check:
# name: Clippy check
# runs-on: ubuntu-latest
#
# steps:
# - uses: actions/checkout@v1
# - uses: actions-rs/toolchain@v1
# with:
# profile: minimal
# toolchain: nightly
# override: true
# components: clippy
#
# - name: clippy
# run: cargo clippy --all

View File

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

@@ -1,46 +0,0 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
set -xe
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
# we seem to need .git for setuptools_scm versioning
find .git >>.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
set +x
echo "--- Running $CIRCLE_JOB remotely"
ssh $SSHTARGET <<_HERE
set +x -e
cd $BUILDDIR
# let's share the target dir with our last run on this branch/job-type
# cargo will make sure to block/unblock us properly
export CARGO_TARGET_DIR=\`pwd\`/../target
export TARGET=release
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
#we rely on tox/virtualenv being available in the host
#rm -rf virtualenv venv
#virtualenv -q -p python3.7 venv
#source venv/bin/activate
#pip install -q tox virtualenv
set -x
which python
source \$HOME/venv/bin/activate
which python
bash ci_scripts/run-python-test.sh
_HERE

View File

@@ -1,32 +0,0 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
set -e
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
echo "--- Running $CIRCLE_JOB remotely"
ssh $SSHTARGET <<_HERE
set +x -e
cd $BUILDDIR
# let's share the target dir with our last run on this branch/job-type
# cargo will make sure to block/unblock us properly
export CARGO_TARGET_DIR=\`pwd\`/../target
export TARGET=x86_64-unknown-linux-gnu
export RUSTC_WRAPPER=sccache
bash ci_scripts/run-rust-test.sh
_HERE

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -ex
cd deltachat-ffi
PROJECT_NUMBER=$(git log -1 --format "%h (%cd)") doxygen

View File

@@ -1,24 +0,0 @@
#!/bin/bash
#
# Run functional tests for Delta Chat core using the python bindings
# and tox/pytest.
set -e -x
# for core-building and python install step
export DCC_RS_TARGET=debug
export DCC_RS_DEV=`pwd`
cd python
python install_python_bindings.py onlybuild
# remove and inhibit writing PYC files
rm -rf tests/__pycache__
rm -rf src/deltachat/__pycache__
export PYTHONDONTWRITEBYTECODE=1
# run python tests (tox invokes pytest to run tests in python/tests)
#TOX_PARALLEL_NO_SPINNER=1 tox -e lint,doc
tox -e lint
tox -e doc,py37

View File

@@ -39,12 +39,8 @@ if [ -n "$TESTS" ]; then
# run tox. The circle-ci project env-var-setting DCC_PY_LIVECONFIG
# allows running of "liveconfig" tests but for speed reasons
# we run them only for the highest python version we support
# we split out qr-tests run to minimize likelyness of flaky tests
# (some qr tests are pretty heavy in terms of send/received
# messages and rust's imap code likely has concurrency problems)
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
tox --workdir "$TOXWORKDIR" -e py37
unset DCC_PY_LIVECONFIG
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
tox --workdir "$TOXWORKDIR" -e auditwheels

View File

@@ -2,7 +2,7 @@
set -ex
#export RUST_TEST_THREADS=1
export RUST_TEST_THREADS=1
export RUST_BACKTRACE=1
export RUSTFLAGS='--deny warnings'
export OPT="--target=$TARGET"
@@ -31,15 +31,12 @@ fi
if [[ $NORUN == "1" ]]; then
export CARGO_SUBCMD="build"
else
export CARGO_SUBCMD="test --all"
export CARGO_SUBCMD="test"
export OPT="${OPT} "
export OPT_RELEASE="${OPT_RELEASE} "
export OPT_RELEASE_IGNORED="${OPT_RELEASE} -- --ignored"
fi
# Run all the test configurations
# RUSTC_WRAPPER=SCCACHE seems to destroy parallelism / prolong the test
unset RUSTC_WRAPPER
# Run all the test configurations:
$CARGO_CMD $CARGO_SUBCMD $OPT
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE_IGNORED

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.25.0"
version = "1.0.0-alpha.4"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
authors = ["dignifiedquire <dignifiedquire@gmail.com>"]
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"]
@@ -16,11 +16,10 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-provider-overview = { git = "https://github.com/deltachat/provider-overview", rev = "366b41a7503973e4ffac3aa5173b419f2f03c211" }
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"]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
extern crate deltachat_provider_overview;
use std::ptr;
use deltachat::dc_tools::{as_str, StrExt};
use deltachat_provider_overview::StatusState;
#[no_mangle]
pub type dc_provider_t = deltachat_provider_overview::Provider;
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_domain(
domain: *const libc::c_char,
) -> *const dc_provider_t {
match deltachat_provider_overview::get_provider_info(as_str(domain)) {
Some(provider) => provider,
None => ptr::null(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email(
email: *const libc::c_char,
) -> *const dc_provider_t {
let domain = deltachat_provider_overview::get_domain_from_email(as_str(email));
match deltachat_provider_overview::get_provider_info(domain) {
Some(provider) => provider,
None => ptr::null(),
}
}
macro_rules! null_guard {
($context:tt) => {
if $context.is_null() {
return ptr::null_mut() as *mut libc::c_char;
}
};
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_overview_page(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
format!(
"{}/{}",
deltachat_provider_overview::PROVIDER_OVERVIEW_URL,
(*provider).overview_page
)
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_name(provider: *const dc_provider_t) -> *mut libc::c_char {
null_guard!(provider);
(*provider).name.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_markdown(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
(*provider).markdown.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status_date(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
(*provider).status.date.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> u32 {
if provider.is_null() {
return 0;
}
match (*provider).status.state {
StatusState::OK => 1,
StatusState::PREPARATION => 2,
StatusState::BROKEN => 3,
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {
()
}
// TODO expose general provider overview url?

View File

@@ -1,395 +0,0 @@
use failure::Fail;
use std::ffi::{CStr, CString};
use std::ptr;
/// Duplicates a string
///
/// returns an empty string if NULL is given, never returns NULL (exits on errors)
///
/// # Examples
///
/// ```rust,norun
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
/// unsafe {
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
/// let str_a_copy = dc_strdup(str_a);
/// assert_eq!(to_string_lossy(str_a_copy), "foobar");
/// assert_ne!(str_a, str_a_copy);
/// }
/// ```
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
let ret: *mut libc::c_char;
if !s.is_null() {
ret = libc::strdup(s);
assert!(!ret.is_null());
} else {
ret = libc::calloc(1, 1) as *mut libc::c_char;
assert!(!ret.is_null());
}
ret
}
/// Error type for the [OsStrExt] trait
#[derive(Debug, Fail, PartialEq)]
pub enum CStringError {
/// The string contains an interior null byte
#[fail(display = "String contains an interior null byte")]
InteriorNullByte,
/// The string is not valid Unicode
#[fail(display = "String is not valid unicode")]
NotUnicode,
}
/// Extra convenience methods on [std::ffi::OsStr] to work with `*libc::c_char`.
///
/// The primary function of this trait is to more easily convert
/// [OsStr], [OsString] or [Path] into pointers to C strings. This always
/// allocates a new string since it is very common for the source
/// string not to have the required terminal null byte.
///
/// It is implemented for `AsRef<std::ffi::OsStr>>` trait, which
/// allows any type which implements this trait to transparently use
/// this. This is how the conversion for [Path] works.
///
/// [OsStr]: std::ffi::OsStr
/// [OsString]: std::ffi::OsString
/// [Path]: std::path::Path
///
/// # Example
///
/// ```
/// use deltachat::dc_tools::{dc_strdup, OsStrExt};
/// let path = std::path::Path::new("/some/path");
/// let path_c = path.to_c_string().unwrap();
/// unsafe {
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
/// }
/// ```
pub trait OsStrExt {
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
///
/// This is useful to convert e.g. a [std::path::Path] to
/// [*libc::c_char] by using
/// [Path::as_os_str()](std::path::Path::as_os_str) and
/// [CStr::as_ptr()](std::ffi::CStr::as_ptr).
///
/// This returns [CString] and not [&CStr] because not all [OsStr]
/// slices end with a null byte, particularly those coming from
/// [Path] do not have a null byte and having to handle this as
/// the caller would defeat the point of this function.
///
/// On Windows this requires that the [OsStr] contains valid
/// unicode, which should normally be the case for a [Path].
///
/// [CString]: std::ffi::CString
/// [CStr]: std::ffi::CStr
/// [OsStr]: std::ffi::OsStr
/// [Path]: std::path::Path
///
/// # Errors
///
/// Since a C `*char` is terminated by a NULL byte this conversion
/// will fail, when the [OsStr] has an interior null byte. The
/// function will return
/// `[Err]([CStringError::InteriorNullByte])`. When converting
/// from a [Path] it should be safe to
/// [`.unwrap()`](std::result::Result::unwrap) this anyway since a
/// [Path] should not contain interior null bytes.
///
/// On windows when the string contains invalid Unicode
/// `[Err]([CStringError::NotUnicode])` is returned.
fn to_c_string(&self) -> Result<CString, CStringError>;
}
impl<T: AsRef<std::ffi::OsStr>> OsStrExt for T {
#[cfg(not(target_os = "windows"))]
fn to_c_string(&self) -> Result<CString, CStringError> {
use std::os::unix::ffi::OsStrExt;
CString::new(self.as_ref().as_bytes()).map_err(|err| match err {
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
})
}
#[cfg(target_os = "windows")]
fn to_c_string(&self) -> Result<CString, CStringError> {
os_str_to_c_string_unicode(&self)
}
}
// Implementation for os_str_to_c_string on windows.
#[allow(dead_code)]
fn os_str_to_c_string_unicode(
os_str: &dyn AsRef<std::ffi::OsStr>,
) -> Result<CString, CStringError> {
match os_str.as_ref().to_str() {
Some(val) => CString::new(val.as_bytes()).map_err(|err| match err {
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
}),
None => Err(CStringError::NotUnicode),
}
}
/// Convenience methods/associated functions for working with [CString]
///
/// This is helps transitioning from unsafe code.
pub trait CStringExt {
/// Create a new [CString], yolo style
///
/// This unwrap the result, panicking when there are embedded NULL
/// bytes.
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
CString::new(t).expect("String contains null byte, can not be CString")
}
}
impl CStringExt for CString {}
/// Convenience methods to turn strings into C strings.
///
/// To interact with (legacy) C APIs we often need to convert from
/// Rust strings to raw C strings. This can be clumsy to do correctly
/// and the compiler sometimes allows it in an unsafe way. These
/// methods make it more succinct and help you get it right.
pub trait StrExt {
/// Allocate a new raw C `*char` version of this string.
///
/// This allocates a new raw C string which must be freed using
/// `free`. It takes care of some common pitfalls with using
/// [CString.as_ptr].
///
/// [CString.as_ptr]: std::ffi::CString.as_ptr
///
/// # Panics
///
/// This function will panic when the original string contains an
/// interior null byte as this can not be represented in raw C
/// strings.
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> StrExt for T {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::yolo(self.as_ref());
dc_strdup(tmp.as_ptr())
}
}
/// Convenience methods to turn optional strings into C strings.
///
/// This is the same as the [StrExt] trait but a different trait name
/// to work around the type system not allowing to implement [StrExt]
/// for `Option<impl StrExt>` When we already have an [StrExt] impl
/// for `AsRef<&str>`.
///
/// When the [Option] is [Option::Some] this behaves just like
/// [StrExt::strdup], when it is [Option::None] a null pointer is
/// returned.
pub trait OptStrExt {
/// Allocate a new raw C `*char` version of this string, or NULL.
///
/// See [StrExt::strdup] for details.
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> OptStrExt for Option<T> {
unsafe fn strdup(&self) -> *mut libc::c_char {
match self {
Some(s) => {
let tmp = CString::yolo(s.as_ref());
dc_strdup(tmp.as_ptr())
}
None => ptr::null_mut(),
}
}
}
pub fn to_string_lossy(s: *const libc::c_char) -> String {
if s.is_null() {
return "".into();
}
let cstr = unsafe { CStr::from_ptr(s) };
cstr.to_string_lossy().to_string()
}
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
if s.is_null() {
return None;
}
Some(to_string_lossy(s))
}
/// Convert a C `*char` pointer to a [std::path::Path] slice.
///
/// This converts a `*libc::c_char` pointer to a [Path] slice. This
/// essentially has to convert the pointer to [std::ffi::OsStr] to do
/// so and thus is the inverse of [OsStrExt::to_c_string]. Just like
/// [OsStrExt::to_c_string] requires valid Unicode on Windows, this
/// requires that the pointer contains valid UTF-8 on Windows.
///
/// Because this returns a reference the [Path] silce can not outlive
/// the original pointer.
///
/// [Path]: std::path::Path
#[cfg(not(target_os = "windows"))]
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
assert!(!s.is_null(), "cannot be used on null pointers");
use std::os::unix::ffi::OsStrExt;
unsafe {
let c_str = std::ffi::CStr::from_ptr(s).to_bytes();
let os_str = std::ffi::OsStr::from_bytes(c_str);
std::path::Path::new(os_str)
}
}
// as_path() implementation for windows, documented above.
#[cfg(target_os = "windows")]
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
as_path_unicode(s)
}
// Implementation for as_path() on Windows.
//
// Having this as a separate function means it can be tested on unix
// too.
#[allow(dead_code)]
fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
assert!(!s.is_null(), "cannot be used on null pointers");
let cstr = unsafe { CStr::from_ptr(s) };
let str = cstr.to_str().unwrap_or_else(|err| panic!("{}", err));
std::path::Path::new(str)
}
#[cfg(test)]
mod tests {
use super::*;
use libc::{free, strcmp};
#[test]
fn test_os_str_to_c_string_cwd() {
let some_dir = std::env::current_dir().unwrap();
some_dir.as_os_str().to_c_string().unwrap();
}
#[test]
fn test_os_str_to_c_string_unicode() {
let some_str = String::from("/some/valid/utf8");
let some_dir = std::path::Path::new(&some_str);
assert_eq!(
some_dir.as_os_str().to_c_string().unwrap(),
CString::new("/some/valid/utf8").unwrap()
);
}
#[test]
fn test_os_str_to_c_string_nul() {
let some_str = std::ffi::OsString::from("foo\x00bar");
assert_eq!(
some_str.to_c_string().err().unwrap(),
CStringError::InteriorNullByte
)
}
#[test]
fn test_path_to_c_string_cwd() {
let some_dir = std::env::current_dir().unwrap();
some_dir.to_c_string().unwrap();
}
#[test]
fn test_path_to_c_string_unicode() {
let some_str = String::from("/some/valid/utf8");
let some_dir = std::path::Path::new(&some_str);
assert_eq!(
some_dir.as_os_str().to_c_string().unwrap(),
CString::new("/some/valid/utf8").unwrap()
);
}
#[test]
fn test_os_str_to_c_string_unicode_fn() {
let some_str = std::ffi::OsString::from("foo");
assert_eq!(
os_str_to_c_string_unicode(&some_str).unwrap(),
CString::new("foo").unwrap()
);
}
#[test]
fn test_path_to_c_string_unicode_fn() {
let some_str = String::from("/some/path");
let some_path = std::path::Path::new(&some_str);
assert_eq!(
os_str_to_c_string_unicode(&some_path).unwrap(),
CString::new("/some/path").unwrap()
);
}
#[test]
fn test_os_str_to_c_string_unicode_fn_nul() {
let some_str = std::ffi::OsString::from("fooz\x00bar");
assert_eq!(
os_str_to_c_string_unicode(&some_str).err().unwrap(),
CStringError::InteriorNullByte
);
}
#[test]
fn test_as_path() {
let some_path = CString::new("/some/path").unwrap();
let ptr = some_path.as_ptr();
assert_eq!(as_path(ptr), std::ffi::OsString::from("/some/path"))
}
#[test]
fn test_as_path_unicode_fn() {
let some_path = CString::new("/some/path").unwrap();
let ptr = some_path.as_ptr();
assert_eq!(as_path_unicode(ptr), std::ffi::OsString::from("/some/path"));
}
#[test]
fn test_cstring_yolo() {
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
}
#[test]
fn test_strdup_str() {
unsafe {
let s = "hello".strdup();
let cmp = strcmp(s, b"hello\x00" as *const u8 as *const libc::c_char);
free(s as *mut libc::c_void);
assert_eq!(cmp, 0);
}
}
#[test]
fn test_strdup_string() {
unsafe {
let s = String::from("hello").strdup();
let cmp = strcmp(s, b"hello\x00" as *const u8 as *const libc::c_char);
free(s as *mut libc::c_void);
assert_eq!(cmp, 0);
}
}
#[test]
fn test_strdup_opt_string() {
unsafe {
let s = Some("hello");
let c = s.strdup();
let cmp = strcmp(c, b"hello\x00" as *const u8 as *const libc::c_char);
free(c as *mut libc::c_void);
assert_eq!(cmp, 0);
let s: Option<&str> = None;
let c = s.strdup();
assert_eq!(c, ptr::null_mut());
}
}
}

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,33 +1,36 @@
use std::path::Path;
use std::ffi::CString;
use std::ptr;
use std::str::FromStr;
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chat::{self, Chat};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::configure::*;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::dc_imex::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::error::Error;
use deltachat::imex::*;
use deltachat::job::*;
use deltachat::location;
use deltachat::lot::LotState;
use deltachat::message::{self, Message, MessageState, MsgId};
use deltachat::message::*;
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::x::*;
use deltachat::Event;
use deltachat::{config, provider};
/// 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 unsafe 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 +40,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,172 +83,215 @@ 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),
msg_id: MsgId::new(0),
chat_id: 0,
msg_id: 0,
});
1
}
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, false) {
println!("dc_receive_imf errored: {:?}", err);
unsafe fn dc_poke_eml_file(context: &Context, filename: *const libc::c_char) -> libc::c_int {
/* mainly for testing, may be called by dc_import_spec() */
let mut success: libc::c_int = 0i32;
let mut data: *mut libc::c_char = ptr::null_mut();
let mut data_bytes = 0;
if !(dc_read_file(
context,
filename,
&mut data as *mut *mut libc::c_char as *mut *mut libc::c_void,
&mut data_bytes,
) == 0i32)
{
dc_receive_imf(context, data, data_bytes, "import", 0, 0);
success = 1;
}
Ok(())
free(data as *mut libc::c_void);
success
}
/// Import a file to the database.
/// For testing, import a folder with eml-files, a single eml-file, e-mail plus public key and so on.
/// For normal importing, use imex().
/// For normal importing, use dc_imex().
///
/// @private @memberof Context
/// @param context The context as created by dc_context_new().
/// @param spec The file or directory to import. NULL for the last command.
/// @return 1=success, 0=error.
fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
unsafe fn poke_spec(context: &Context, spec: *const libc::c_char) -> libc::c_int {
if !context.sql.is_open() {
error!(context, "Import: Database not opened.");
return 0;
}
let mut read_cnt = 0;
let real_spec: String;
let ok_to_continue;
let mut success: libc::c_int = 0;
let real_spec: *mut libc::c_char;
let mut suffix: *mut libc::c_char = ptr::null_mut();
let mut read_cnt: libc::c_int = 0;
/* if `spec` is given, remember it for later usage; if it is not given, try to use the last one */
if let Some(spec) = spec {
real_spec = spec.to_string();
if !spec.is_null() {
real_spec = dc_strdup(spec);
context
.sql
.set_raw_config(context, "import_spec", Some(&real_spec))
.set_config(context, "import_spec", Some(as_str(real_spec)))
.unwrap();
ok_to_continue = true;
} else {
let rs = context.sql.get_raw_config(context, "import_spec");
let rs = context.sql.get_config(context, "import_spec");
if rs.is_none() {
error!(context, "Import: No file or folder given.");
return 0;
}
real_spec = rs.unwrap();
}
if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) {
if suffix == "eml" && dc_poke_eml_file(context, &real_spec).is_ok() {
read_cnt += 1
}
} else {
/* import a directory */
let dir_name = std::path::Path::new(&real_spec);
let dir = std::fs::read_dir(dir_name);
if dir.is_err() {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
return 0;
ok_to_continue = false;
} else {
let dir = dir.unwrap();
for entry in dir {
if entry.is_err() {
break;
}
let entry = entry.unwrap();
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", &real_spec, name);
println!("Import: {}", path_plus_name);
if dc_poke_eml_file(context, path_plus_name).is_ok() {
read_cnt += 1
ok_to_continue = true;
}
real_spec = rs.unwrap_or_default().strdup();
}
if ok_to_continue {
let ok_to_continue2;
suffix = dc_get_filesuffix_lc(as_str(real_spec));
if !suffix.is_null() && strcmp(suffix, b"eml\x00" as *const u8 as *const libc::c_char) == 0
{
if 0 != dc_poke_eml_file(context, real_spec) {
read_cnt += 1
}
ok_to_continue2 = true;
} else {
/* import a directory */
let dir_name = std::path::Path::new(as_str(real_spec));
let dir = std::fs::read_dir(dir_name);
if dir.is_err() {
error!(
context,
"Import: Cannot open directory \"{}\".",
as_str(real_spec),
);
ok_to_continue2 = false;
} else {
let dir = dir.unwrap();
for entry in dir {
if entry.is_err() {
break;
}
let entry = entry.unwrap();
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", as_str(real_spec), name);
info!(context, "Import: {}", path_plus_name);
let path_plus_name_c = CString::yolo(path_plus_name);
if 0 != dc_poke_eml_file(context, path_plus_name_c.as_ptr()) {
read_cnt += 1
}
}
}
ok_to_continue2 = true;
}
}
if ok_to_continue2 {
info!(
context,
"Import: {} items read from \"{}\".",
read_cnt,
as_str(real_spec)
);
if read_cnt > 0 {
context.call_cb(Event::MsgsChanged {
chat_id: 0,
msg_id: 0,
});
}
success = 1
}
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
if read_cnt > 0 {
context.call_cb(Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
1
free(real_spec as *mut libc::c_void);
free(suffix as *mut libc::c_void);
success
}
fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let contact = Contact::get_by_id(context, msg.get_from_id()).expect("invalid contact");
unsafe fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let contact = Contact::get_by_id(context, dc_msg_get_from_id(msg)).expect("invalid contact");
let contact_name = contact.get_name();
let contact_id = contact.get_id();
let statestr = match msg.get_state() {
let statestr = match dc_msg_get_state(msg) {
MessageState::OutPending => " o",
MessageState::OutDelivered => "",
MessageState::OutMdnRcvd => " √√",
MessageState::OutFailed => " !!",
_ => "",
};
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text();
println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]",
let temp2 = dc_timestamp_to_str(dc_msg_get_timestamp(msg));
let msgtext = dc_msg_get_text(msg);
info!(
context,
"{}#{}{}{}: {} (Contact#{}): {} {}{}{}{} [{}]",
prefix.as_ref(),
msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" },
if msg.has_location() { "📍" } else { "" },
dc_msg_get_id(msg) as libc::c_int,
if dc_msg_get_showpadlock(msg) {
"🔒"
} else {
""
},
if dc_msg_has_location(msg) { "📍" } else { "" },
&contact_name,
contact_id,
msgtext.unwrap_or_default(),
if msg.is_starred() { "" } else { "" },
if msg.get_from_id() == 1 as libc::c_uint {
as_str(msgtext),
if dc_msg_is_starred(msg) { "" } else { "" },
if dc_msg_get_from_id(msg) == 1 as libc::c_uint {
""
} else if msg.get_state() == MessageState::InSeen {
} else if dc_msg_get_state(msg) == MessageState::InSeen {
"[SEEN]"
} else if msg.get_state() == MessageState::InNoticed {
} else if dc_msg_get_state(msg) == MessageState::InNoticed {
"[NOTICED]"
} else {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.is_forwarded() {
"[FORWARDED]"
} else {
""
},
if dc_msg_is_info(msg) { "[INFO]" } else { "" },
statestr,
&temp2,
);
free(msgtext as *mut libc::c_void);
}
fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
unsafe fn log_msglist(context: &Context, msglist: &Vec<u32>) -> Result<(), Error> {
let mut lines_out = 0;
for &msg_id in msglist {
if msg_id.is_daymarker() {
println!(
if msg_id == 9 as libc::c_uint {
info!(
context,
"--------------------------------------------------------------------------------"
);
lines_out += 1
} else if !msg_id.is_special() {
} else if msg_id > 0 {
if lines_out == 0 {
println!(
info!(
context,
"--------------------------------------------------------------------------------",
);
lines_out += 1
}
let msg = Message::load_from_db(context, msg_id)?;
log_msg(context, "", &msg);
let msg = dc_get_msg(context, msg_id)?;
log_msg(context, "Msg", &msg);
}
}
if lines_out > 0 {
println!(
info!(
context,
"--------------------------------------------------------------------------------"
);
}
Ok(())
}
fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
unsafe fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
let mut contacts = contacts.clone();
if !contacts.contains(&1) {
contacts.push(1);
@@ -288,7 +334,7 @@ fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
);
}
println!("Contact#{}: {}{}", contact_id, line, line2);
info!(context, "Contact#{}: {}{}", contact_id, line, line2);
}
}
}
@@ -297,9 +343,9 @@ fn chat_prefix(chat: &Chat) -> &'static str {
chat.typ.into()
}
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let chat_id = *context.cmdline_sel_chat_id.read().unwrap();
let 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
@@ -308,9 +354,18 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let mut args = line.splitn(3, ' ');
let arg0 = args.next().unwrap_or_default();
let arg1 = args.next().unwrap_or_default();
let arg1_c = if arg1.is_empty() {
std::ptr::null()
} else {
arg1.strdup() as *const _
};
let arg2 = args.next().unwrap_or_default();
let arg2_c = if arg2.is_empty() {
std::ptr::null()
} else {
arg2.strdup() as *const _
};
let blobdir = context.get_blobdir();
match arg0 {
"help" | "?" => match arg1 {
// TODO: reuse commands definition in main.rs.
@@ -341,7 +396,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
configure\n\
connect\n\
disconnect\n\
interrupt\n\
maybenetwork\n\
housekeeping\n\
help imex (Import/Export)\n\
@@ -367,12 +421,9 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
sendimage <file> [<text>]\n\
sendfile <file> [<text>]\n\
draft [<text>]\n\
devicemsg <text>\n\
listmedia\n\
archive <chat-id>\n\
unarchive <chat-id>\n\
pin <chat-id>\n\
unpin <chat-id>\n\
delchat <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
@@ -394,35 +445,39 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
getqr [<chat-id>]\n\
getbadqr\n\
checkqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
clear -- clear screen\n\
exit or quit\n\
============================================="
),
},
"initiate-key-transfer" => match initiate_key_transfer(context) {
Ok(setup_code) => println!(
"Setup code for the transferred setup message: {}",
setup_code,
),
Err(err) => bail!("Failed to generate setup code: {}", err),
},
"initiate-key-transfer" => {
let setup_code = dc_initiate_key_transfer(context);
if !setup_code.is_null() {
println!(
"Setup code for the transferred setup message: {}",
as_str(setup_code),
);
free(setup_code as *mut libc::c_void);
} else {
bail!("Failed to generate setup code");
};
}
"get-setupcodebegin" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let msg_id: MsgId = MsgId::new(arg1.parse()?);
let msg = Message::load_from_db(context, msg_id)?;
if msg.is_setupmessage() {
let setupcodebegin = msg.get_setupcodebegin(context);
let msg_id: u32 = arg1.parse()?;
let msg = dc_get_msg(context, msg_id)?;
if dc_msg_is_setupmessage(&msg) {
let setupcodebegin = dc_msg_get_setupcodebegin(context, &msg);
println!(
"The setup code for setup message {} starts with: {}",
"The setup code for setup message Msg#{} starts with: {}",
msg_id,
setupcodebegin.unwrap_or_default(),
as_str(setupcodebegin),
);
free(setupcodebegin as *mut libc::c_void);
} else {
bail!("{} is no setup message.", msg_id,);
bail!("Msg#{} is no setup message.", msg_id,);
}
}
"continue-key-transfer" => {
@@ -430,28 +485,33 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
!arg1.is_empty() && !arg2.is_empty(),
"Arguments <msg-id> <setup-code> expected"
);
continue_key_transfer(context, MsgId::new(arg1.parse()?), &arg2)?;
if !dc_continue_key_transfer(context, arg1.parse()?, arg2_c) {
bail!("Continue key transfer failed");
}
}
"has-backup" => {
has_backup(context, blobdir)?;
let ret = dc_imex_has_backup(context, context.get_blobdir());
if ret.is_null() {
println!("No backup found.");
}
}
"export-backup" => {
imex(context, ImexMode::ExportBackup, Some(blobdir));
dc_imex(context, 11, Some(context.get_blobdir()), ptr::null());
}
"import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(context, ImexMode::ImportBackup, Some(arg1));
dc_imex(context, 12, Some(arg1), ptr::null());
}
"export-keys" => {
imex(context, ImexMode::ExportSelfKeys, Some(blobdir));
dc_imex(context, 1, Some(context.get_blobdir()), ptr::null());
}
"import-keys" => {
imex(context, ImexMode::ImportSelfKeys, Some(blobdir));
dc_imex(context, 2, Some(context.get_blobdir()), ptr::null());
}
"export-setup" => {
let setup_code = create_setup_code(context);
let file_name = blobdir.join("autocrypt-setup-message.html");
let file_content = render_setup_file(context, &setup_code)?;
let setup_code = dc_create_setup_code(context);
let file_name = context.get_blobdir().join("autocrypt-setup-message.html");
let file_content = dc_render_setup_file(context, &setup_code)?;
std::fs::write(&file_name, file_content)?;
println!(
"Setup message written to: {}\nSetup code: {}",
@@ -460,7 +520,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
);
}
"poke" => {
ensure!(0 != poke_spec(context, Some(arg1)), "Poke failed");
ensure!(0 != poke_spec(context, arg1_c), "Poke failed");
}
"reset" => {
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
@@ -469,7 +529,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(0 != dc_reset_tables(context, bits), "Reset failed");
}
"stop" => {
context.stop_ongoing();
dc_stop_ongoing_process(context);
}
"set" => {
ensure!(!arg1.is_empty(), "Argument <key> missing.");
@@ -486,9 +546,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
"info" => {
println!("{:#?}", context.get_info());
}
"interrupt" => {
interrupt_inbox_idle(context);
}
"maybenetwork" => {
maybe_network(context);
}
@@ -506,26 +563,26 @@ 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!(
"{}#{}: {} [{} fresh] {}",
let temp_subtitle = chat.get_subtitle(context);
let temp_name = chat.get_name();
info!(
context,
"{}#{}: {} [{}] [{} fresh]",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
chat.get_id().get_fresh_msg_cnt(context),
match chat.visibility {
ChatVisibility::Normal => "",
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
temp_name,
temp_subtitle,
chat::get_fresh_msg_cnt(context, chat.get_id()),
);
let lot = chatlist.get_summary(context, i, Some(&chat));
let statestr = if chat.visibility == ChatVisibility::Archived {
let statestr = if chat.is_archived() {
" [Archived]"
} else {
match lot.get_state() {
@@ -539,7 +596,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 { "" },
@@ -552,13 +610,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);
}
@@ -567,8 +626,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;
}
@@ -576,41 +635,31 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(sel_chat.is_some(), "Failed to select chat");
let sel_chat = sel_chat.as_ref().unwrap();
let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, None);
let members = chat::get_chat_contacts(context, sel_chat.id);
let subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string()
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
let contact = Contact::get_by_id(context, members[0])?;
contact.get_addr().to_string()
} else {
format!("{} member(s)", members.len())
};
println!(
"{}#{}: {} [{}]{}{}",
let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, 0);
let temp2 = sel_chat.get_subtitle(context);
let temp_name = sel_chat.get_name();
info!(
context,
"{}#{}: {} [{}]{}",
chat_prefix(sel_chat),
sel_chat.get_id(),
sel_chat.get_name(),
subtitle,
temp_name,
temp2,
if sel_chat.is_sending_locations() {
"📍"
} else {
""
},
match sel_chat.get_profile_image(context) {
Some(icon) => match icon.to_str() {
Some(icon) => format!(" Icon: {}", icon),
_ => " Icon: Err".to_string(),
},
_ => "".to_string(),
},
);
log_msglist(context, &msglist)?;
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" => {
@@ -622,7 +671,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
}
"createchatbymsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
let msg_id: u32 = arg1.parse()?;
let chat_id = chat::create_by_msg_id(context, msg_id)?;
let chat = Chat::load_from_db(context, chat_id)?;
@@ -686,7 +735,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!(
@@ -712,8 +761,9 @@ 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!(
"Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} {} {}",
info!(
context,
"Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} Msg#{} {}",
location.location_id,
dc_timestamp_to_str(location.timestamp),
location.latitude,
@@ -726,7 +776,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
);
}
if locations.is_empty() {
println!("No locations.");
info!(context, "No locations.");
}
}
"sendlocations" => {
@@ -750,7 +800,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let longitude = arg2.parse()?;
let continue_streaming = location::set(context, latitude, longitude, 0.);
if continue_streaming {
if 0 != continue_streaming {
println!("Success, streaming should be continued.");
} else {
println!("Success, streaming can be stoppped.");
@@ -775,14 +825,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No file given.");
let mut msg = Message::new(if arg0 == "sendimage" {
let mut msg = dc_msg_new(if arg0 == "sendimage" {
Viewtype::Image
} else {
Viewtype::File
});
msg.set_file(arg1, None);
dc_msg_set_file(&mut msg, arg1_c, ptr::null());
if !arg2.is_empty() {
msg.set_text(Some(arg2.to_string()));
dc_msg_set_text(&mut msg, arg2_c);
}
chat::send_msg(context, sel_chat.as_ref().unwrap().get_id(), &mut msg)?;
}
@@ -792,7 +842,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);
@@ -804,31 +854,19 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(sel_chat.is_some(), "No chat selected.");
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));
let mut draft = dc_msg_new(Viewtype::Text);
dc_msg_set_text(&mut draft, arg1_c);
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.");
}
}
"devicemsg" => {
ensure!(
!arg1.is_empty(),
"Please specify text to add as device message."
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(context, None, Some(&mut msg))?;
}
"updatedevicechats" => {
context.update_device_chats()?;
}
"listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -842,36 +880,32 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
println!("{} images or videos: ", images.len());
for (i, data) in images.iter().enumerate() {
if 0 == i {
print!("{}", data);
print!("Msg#{}", data);
} else {
print!(", {}", data);
print!(", Msg#{}", data);
}
}
print!("\n");
}
"archive" | "unarchive" | "pin" | "unpin" => {
"archive" | "unarchive" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id.set_visibility(
let chat_id = arg1.parse()?;
chat::archive(
context,
match arg0 {
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => panic!("Unexpected command (This should never happen)"),
},
chat_id,
if arg0 == "archive" { true } else { false },
)?;
}
"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.");
let id = MsgId::new(arg1.parse()?);
let res = message::get_msg_info(context, id);
println!("{}", res);
let id = arg1.parse()?;
let res = dc_get_msg_info(context, id);
println!("{}", as_str(res));
}
"listfresh" => {
let msglist = context.get_fresh_msgs();
@@ -881,32 +915,37 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
}
"forward" => {
ensure!(
!arg1.is_empty() && !arg2.is_empty(),
!arg1.is_empty() && arg2.is_empty(),
"Arguments <msg-id> <chat-id> expected"
);
let mut msg_ids = [MsgId::new(0); 1];
let chat_id = ChatId::new(arg2.parse()?);
msg_ids[0] = MsgId::new(arg1.parse()?);
chat::forward_msgs(context, &msg_ids, chat_id)?;
let mut msg_ids = [0; 1];
let chat_id = arg2.parse()?;
msg_ids[0] = arg1.parse()?;
chat::forward_msgs(context, msg_ids.as_mut_ptr(), 1, chat_id);
}
"markseen" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = [MsgId::new(0); 1];
msg_ids[0] = MsgId::new(arg1.parse()?);
message::markseen_msgs(context, &msg_ids);
let mut msg_ids = [0; 1];
msg_ids[0] = arg1.parse()?;
dc_markseen_msgs(context, msg_ids.as_mut_ptr(), 1);
}
"star" | "unstar" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = [MsgId::new(0); 1];
msg_ids[0] = MsgId::new(arg1.parse()?);
message::star_msgs(context, &msg_ids, arg0 == "star");
let mut msg_ids = [0; 1];
msg_ids[0] = arg1.parse()?;
dc_star_msgs(
context,
msg_ids.as_mut_ptr(),
1,
if arg0 == "star" { 1 } else { 0 },
);
}
"delmsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut ids = [MsgId::new(0); 1];
ids[0] = MsgId::new(arg1.parse()?);
message::delete_msgs(context, &ids);
let mut ids = [0; 1];
ids[0] = arg1.parse()?;
dc_delete_msgs(context, ids.as_mut_ptr(), 1);
}
"listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all(
@@ -938,14 +977,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let contact = Contact::get_by_id(context, contact_id)?;
let 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)?;
@@ -982,31 +1014,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
res.get_text2()
);
}
"setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(context, arg1) {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => println!("Cannot set config from QR code: {:?}", err),
}
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);
println!("before_login_hint: {}", info.before_login_hint);
println!("after_login_hint: {}", info.after_login_hint);
println!("overview_page: {}", info.overview_page);
for server in info.server.iter() {
println!("server: {}:{}", server.hostname, server.port,);
}
}
None => {
println!("No information for provider belonging to {} found.", arg1);
}
}
}
// TODO: implement this again, unclear how to match this through though, without writing a parser.
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
@@ -1021,21 +1028,19 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
if let Ok(buf) = dc_read_file(context, &arg1) {
if let Some(buf) = dc_read_file_safe(context, &arg1) {
let (width, height) = dc_get_filemeta(&buf)?;
println!("width={}, height={}", width, height);
} else {
bail!("Command failed.");
}
}
"emptyserver" => {
ensure!(!arg1.is_empty(), "Argument <flags> missing");
message::dc_empty_server(context, arg1.parse()?);
}
"" => (),
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
}
free(arg1_c as *mut _);
free(arg2_c as *mut _);
Ok(())
}

View File

@@ -20,12 +20,14 @@ use std::process::Command;
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::dc_tools::*;
use deltachat::job::*;
use deltachat::oauth2::*;
use deltachat::securejoin::*;
use deltachat::x::*;
use deltachat::Event;
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType;
@@ -41,8 +43,9 @@ 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::GetString { .. } => {}
Event::Info(msg) => {
/* do not show the event as this would fill the screen */
println!("{}", msg);
@@ -111,6 +114,8 @@ fn receive_event(_context: &Context, event: Event) {
print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event);
}
}
0
}
// Threads for waiting for messages and for jobs
@@ -148,11 +153,11 @@ fn start_threads(c: Arc<RwLock<Context>>) {
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());
perform_imap_jobs(&ctx.read().unwrap());
perform_imap_fetch(&ctx.read().unwrap());
while_running!({
let context = ctx.read().unwrap();
perform_inbox_idle(&context);
perform_imap_idle(&context);
});
});
});
@@ -200,7 +205,7 @@ fn stop_threads(context: &Context) {
println!("Stopping threads");
IS_RUNNING.store(false, Ordering::Relaxed);
interrupt_inbox_idle(context);
interrupt_imap_idle(context);
interrupt_mvbox_idle(context);
interrupt_sentbox_idle(context);
interrupt_smtp_idle(context);
@@ -233,7 +238,7 @@ impl Completer for DcHelper {
}
}
const IMEX_COMMANDS: [&str; 12] = [
const IMEX_COMMANDS: [&'static str; 12] = [
"initiate-key-transfer",
"get-setupcodebegin",
"continue-key-transfer",
@@ -248,7 +253,7 @@ const IMEX_COMMANDS: [&str; 12] = [
"stop",
];
const DB_COMMANDS: [&str; 11] = [
const DB_COMMANDS: [&'static str; 11] = [
"info",
"open",
"close",
@@ -262,7 +267,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 26] = [
const CHAT_COMMANDS: [&'static str; 24] = [
"listchats",
"listarchived",
"chat",
@@ -286,11 +291,9 @@ const CHAT_COMMANDS: [&str; 26] = [
"listmedia",
"archive",
"unarchive",
"pin",
"unpin",
"delchat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
const MESSAGE_COMMANDS: [&'static str; 8] = [
"listmsgs",
"msginfo",
"listfresh",
@@ -300,7 +303,7 @@ const MESSAGE_COMMANDS: [&str; 8] = [
"unstar",
"delmsg",
];
const CONTACT_COMMANDS: [&str; 6] = [
const CONTACT_COMMANDS: [&'static str; 6] = [
"listcontacts",
"listverified",
"addcontact",
@@ -308,7 +311,7 @@ const CONTACT_COMMANDS: [&str; 6] = [
"delcontact",
"cleanupcontacts",
];
const MISC_COMMANDS: [&str; 9] = [
const MISC_COMMANDS: [&'static str; 9] = [
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
];
@@ -334,8 +337,8 @@ impl Hinter for DcHelper {
}
}
static COLORED_PROMPT: &str = "\x1b[1;32m> \x1b[0m";
static PROMPT: &str = "> ";
static COLORED_PROMPT: &'static str = "\x1b[1;32m> \x1b[0m";
static PROMPT: &'static str = "> ";
impl Highlighter for DcHelper {
fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
@@ -403,7 +406,7 @@ fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str());
let ctx = ctx.clone();
match handle_cmd(line.trim(), ctx) {
match unsafe { handle_cmd(line.trim(), ctx) } {
Ok(ExitResult::Continue) => {}
Ok(ExitResult::Exit) => break,
Err(err) => println!("Error: {}", err),
@@ -434,10 +437,15 @@ enum ExitResult {
Exit,
}
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failure::Error> {
unsafe fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failure::Error> {
let mut args = line.splitn(2, ' ');
let arg0 = args.next().unwrap_or_default();
let arg1 = args.next().unwrap_or_default();
let arg1_c = if arg1.is_empty() {
std::ptr::null()
} else {
arg1.strdup()
};
match arg0 {
"connect" => {
@@ -455,14 +463,14 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
}
"imap-jobs" => {
if HANDLE.clone().lock().unwrap().is_some() {
println!("inbox-jobs are already running in a thread.");
println!("imap-jobs are already running in a thread.");
} else {
perform_inbox_jobs(&ctx.read().unwrap());
perform_imap_jobs(&ctx.read().unwrap());
}
}
"configure" => {
start_threads(ctx.clone());
ctx.read().unwrap().configure();
configure(&ctx.read().unwrap());
}
"oauth2" => {
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
@@ -486,10 +494,9 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
}
"getqr" | "getbadqr" => {
start_threads(ctx.clone());
if let Some(mut qr) = dc_get_securejoin_qr(
&ctx.read().unwrap(),
ChatId::new(arg1.parse().unwrap_or_default()),
) {
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")
@@ -514,6 +521,8 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
_ => dc_cmdline(&ctx.read().unwrap(), line)?,
}
free(arg1_c as *mut _);
Ok(ExitResult::Continue)
}

View File

@@ -7,105 +7,121 @@ 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,
perform_imap_fetch, perform_imap_idle, perform_imap_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) => {
println!(" progress: {}", progress);
print!(" progress: {}\n", progress);
0
}
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
println!(" {}", msg);
print!(" {}\n", msg);
0
}
_ => {
println!();
print!("\n");
0
}
}
}
fn main() {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
println!("creating database {:?}", dbfile);
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(4000);
println!("info: {:#?}", info);
let ctx = Arc::new(ctx);
let ctx1 = ctx.clone();
let r1 = running.clone();
let t1 = thread::spawn(move || {
while *r1.read().unwrap() {
perform_inbox_jobs(&ctx1);
if *r1.read().unwrap() {
perform_inbox_fetch(&ctx1);
unsafe {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
println!("creating database {:?}", dbfile);
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(4000);
println!("info: {:#?}", info);
let ctx = Arc::new(ctx);
let ctx1 = ctx.clone();
let r1 = running.clone();
let t1 = thread::spawn(move || {
while *r1.read().unwrap() {
perform_imap_jobs(&ctx1);
if *r1.read().unwrap() {
perform_inbox_idle(&ctx1);
perform_imap_fetch(&ctx1);
if *r1.read().unwrap() {
perform_imap_idle(&ctx1);
}
}
}
}
});
});
let ctx1 = ctx.clone();
let r1 = running.clone();
let t2 = thread::spawn(move || {
while *r1.read().unwrap() {
perform_smtp_jobs(&ctx1);
if *r1.read().unwrap() {
perform_smtp_idle(&ctx1);
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!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 2, "missing password");
let pw = args[1].clone();
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
configure(&ctx);
thread::sleep(duration);
println!("sending a message");
let contact_id =
Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
println!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
for i in 0..chats.len() {
let summary = chats.get_summary(&ctx, 0, None);
let text1 = summary.get_text1();
let text2 = summary.get_text2();
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
}
});
println!("configuring");
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);
thread::sleep(duration);
// let msglist = dc_get_chat_msgs(&ctx, chat_id, 0, 0);
// for i in 0..dc_array_get_cnt(msglist) {
// let msg_id = dc_array_get_id(msglist, i);
// let msg = dc_get_msg(context, msg_id);
// let text = CStr::from_ptr(dc_msg_get_text(msg)).unwrap();
// println!("Message {}: {}\n", i + 1, text.to_str().unwrap());
// dc_msg_unref(msg);
// }
// dc_array_unref(msglist);
println!("sending a message");
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!("stopping threads");
println!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
*running.clone().write().unwrap() = false;
deltachat::job::interrupt_imap_idle(&ctx);
deltachat::job::interrupt_smtp_idle(&ctx);
for i in 0..chats.len() {
let summary = chats.get_summary(&ctx, 0, None);
let text1 = summary.get_text1();
let text2 = summary.get_text2();
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
println!("joining");
t1.join().unwrap();
t2.join().unwrap();
println!("closing");
}
thread::sleep(duration);
println!("stopping threads");
*running.write().unwrap() = false;
deltachat::job::interrupt_inbox_idle(&ctx);
deltachat::job::interrupt_smtp_idle(&ctx);
println!("joining");
t1.join().unwrap();
t2.join().unwrap();
println!("closing");
}

View File

@@ -1,7 +0,0 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 03cab93c6d1f3a8245f63cf84dacb307944294fe6333c1e38f078a6600659c7a # shrinks to data = "a\t0aA\ta\t0 \ta\t0 \ta a\t\ta A\tAA0a0a 0\t a\t aA \t a\t A0\t AAa\taA0\taAAaA\t0\taa0a\ta Aa Aaaa A0A\t a aA 0\t A\t0\t0\t\t\t\t\t\tA \t\t a\tA Aa aAA0A0AA0aaA A\t\t aa0\ta\t \tAa\taA\t00 AA A a\tA a aAAa \t 00\t0 \t\t a A 0\t\t\t aAA Aa \taAAAA0a A0A\t\t1\\E$\t$R\tc\t^=\t\"\tQ<Uk\t\t>A\t\t&\t}v&\tM^\'|\tW5?dn\t\t+\t\tP\te`\t\t>:Brlq--?\t$#Q\tK=zJ\tb\"9*.\"\t`\tF&T*\tBs,\tg\'*\\\t:\t?l$\t\t|A\"HR:Hk\t\\KkV\t\t{=&!^e%:|_*0wV\t[$`\t:\t$%f\t\t[!\"Y. \tP\t\th\'?\'/?%:++NfQo#@\"+?\t(\\??{\t\'\'$Dzj\t0.?{s4?&?Y=/yj]Z=\t4n\t?Ja\"\t{I\t$\t;I}_8V\t&\t?N\'\tI2/\t9.\tFT%9%`\'\tz\to7Y\t|AXP&@G12g\t\'w\t\t%??\t\"h$?F\"\"6%q\\\\{\tT\t\"]$87$}h\'\t<\t$\tc%U:mT2:<v\t#Rl!;U\t\t\"^D\tRZ(BZ{n\t%[$fgH`\t{B}:*\t\t%%*F`%W\t//B}PQ\t\tsu2\tLz<1*!p-X\tnKv&&0\thm4<n\\.\\/.w\'\t<)E1g3#ood\t`?\t\\({N?,8_?h\ty\t0%\t*$A\t\t*w-ViQUj\tTiH\t%\t%&0p\'\'\tA%r**Fo\'Z\\\tNI*ely4=I?}$.o`\t$\ts\'&lt\t\",:~=Nv/0O%=+<LS\t%P\'\t$r%d.\t{G{/L:\t&I&8-`cy*\"{\t/%fP9.P<\t\t\'/`\t\t`\t\t`!t:\t::\t\tW\'X^\t@.uL&a\tN\t\t\t.\t?0*\tvUK>UJ\\\tQbj%w]P=Js\t\"R\t&s^?</=%\t\'VI:\" kT`{b*<\t\tF&?Y\t\t:/\t!C\'e0`\t\t\tx-\t*\\N\\wwu.I4\tb/y\t\"P:P%\"\t\tQ][\\\t^x\t\t):\t\t&s\t$1-\t\t\tXt`0\t;\t/UeCP*\"G\t\t\':\tk@91Hf&#\t(Uzz&`p2;\t{]\t\"I_%=\\%,XD\"\'06QfCd<;$:q^\t8:\"*\"H\t\to\t&xK/\t\ty0/<}<j<|*`~::b\t=S.=}=Ki\t<Y.\'{\tf\t{Ub*H%(_4*?*\tn2\t/\'\t\t\t/,4]\tt\t<y\t\t\tWi\t\tT&\"\t\t\t\t\t=/1Wu?\t\'A\"W-P\t$?e\\\t`\t6`vD\t8`\t\tccD J\tY&jI//?_\t\\j\t_\tsiq$\t?9\tQ\t.^%&..?%Jm??A%+<\tN&*\t.g\tS$W\"\"\tMpq\t\t:&\\\thTz&<iz%/%T&\'F\t\\]&\t\t}\t\t\tXr=dtp`:+<,\t%60Y*<(&K*kJ\todO\t=<V?&\tMU/\"\t= Y!)<\tV\t9\t)\t&v8q{\t\t&pT\t3\ttB,qcq\'i$;gv%j_%M_{[\"&;\t\t\t.B;6Y\\%\t\"\tY/a\t\\`:?\t<\t?]\taNwD;\\\t%l*74%5T?QS :~yR/E*R\t\t=u\t\\\t\t.Q<;\\\t_S/{&F$^\tw_>\'h=|%\t\t:W!\\<U2\'$\tb&`\t=|=L9\t\t\t\\WZ:! }{\t ;\t;\t\t 0.*\t.%\"\'r%{<Mh_t[[}\t-\tJo\"b/AC*-=`=T\tz$2\tC\t\t/k{47\"\t\t,q%\tZ\tT3\t\tf>%\t\'?%@^tx\t7\"1Bk{b{n\t\"Pj3\tHc\t\tt\tY<\t#?\tSh\\yk/N\\\t8 7=R4*9Cw&m\t\\-\'f\t|\'#t(Etu.Hdu(E\t%&v:\'aqW~A5\t\t w.s{J%lX<\"\t\'*%m<&:/B<&\':U}$&`.{)\t\t6S\t:/$*kQ-Z\t^\'t${/tH*\'v\t3\t=\t\tDyp:B\t`I_R&4SO\t\t&-j=*.\t87&\'e*( \t\t\t\'<$\\DJ<$p?{}\'&\tv\t\\Xk<Y:Y`!$K{\tF&\tzd\t\t*i$\tj\'\t<)R*\t%?\t!.\t=\"@#~:=*\t\tXO=_T,1\"\'.%%\"`{\\:\t\"\tfkeOb/\'$I~\ta\t|&\t[\\KK\"1&Z\t<k\t\t)%\'-~\"2n\tj\tW?*<@w{g%d\ta\\\'\'I\t;:ySR%ke:4\tc\t$=\t&9P]x4\tJ=\t6C6%a\t`0\tF\tm-\tTr\t}\t\tQum\t&@\typ|w2&\t\t3`i&t\t\tT5\"\t.&b&e*/==1.\'*\\[U*\tqPL%?$-0/}~|q`\t\t}\t$\tq==o+T$\'!H\t\ti&um\"?\"%%\t/\'p\tg>?{0{J{\t\t/\t\t{zKZ&>=\t[\"1h<H%z/8,/]s\tv{7\t\t:j*H,M//\t\t\td\'.)\t"

View File

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

View File

@@ -1,17 +1,6 @@
0.800.0
-------
- use latest core 1.25.0
- refine tests and some internal changes to core bindings
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,98 +3,41 @@ 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)
==========================================
Installing bindings from source (Updated: 20-Jan-2020)
=========================================================
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.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then GIT clone the deltachat-core-rust repo and get the actual
rust- and cargo-toolchain needed by deltachat::
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.
Ensure you are in the deltachat-core-rust/python directory, create the
virtual environment and activate it in your shell::
cd python
python3 -m venv venv # or: virtualenv venv
source venv/bin/activate
You should now be able to build the python bindings using the supplied script::
./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.
After successful binding installation you can install a few more
Python packages before running the tests::
python -m pip install pytest pytest-timeout pytest-rerunfailures requests
pytest -v tests
running "live" tests with temporary accounts
---------------------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_NEW_TMP_EMAIL`` to:
- a particular https-url that you can ask for from the delta
chat devs. This is implemented on the server side via
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
- or the path of a file that contains two lines, each describing
via "addr=... mail_pw=..." a test account login that will
be used for the live tests.
With ``DCC_NEW_TMP_EMAIL`` 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
If you have a linux system you may install the ``deltachat`` binary "wheel" package
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::
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`` only
modifies files in your ``venv`` directory and leaves
your system installation alone.
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 a wheel from a PR/branch
---------------------------------------
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::
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"
pip install -i https://m.devpi.net/dc/master deltachat
.. note::
@@ -103,6 +46,65 @@ To verify it worked::
`in contact with us <https://delta.chat/en/contribute>`_.
Installing bindings from source
===============================
If you can't use "binary" method above then you need to compile
to core deltachat library::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
cd python
If you don't have one active, create and activate a python "virtualenv":
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 bindings will be installed in release mode but with debug symbols.
The release mode is necessary because some tests generate RSA keys
which is prohibitively slow in debug mode.
After successful binding installation you can finally run the tests::
pytest -v tests
.. note::
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)
-----------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_PY_LIVECONFIG`` to:
- a particular https-url that you can ask for from the delta
chat devs.
- or the path of a file that contains two lines, each describing
via "addr=... mail_pwd=..." a test account login that will
be used for the live tests.
With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real
e-mail accounts and run through all functional "liveconfig" tests.
Code examples
=============
@@ -113,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
------------------------------------
@@ -130,9 +136,9 @@ organization::
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e DCC_NEW_TMP_EMAIL \
--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

@@ -6,6 +6,7 @@
<li><a href="{{ pathto('install') }}">install</a></li>
<li><a href="{{ pathto('api') }}">high level API</a></li>
<li><a href="{{ pathto('lapi') }}">low level API</a></li>
<li><a href="{{ pathto('capi') }}">C deltachat.h</a></li>
</ul>
<b>external links:</b>
<ul>

View File

@@ -8,8 +8,8 @@ high level API reference
- :class:`deltachat.account.Account` (your main entry point, creates the
other classes)
- :class:`deltachat.contact.Contact`
- :class:`deltachat.chat.Chat`
- :class:`deltachat.chatting.Contact`
- :class:`deltachat.chatting.Chat`
- :class:`deltachat.message.Message`
Account
@@ -22,13 +22,13 @@ Account
Contact
-------
.. autoclass:: deltachat.contact.Contact
.. autoclass:: deltachat.chatting.Contact
:members:
Chat
----
.. autoclass:: deltachat.chat.Chat
.. autoclass:: deltachat.chatting.Chat
:members:
Message

View File

@@ -2,13 +2,7 @@
low level API reference
===================================
for full doxygen-generated C-docs, defines and functions please checkout
https://c.delta.chat
Python low-level capi calls
---------------------------
for full C-docs, defines and function checkout :doc:`capi`
.. automodule:: deltachat.capi.lib

View File

@@ -6,23 +6,20 @@
import os
import subprocess
import sys
import os
if __name__ == "__main__":
target = os.environ.get("DCC_RS_TARGET")
if target is None:
os.environ["DCC_RS_TARGET"] = target = "debug"
os.environ["DCC_RS_TARGET"] = target = "release"
if "DCC_RS_DEV" not in os.environ:
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.environ["DCC_RS_DEV"] = dn
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
if target == 'release':
cmd.append("--release")
subprocess.check_call(cmd)
os.environ["RUSTFLAGS"] = "-g"
subprocess.check_call([
"cargo", "build", "-p", "deltachat_ffi", "--" + target
])
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
subprocess.check_call([
sys.executable, "-m", "pip", "install", "-e", "."
])
subprocess.check_call([
"pip", "install", "-e", "."
])

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

@@ -29,10 +29,7 @@ def ffibuilder():
extra_link_args = []
else:
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
target_dir = os.environ.get("CARGO_TARGET_DIR")
if target_dir is None:
target_dir = os.path.join(projdir, 'target')
objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
objs = [os.path.join(projdir, 'target', target, 'libdeltachat.a')]
assert os.path.exists(objs[0]), objs
incs = [os.path.join(projdir, 'deltachat-ffi')]
else:

View File

@@ -1,7 +1,6 @@
""" Account class implementation. """
from __future__ import print_function
import atexit
import threading
import os
import re
@@ -16,17 +15,15 @@ import deltachat
from . import const
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
from .chat import Chat
from .message import Message
from .contact import Contact
from .chatting import Contact, Chat, Message
class Account(object):
""" Each account is tied to a sqlite database file which is fully managed
by the underlying deltachat core library. All public Account methods are
by the underlying deltachat c-library. All public Account methods are
meant to be memory-safe and return memory-safe objects.
"""
def __init__(self, db_path, logid=None, eventlogging=True, os_name=None, debug=True):
def __init__(self, db_path, logid=None, eventlogging=True):
""" initialize account object.
:param db_path: a path to the account database. The database
@@ -34,15 +31,13 @@ class Account(object):
:param logid: an optional logging prefix that should be used with
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(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
_destroy_dc_context,
)
if eventlogging:
self._evlogger = EventLogger(self._dc_context, logid, debug)
self._evlogger = EventLogger(self._dc_context, logid)
deltachat.set_context_callback(self._dc_context, self._process_event)
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
else:
@@ -53,11 +48,10 @@ class Account(object):
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
raise ValueError("Could not dc_open: {}".format(db_path))
self._configkeys = self.get_config("sys.config_keys").split()
self._imex_events = Queue()
atexit.register(self.shutdown)
self._imex_completed = threading.Event()
# def __del__(self):
# self.shutdown()
def __del__(self):
self.shutdown()
def _check_config_key(self, name):
if name not in self._configkeys:
@@ -75,18 +69,6 @@ class Account(object):
d[key.lower()] = value
return d
def set_stock_translation(self, id, string):
""" set stock translation string.
:param id: id of stock string (const.DC_STR_*)
:param value: string to set as new transalation
:returns: None
"""
string = string.encode("utf8")
res = lib.dc_set_stock_translation(self._dc_context, id, string)
if res == 0:
raise ValueError("could not set translation string")
def set_config(self, name, value):
""" set configuration values.
@@ -96,12 +78,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):
@@ -118,18 +97,6 @@ class Account(object):
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr, public, secret):
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
"""
res = lib.dc_preconfigure_keypair(self._dc_context,
as_dc_charpointer(addr),
as_dc_charpointer(public),
as_dc_charpointer(secret))
if res == 0:
raise Exception("Failed to set key")
def configure(self, **kwargs):
""" set config values and configure this account.
@@ -149,47 +116,16 @@ 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():
raise ValueError("need to configure first")
def empty_server_folders(self, inbox=False, mvbox=False):
""" empty server folders. """
flags = 0
if inbox:
flags |= const.DC_EMPTY_INBOX
if mvbox:
flags |= const.DC_EMPTY_MVBOX
if not flags:
raise ValueError("no flags set")
lib.dc_empty_server(self._dc_context, flags)
def get_infostring(self):
""" return info of the configured account. """
self.check_is_configured()
return from_dc_charpointer(lib.dc_get_info(self._dc_context))
def get_latest_backupfile(self, backupdir):
""" return the latest backup file in a given directory.
"""
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
if res == ffi.NULL:
return None
return from_dc_charpointer(res)
def get_blobdir(self):
""" return the directory for files.
@@ -199,9 +135,9 @@ class Account(object):
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
def get_self_contact(self):
""" return this account's identity as a :class:`deltachat.contact.Contact`.
""" return this account's identity as a :class:`deltachat.chatting.Contact`.
:returns: :class:`deltachat.contact.Contact`
:returns: :class:`deltachat.chatting.Contact`
"""
self.check_is_configured()
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
@@ -213,7 +149,7 @@ class Account(object):
:param email: email-address (text type)
:param name: display name for this contact (optional)
:returns: :class:`deltachat.contact.Contact` instance.
:returns: :class:`deltachat.chatting.Contact` instance.
"""
name = as_dc_charpointer(name)
email = as_dc_charpointer(email)
@@ -239,7 +175,7 @@ class Account(object):
whose name or e-mail matches query.
:param only_verified: if true only return verified contacts.
:param with_self: if true the self-contact is also returned.
:returns: list of :class:`deltachat.contact.Contact` objects.
:returns: list of :class:`deltachat.chatting.Contact` objects.
"""
flags = 0
query = as_dc_charpointer(query)
@@ -257,7 +193,7 @@ class Account(object):
""" create or get an existing 1:1 chat object for the specified contact or contact id.
:param contact: chat_id (int) or contact object.
:returns: a :class:`deltachat.chat.Chat` object.
:returns: a :class:`deltachat.chatting.Chat` object.
"""
if hasattr(contact, "id"):
if contact._dc_context != self._dc_context:
@@ -274,7 +210,7 @@ class Account(object):
the specified message.
:param message: messsage id or message instance.
:returns: a :class:`deltachat.chat.Chat` object.
:returns: a :class:`deltachat.chatting.Chat` object.
"""
if hasattr(message, "id"):
if self._dc_context != message._dc_context:
@@ -292,16 +228,16 @@ class Account(object):
Chats are unpromoted until the first message is sent.
:param verified: if true only verified contacts can be added.
:returns: a :class:`deltachat.chat.Chat` object.
:returns: a :class:`deltachat.chatting.Chat` object.
"""
bytes_name = name.encode("utf8")
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
chat_id = lib.dc_create_group_chat(self._dc_context, verified, bytes_name)
return Chat(self, chat_id)
def get_chats(self):
""" return list of chats.
:returns: a list of :class:`deltachat.chat.Chat` objects.
:returns: a list of :class:`deltachat.chatting.Chat` objects.
"""
dc_chatlist = ffi.gc(
lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0),
@@ -319,24 +255,9 @@ class Account(object):
return Chat(self, const.DC_CHAT_ID_DEADDROP)
def get_message_by_id(self, msg_id):
""" return Message instance.
:param msg_id: integer id of this message.
:returns: :class:`deltachat.message.Message` instance.
"""
""" return Message instance. """
return Message.from_db(self, msg_id)
def get_chat_by_id(self, chat_id):
""" return Chat instance.
:param chat_id: integer id of this chat.
:returns: :class:`deltachat.chat.Chat` instance.
:raises: ValueError if chat does not exist.
"""
res = lib.dc_get_chat(self._dc_context, chat_id)
if res == ffi.NULL:
raise ValueError("cannot get chat with id={}".format(chat_id))
lib.dc_chat_unref(res)
return Chat(self, chat_id)
def mark_seen_messages(self, messages):
""" mark the given set of messages as seen.
@@ -353,7 +274,7 @@ class Account(object):
""" Forward list of messages to a chat.
:param messages: list of :class:`deltachat.message.Message` object.
:param chat: :class:`deltachat.chat.Chat` object.
:param chat: :class:`deltachat.chatting.Chat` object.
:returns: None
"""
msg_ids = [msg.id for msg in messages]
@@ -368,64 +289,31 @@ class Account(object):
msg_ids = [msg.id for msg in messages]
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
def export_self_keys(self, path):
""" export public and private keys to the specified directory. """
return self._export(path, imex_cmd=1)
def export_all(self, path):
"""return new file containing a backup of all database state
(chats, contacts, keys, media, ...). The file is created in the
the `path` directory.
def export_to_dir(self, backupdir):
"""return after all delta chat state is exported to a new file in
the specified directory.
"""
export_files = self._export(path, 11)
if len(export_files) != 1:
raise RuntimeError("found more than one new file")
return export_files[0]
def _imex_events_clear(self):
try:
while True:
self._imex_events.get_nowait()
except Empty:
pass
def _export(self, path, imex_cmd):
self._imex_events_clear()
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
snap_files = os.listdir(backupdir)
self._imex_completed.clear()
lib.dc_imex(self._dc_context, 11, as_dc_charpointer(backupdir), 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()
if isinstance(ev, str):
files_written.append(ev)
elif isinstance(ev, bool):
if not ev:
raise ValueError("export failed, exp-files: {}".format(files_written))
return files_written
self._imex_completed.wait()
for x in os.listdir(backupdir):
if x not in snap_files:
return os.path.join(backupdir, x)
def import_self_keys(self, path):
""" Import private keys found in the `path` directory.
The last imported key is made the default keys unless its name
contains the string legacy. Public keys are not imported.
"""
self._import(path, imex_cmd=2)
def import_all(self, path):
"""import delta chat state from the specified backup `path` (a file).
def import_from_file(self, path):
"""import delta chat state from the specified backup file.
The account must be in unconfigured state for import to attempted.
"""
assert not self.is_configured(), "cannot import into configured account"
self._import(path, imex_cmd=12)
def _import(self, path, imex_cmd):
self._imex_events_clear()
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
self._imex_completed.clear()
lib.dc_imex(self._dc_context, 12, 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))
self._imex_completed.wait()
def initiate_key_transfer(self):
"""return setup code after a Autocrypt setup message
@@ -465,7 +353,7 @@ class Account(object):
""" setup contact and return a Chat after contact is established.
Note that this function may block for a long time as messages are exchanged
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
is returned.
:param qr: valid "setup contact" QR code (all other QR codes will result in an exception)
"""
@@ -479,7 +367,7 @@ class Account(object):
""" join a chat group through a QR code.
Note that this function may block for a long time as messages are exchanged
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
is returned which is the chat that we just joined.
:param qr: valid "join-group" QR code (all other QR codes will result in an exception)
@@ -490,19 +378,7 @@ class Account(object):
raise ValueError("could not join group")
return Chat(self, chat_id)
def stop_ongoing(self):
lib.dc_stop_ongoing_process(self._dc_context)
#
# meta API for start/stop and event based processing
#
def wait_next_incoming_message(self):
""" wait for and return next incoming message. """
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
return self.get_message_by_id(ev[2])
def start_threads(self, mvbox=False, sentbox=False):
def start_threads(self):
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
@@ -510,13 +386,12 @@ class Account(object):
"""
if not self.is_configured():
self.configure()
self._threads.start(mvbox=mvbox, sentbox=sentbox)
self._threads.start()
def stop_threads(self, wait=True):
""" stop IMAP/SMTP threads. """
if self._threads.is_started():
self.stop_ongoing()
self._threads.stop(wait=wait)
lib.dc_stop_ongoing_process(self._dc_context)
self._threads.stop(wait=wait)
def shutdown(self, wait=True):
""" stop threads and close and remove underlying dc_context and callbacks. """
@@ -527,7 +402,6 @@ class Account(object):
self.stop_threads(wait=wait) # to wait for threads
deltachat.clear_context_callback(self._dc_context)
del self._dc_context
atexit.unregister(self.shutdown)
def _process_event(self, ctx, evt_name, data1, data2):
assert ctx == self._dc_context
@@ -540,26 +414,7 @@ class Account(object):
def on_dc_event_imex_progress(self, data1, data2):
if data1 == 1000:
self._imex_events.put(True)
elif data1 == 0:
self._imex_events.put(False)
def on_dc_event_imex_file_written(self, data1, data2):
self._imex_events.put(data1)
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
"""set a new location. It effects all chats where we currently
have enabled location streaming.
:param latitude: float (use 0.0 if not known)
:param longitude: float (use 0.0 if not known)
:param accuracy: float (use 0.0 if not known)
:raises: ValueError if no chat is currently streaming locations
:returns: None
"""
dc_res = lib.dc_set_location(self._dc_context, latitude, longitude, accuracy)
if dc_res == 0:
raise ValueError("no chat is streaming locations")
self._imex_completed.set()
class IOThreads:
@@ -572,14 +427,10 @@ class IOThreads:
def is_started(self):
return len(self._name2thread) > 0
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
def start(self, imap=True, smtp=True):
assert not self.is_started()
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)
self._start_one_thread("imap", self.imap_thread_run)
if smtp:
self._start_one_thread("smtp", self.smtp_thread_run)
@@ -590,55 +441,25 @@ class IOThreads:
def stop(self, wait=False):
self._thread_quitflag = True
# Workaround for a race condition. Make sure that thread is
# not in between checking for quitflag and entering idle.
time.sleep(0.5)
lib.dc_interrupt_imap_idle(self._dc_context)
lib.dc_interrupt_smtp_idle(self._dc_context)
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 imap_thread_run(self):
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
self._log_event("py-bindings-info", 0, "IMAP THREAD START")
while not self._thread_quitflag:
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)
if not self._thread_quitflag:
lib.dc_perform_mvbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_idle(self._dc_context)
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)
if not self._thread_quitflag:
lib.dc_perform_sentbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
lib.dc_perform_imap_fetch(self._dc_context)
lib.dc_perform_imap_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "IMAP 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)
if not self._thread_quitflag:
lib.dc_perform_smtp_idle(self._dc_context)
lib.dc_perform_smtp_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
@@ -684,11 +505,11 @@ class EventLogger:
else:
assert not rex.match(ev[0]), "event found {}".format(ev)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
def get_matching(self, event_name_regex, check_error=True):
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
ev = self.get(timeout=timeout, check_error=check_error)
ev = self.get()
if rex.match(ev[0]):
return ev

View File

@@ -1,16 +1,58 @@
""" Chat and Location related API. """
""" chatting related objects: Contact, Chat, Message. """
import mimetypes
import calendar
import json
from datetime import datetime
import os
from . import props
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .capi import lib, ffi
from . import const
from .message import Message
class Contact(object):
""" Delta-Chat Contact.
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, dc_context, id):
self._dc_context = dc_context
self.id = id
def __eq__(self, other):
return self._dc_context == other._dc_context and self.id == other.id
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
@property
def _dc_contact(self):
return ffi.gc(
lib.dc_get_contact(self._dc_context, self.id),
lib.dc_contact_unref
)
@props.with_doc
def addr(self):
""" normalized e-mail address for this account. """
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
@props.with_doc
def display_name(self):
""" display name for this contact. """
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
def is_blocked(self):
""" Return True if the contact is blocked. """
return lib.dc_contact_is_blocked(self._dc_contact)
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)
class Chat(object):
""" Chat object which manages members and through which you can send and retrieve messages.
@@ -58,13 +100,6 @@ class Chat(object):
"""
return self.id == const.DC_CHAT_ID_DEADDROP
def is_muted(self):
""" return true if this chat is muted.
:returns: True if chat is muted, False otherwise.
"""
return lib.dc_chat_is_muted(self._dc_chat)
def is_promoted(self):
""" return True if this chat is promoted, i.e.
the member contacts are aware of their membership,
@@ -74,13 +109,6 @@ class Chat(object):
"""
return not lib.dc_chat_is_unpromoted(self._dc_chat)
def is_verified(self):
""" return True if this chat is a verified group.
:returns: True if chat is verified, False otherwise.
"""
return lib.dc_chat_is_verified(self._dc_chat)
def get_name(self):
""" return name of this chat.
@@ -91,43 +119,12 @@ class Chat(object):
def set_name(self, name):
""" set name of this chat.
:param name: as a unicode string.
:param: name as a unicode string.
:returns: None
"""
name = as_dc_charpointer(name)
return lib.dc_set_chat_name(self._dc_context, self.id, name)
def mute(self, duration=None):
""" mutes the chat
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
:returns: None
"""
if duration is None:
mute_duration = -1
else:
mute_duration = duration
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration)
if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self):
""" unmutes the chat
:returns: None
"""
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0)
if not bool(ret):
raise ValueError("Failed to unmute chat")
def get_mute_duration(self):
""" Returns the number of seconds until the mute of this chat is lifted.
:param duration:
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
"""
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
def get_type(self):
""" return type of this chat.
@@ -147,30 +144,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.
@@ -192,12 +165,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.
@@ -207,12 +177,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.
@@ -311,12 +278,6 @@ class Chat(object):
"""
return lib.dc_marknoticed_chat(self._dc_context, self.id)
def get_summary(self):
""" return dictionary with summary information. """
dc_res = lib.dc_chat_get_info_json(self._dc_context, self.id)
s = from_dc_charpointer(dc_res)
return json.loads(s)
# ------ group management API ------------------------------
def add_contact(self, contact):
@@ -344,10 +305,9 @@ class Chat(object):
def get_contacts(self):
""" get all contacts for this chat.
:params: contact object.
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
:returns: list of :class:`deltachat.chatting.Contact` objects for this chat
"""
from .contact import Contact
dc_array = ffi.gc(
lib.dc_get_chat_contacts(self._dc_context, self.id),
lib.dc_array_unref
@@ -398,80 +358,3 @@ class Chat(object):
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)
def get_color(self):
"""return the color of the chat.
:returns: color as 0x00rrggbb
"""
return lib.dc_chat_get_color(self._dc_chat)
def get_subtitle(self):
"""return the subtitle of the chat
:returns: the subtitle
"""
return from_dc_charpointer(lib.dc_chat_get_subtitle(self._dc_chat))
# ------ location streaming API ------------------------------
def is_sending_locations(self):
"""return True if this chat has location-sending enabled currently.
:returns: True if location sending is enabled.
"""
return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id)
def is_archived(self):
"""return True if this chat is archived.
:returns: True if archived.
"""
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
def enable_sending_locations(self, seconds):
"""enable sending locations for this chat.
all subsequent messages will carry a location with them.
"""
lib.dc_send_locations_to_chat(self._dc_context, self.id, seconds)
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
"""return list of locations for the given contact in the given timespan.
:param contact: the contact for which locations shall be returned.
:param timespan_from: a datetime object or None (indicating "since beginning")
:param timespan_to: a datetime object or None (indicating up till now)
:returns: list of :class:`deltachat.chat.Location` objects.
"""
if timestamp_from is None:
time_from = 0
else:
time_from = calendar.timegm(timestamp_from.utctimetuple())
if timestamp_to is None:
time_to = 0
else:
time_to = calendar.timegm(timestamp_to.utctimetuple())
if contact is None:
contact_id = 0
else:
contact_id = contact.id
dc_array = lib.dc_get_locations(self._dc_context, self.id, contact_id, time_from, time_to)
return [
Location(
latitude=lib.dc_array_get_latitude(dc_array, i),
longitude=lib.dc_array_get_longitude(dc_array, i),
accuracy=lib.dc_array_get_accuracy(dc_array, i),
timestamp=datetime.utcfromtimestamp(lib.dc_array_get_timestamp(dc_array, i)))
for i in range(lib.dc_array_get_cnt(dc_array))
]
class Location:
def __init__(self, latitude, longitude, accuracy, timestamp):
assert isinstance(timestamp, datetime)
self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
self.timestamp = timestamp
def __eq__(self, other):
return self.__dict__ == other.__dict__

View File

@@ -8,6 +8,9 @@ from os.path import join as joinpath
# this works well when you in a git-checkout
# run "python deltachat/const.py" to regenerate events
# begin const generated
DC_PROVIDER_STATUS_OK = 1
DC_PROVIDER_STATUS_PREPARATION = 2
DC_PROVIDER_STATUS_BROKEN = 3
DC_GCL_ARCHIVED_ONLY = 0x01
DC_GCL_NO_SPECIALS = 0x02
DC_GCL_ADD_ALLDONE_HINT = 0x04
@@ -18,7 +21,6 @@ DC_QR_ASK_VERIFYGROUP = 202
DC_QR_FPR_OK = 210
DC_QR_FPR_MISMATCH = 220
DC_QR_FPR_WITHOUT_ADDR = 230
DC_QR_ACCOUNT = 250
DC_QR_ADDR = 320
DC_QR_TEXT = 330
DC_QR_URL = 332
@@ -48,39 +50,19 @@ DC_STATE_OUT_FAILED = 24
DC_STATE_OUT_DELIVERED = 26
DC_STATE_OUT_MDN_RCVD = 28
DC_CONTACT_ID_SELF = 1
DC_CONTACT_ID_INFO = 2
DC_CONTACT_ID_DEVICE = 5
DC_CONTACT_ID_DEVICE = 2
DC_CONTACT_ID_LAST_SPECIAL = 9
DC_MSG_TEXT = 10
DC_MSG_IMAGE = 20
DC_MSG_GIF = 21
DC_MSG_STICKER = 23
DC_MSG_AUDIO = 40
DC_MSG_VOICE = 41
DC_MSG_VIDEO = 50
DC_MSG_FILE = 60
DC_LP_AUTH_OAUTH2 = 0x2
DC_LP_AUTH_NORMAL = 0x4
DC_LP_IMAP_SOCKET_STARTTLS = 0x100
DC_LP_IMAP_SOCKET_SSL = 0x200
DC_LP_IMAP_SOCKET_PLAIN = 0x400
DC_LP_SMTP_SOCKET_STARTTLS = 0x10000
DC_LP_SMTP_SOCKET_SSL = 0x20000
DC_LP_SMTP_SOCKET_PLAIN = 0x40000
DC_CERTCK_AUTO = 0
DC_CERTCK_STRICT = 1
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3
DC_EMPTY_MVBOX = 0x01
DC_EMPTY_INBOX = 0x02
DC_EVENT_INFO = 100
DC_EVENT_SMTP_CONNECTED = 101
DC_EVENT_IMAP_CONNECTED = 102
DC_EVENT_SMTP_MESSAGE_SENT = 103
DC_EVENT_IMAP_MESSAGE_DELETED = 104
DC_EVENT_IMAP_MESSAGE_MOVED = 105
DC_EVENT_IMAP_FOLDER_EMPTIED = 106
DC_EVENT_NEW_BLOB_FILE = 150
DC_EVENT_DELETED_BLOB_FILE = 151
DC_EVENT_WARNING = 300
DC_EVENT_ERROR = 400
DC_EVENT_ERROR_NETWORK = 401
@@ -98,73 +80,15 @@ DC_EVENT_IMEX_PROGRESS = 2051
DC_EVENT_IMEX_FILE_WRITTEN = 2052
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
DC_EVENT_SECUREJOIN_MEMBER_ADDED = 2062
DC_EVENT_GET_STRING = 2091
DC_EVENT_FILE_COPIED = 2055
DC_EVENT_IS_OFFLINE = 2081
DC_EVENT_GET_STRING = 2091
DC_STR_SELFNOTINGRP = 21
DC_KEY_GEN_DEFAULT = 0
DC_KEY_GEN_RSA2048 = 1
DC_KEY_GEN_ED25519 = 2
DC_PROVIDER_STATUS_OK = 1
DC_PROVIDER_STATUS_PREPARATION = 2
DC_PROVIDER_STATUS_BROKEN = 3
DC_CHAT_VISIBILITY_NORMAL = 0
DC_CHAT_VISIBILITY_ARCHIVED = 1
DC_CHAT_VISIBILITY_PINNED = 2
DC_STR_NOMESSAGES = 1
DC_STR_SELF = 2
DC_STR_DRAFT = 3
DC_STR_MEMBER = 4
DC_STR_CONTACT = 6
DC_STR_VOICEMESSAGE = 7
DC_STR_DEADDROP = 8
DC_STR_IMAGE = 9
DC_STR_VIDEO = 10
DC_STR_AUDIO = 11
DC_STR_FILE = 12
DC_STR_STATUSLINE = 13
DC_STR_NEWGROUPDRAFT = 14
DC_STR_MSGGRPNAME = 15
DC_STR_MSGGRPIMGCHANGED = 16
DC_STR_MSGADDMEMBER = 17
DC_STR_MSGDELMEMBER = 18
DC_STR_MSGGROUPLEFT = 19
DC_STR_GIF = 23
DC_STR_ENCRYPTEDMSG = 24
DC_STR_E2E_AVAILABLE = 25
DC_STR_ENCR_TRANSP = 27
DC_STR_ENCR_NONE = 28
DC_STR_CANTDECRYPT_MSG_BODY = 29
DC_STR_FINGERPRINTS = 30
DC_STR_READRCPT = 31
DC_STR_READRCPT_MAILBODY = 32
DC_STR_MSGGRPIMGDELETED = 33
DC_STR_E2E_PREFERRED = 34
DC_STR_CONTACT_VERIFIED = 35
DC_STR_CONTACT_NOT_VERIFIED = 36
DC_STR_CONTACT_SETUP_CHANGED = 37
DC_STR_ARCHIVEDCHATS = 40
DC_STR_STARREDMSGS = 41
DC_STR_AC_SETUP_MSG_SUBJECT = 42
DC_STR_AC_SETUP_MSG_BODY = 43
DC_STR_SELFTALK_SUBTITLE = 50
DC_STR_CANNOT_LOGIN = 60
DC_STR_SERVER_RESPONSE = 61
DC_STR_MSGACTIONBYUSER = 62
DC_STR_MSGACTIONBYME = 63
DC_STR_MSGLOCATIONENABLED = 64
DC_STR_MSGLOCATIONDISABLED = 65
DC_STR_LOCATION = 66
DC_STR_STICKER = 67
DC_STR_DEVICE_MESSAGES = 68
DC_STR_COUNT = 68
# end const generated
def read_event_defines(f):
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER|DC_KEY_GEN)_\S+)\s+([x\d]+).*')
rex = re.compile(r'#define\s+((?:DC_EVENT_|DC_QR|DC_MSG|DC_STATE_|'
r'DC_CONTACT_ID_|DC_GCL|DC_CHAT|DC_PROVIDER)\S+)\s+([x\d]+).*')
for line in f:
m = rex.match(line)
if m:

View File

@@ -1,59 +0,0 @@
""" Contact object. """
from . import props
from .cutil import from_dc_charpointer
from .capi import lib, ffi
class Contact(object):
""" Delta-Chat Contact.
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, dc_context, id):
self._dc_context = dc_context
self.id = id
def __eq__(self, other):
return self._dc_context == other._dc_context and self.id == other.id
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
@property
def _dc_contact(self):
return ffi.gc(
lib.dc_get_contact(self._dc_context, self.id),
lib.dc_contact_unref
)
@props.with_doc
def addr(self):
""" normalized e-mail address for this account. """
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
@props.with_doc
def display_name(self):
""" display name for this contact. """
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
def is_blocked(self):
""" Return True if the contact is blocked. """
return lib.dc_contact_is_blocked(self._dc_contact)
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)
def get_profile_image(self):
"""Get contact profile image.
:returns: path to profile image, None if no profile image exists.
"""
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)

View File

@@ -1,6 +1,7 @@
""" The Message object. """
""" chatting related objects: Contact, Chat, Message. """
import os
import shutil
from . import props
from .cutil import from_dc_charpointer, as_dc_charpointer
from .capi import lib, ffi
@@ -12,7 +13,7 @@ class Message(object):
""" Message object.
You obtain instances of it through :class:`deltachat.account.Account` or
:class:`deltachat.chat.Chat`.
:class:`deltachat.chatting.Chat`.
"""
def __init__(self, account, dc_msg):
self.account = account
@@ -57,6 +58,8 @@ class Message(object):
def set_text(self, text):
"""set text of this message. """
assert self.id > 0, "message not prepared"
assert self.is_out_preparing()
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
@props.with_doc
@@ -69,6 +72,19 @@ class Message(object):
mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type)
if not os.path.exists(path):
raise ValueError("path does not exist: {!r}".format(path))
blobdir = self.account.get_blobdir()
if not path.startswith(blobdir):
for i in range(50):
ext = "" if i == 0 else "-" + str(i)
dest = os.path.join(blobdir, os.path.basename(path) + ext)
if os.path.exists(dest):
continue
shutil.copyfile(path, dest)
break
else:
raise ValueError("could not create blobdir-path for {}".format(path))
path = dest
assert path.startswith(blobdir), path
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
@props.with_doc
@@ -85,18 +101,6 @@ class Message(object):
""" return True if this message is a setup message. """
return lib.dc_msg_is_setupmessage(self._dc_msg)
def get_setupcodebegin(self):
""" return the first characters of a setup code in a setup message. """
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
def is_encrypted(self):
""" return True if this message was encrypted. """
return bool(lib.dc_msg_get_showpadlock(self._dc_msg))
def is_forwarded(self):
""" return True if this message was forwarded. """
return bool(lib.dc_msg_is_forwarded(self._dc_msg))
def get_message_info(self):
""" Return informational text for a single message.
@@ -146,25 +150,25 @@ class Message(object):
if mime_headers:
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
if isinstance(s, bytes):
return email.message_from_bytes(s)
s = s.decode("ascii")
return email.message_from_string(s)
@property
def chat(self):
"""chat this message was posted in.
:returns: :class:`deltachat.chat.Chat` object
:returns: :class:`deltachat.chatting.Chat` object
"""
from .chat import Chat
from .chatting import Chat
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
return Chat(self.account, chat_id)
def get_sender_contact(self):
"""return the contact of who wrote the message.
:returns: :class:`deltachat.chat.Contact` instance
:returns: :class:`deltachat.chatting.Contact` instance
"""
from .contact import Contact
from .chatting import Contact
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return Contact(self._dc_context, contact_id)
@@ -174,7 +178,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

@@ -11,18 +11,27 @@ class ProviderNotFoundError(Exception):
class Provider(object):
"""Provider information.
:param domain: The email to get the provider info for.
:param domain: The domain to get the provider info for, this is
normally the part following the `@` of the domain.
"""
def __init__(self, account, addr):
def __init__(self, domain):
provider = ffi.gc(
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
lib.dc_provider_new_from_domain(as_dc_charpointer(domain)),
lib.dc_provider_unref,
)
if provider == ffi.NULL:
raise ProviderNotFoundError("Provider not found")
self._provider = provider
@classmethod
def from_email(cls, email):
"""Create provider info from an email address.
:param email: Email address to get provider info for.
"""
return cls(email.split('@')[-1])
@property
def overview_page(self):
"""URL to the overview page of the provider on providers.delta.chat."""
@@ -30,10 +39,21 @@ class Provider(object):
lib.dc_provider_get_overview_page(self._provider))
@property
def get_before_login_hints(self):
"""Should be shown to the user on login."""
def name(self):
"""The name of the provider."""
return from_dc_charpointer(lib.dc_provider_get_name(self._provider))
@property
def markdown(self):
"""Content of the information page, formatted as markdown."""
return from_dc_charpointer(
lib.dc_provider_get_before_login_hints(self._provider))
lib.dc_provider_get_markdown(self._provider))
@property
def status_date(self):
"""The date the provider info was last updated, as a string."""
return from_dc_charpointer(
lib.dc_provider_get_status_date(self._provider))
@property
def status(self):

View File

@@ -1,11 +1,9 @@
from __future__ import print_function
import os
import py
import pytest
import requests
import time
from deltachat import Account
from deltachat import const
from deltachat.capi import lib
import tempfile
@@ -16,29 +14,16 @@ def pytest_addoption(parser):
help="a file with >=2 lines where each line "
"contains NAME=VALUE config settings for one account"
)
parser.addoption(
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
def pytest_configure(config):
config.addinivalue_line(
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
)
cfg = config.getoption('--liveconfig')
if not cfg:
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
cfg = os.getenv('DCC_PY_LIVECONFIG')
if cfg:
config.option.liveconfig = cfg
def pytest_runtest_setup(item):
if (list(item.iter_markers(name="ignored"))
and not item.config.getoption("ignored")):
pytest.skip("Ignored tests not requested, use --ignored")
def pytest_report_header(config, startdir):
summary = []
@@ -97,22 +82,18 @@ class SessionLiveConfigFromFile:
class SessionLiveConfigFromURL:
def __init__(self, url):
def __init__(self, url, create_token):
self.configlist = []
self.url = url
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url)
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)
@@ -123,24 +104,14 @@ def session_liveconfig(request):
liveconfig_opt = request.config.option.liveconfig
if liveconfig_opt:
if liveconfig_opt.startswith("http"):
return SessionLiveConfigFromURL(liveconfig_opt)
url, create_token = liveconfig_opt.split("#", 1)
return SessionLiveConfigFromURL(url, create_token)
else:
return SessionLiveConfigFromFile(liveconfig_opt)
@pytest.fixture(scope='session')
def datadir():
"""The py.path.local object of the test-data/ directory."""
for path in reversed(py.path.local(__file__).parts()):
datadir = path.join('test-data')
if datadir.isdir():
return datadir
else:
pytest.skip('test-data directory not found')
@pytest.fixture
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
class AccountMaker:
def __init__(self):
@@ -148,8 +119,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
self.offline_count = 0
self._finalizers = []
self.init_time = time.time()
self._generated_keys = ["alice", "bob", "charlie",
"dom", "elena", "fiona"]
def finalize(self):
while self._finalizers:
@@ -169,65 +138,33 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
ac._evlogger.set_timeout(2)
return ac
def _preconfigure_key(self, account, addr):
# Only set a key if we haven't used it yet for another account.
if self._generated_keys:
keyname = self._generated_keys.pop(0)
fname_pub = "key/{name}-public.asc".format(name=keyname)
fname_sec = "key/{name}-secret.asc".format(name=keyname)
account._preconfigure_keypair(addr,
datadir.join(fname_pub).read(),
datadir.join(fname_sec).read())
def get_configured_offline_account(self):
ac = self.get_unconfigured_account()
# do a pseudo-configured account
addr = "addr{}@offline.org".format(self.offline_count)
ac.set_config("addr", addr)
self._preconfigure_key(ac, addr)
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
ac.set_config("mail_pw", "123")
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
lib.dc_set_config(ac._dc_context, b"configured", b"1")
return ac
def get_online_config(self, pre_generated_key=True):
def get_online_configuring_account(self):
if not session_liveconfig:
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
configdict = session_liveconfig.get(self.live_count)
self.live_count += 1
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False,
pre_generated_key=True, config={}):
ac, configdict = self.get_online_config(
pre_generated_key=pre_generated_key)
configdict.update(config)
ac.configure(**configdict)
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
ac.start_threads()
return ac
def get_one_online_account(self, pre_generated_key=True):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key)
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
return ac1
def get_two_online_accounts(self):
ac1 = self.get_online_configuring_account()
ac2 = self.get_online_configuring_account()
@@ -237,12 +174,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
wait_configuration_progress(ac2, 1000)
return ac1, ac2
def clone_online_account(self, account, pre_generated_key=True):
def clone_online_account(self, account):
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, account.get_config("addr"))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
@@ -271,13 +206,12 @@ def lp():
return Printer()
def wait_configuration_progress(account, min_target, max_target=1001):
min_target = min(min_target, max_target)
def wait_configuration_progress(account, target):
while 1:
evt_name, data1, data2 = \
account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if data1 >= min_target and data1 <= max_target:
print("** CONFIG PROGRESS {}".format(min_target), account)
if data1 >= target:
print("** CONFIG PROGRESS {}".format(target), account)
break

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because it is too large Load Diff

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)
@@ -100,13 +57,13 @@ class TestOnlineInCreation:
lp.sec("wait1 for original or forwarded messages to arrive")
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev1[1] >= const.DC_CHAT_ID_LAST_SPECIAL
received_original = ac2.get_message_by_id(ev1[2])
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] >= 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

@@ -41,7 +41,7 @@ def test_dc_close_events(tmpdir):
else:
print("skipping event", *ev)
find("disconnecting inbox-thread")
find("disconnecting INBOX-watch")
find("disconnecting sentbox-thread")
find("disconnecting mvbox-thread")
find("disconnecting SMTP")
@@ -59,16 +59,6 @@ def test_wrong_db(tmpdir):
assert not lib.dc_open(dc_context, p.strpath.encode("ascii"), ffi.NULL)
def test_empty_blobdir(tmpdir):
# Apparently some client code expects this to be the same as passing NULL.
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("hello.db")
assert lib.dc_open(ctx, db_fname.strpath.encode("ascii"), b"")
def test_event_defines():
assert const.DC_EVENT_INFO == 100
assert const.DC_CONTACT_ID_SELF
@@ -95,56 +85,16 @@ def test_markseen_invalid_message_ids(acfactory):
ac1._evlogger.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
def test_get_special_message_id_returns_empty_message(acfactory):
ac1 = acfactory.get_configured_offline_account()
for i in range(1, 10):
msg = ac1.get_message_by_id(i)
assert msg.id == 0
def test_provider_info():
provider = lib.dc_provider_new_from_email(cutil.as_dc_charpointer("ex@example.com"))
assert cutil.from_dc_charpointer(
lib.dc_provider_get_overview_page(provider)
) == "https://providers.delta.chat/example.com"
assert cutil.from_dc_charpointer(lib.dc_provider_get_name(provider)) == "Example"
assert cutil.from_dc_charpointer(lib.dc_provider_get_markdown(provider)) == "\n..."
assert cutil.from_dc_charpointer(lib.dc_provider_get_status_date(provider)) == "2018-09"
assert lib.dc_provider_get_status(provider) == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_closed():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
assert 'deltachat_core_version' in info
assert 'database_dir' not in info
def test_get_info_open(tmpdir):
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("test.db")
lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
assert 'deltachat_core_version' in info
assert 'database_dir' in info
def test_is_open_closed():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_is_open(ctx) == 0
def test_is_open_actually_open(tmpdir):
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("test.db")
lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL)
assert lib.dc_is_open(ctx) == 1
assert lib.dc_provider_new_from_email(cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL

View File

@@ -0,0 +1,27 @@
import pytest
from deltachat import const
from deltachat import provider
def test_provider_info_from_email():
example = provider.Provider.from_email("email@example.com")
assert example.overview_page == "https://providers.delta.chat/example.com"
assert example.name == "Example"
assert example.markdown == "\n..."
assert example.status_date == "2018-09"
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_from_domain():
example = provider.Provider("example.com")
assert example.overview_page == "https://providers.delta.chat/example.com"
assert example.name == "Example"
assert example.markdown == "\n..."
assert example.status_date == "2018-09"
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
with pytest.raises(provider.ProviderNotFoundError):
provider.Provider.from_email("email@unexistent.no")

View File

@@ -1,21 +1,19 @@
[tox]
# make sure to update environment list in travis.yml and appveyor.yml
envlist =
py37
py35
lint
auditwheels
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs:tests}
pytest -v -rsXx {posargs:tests}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
DCC_RS_DEV
DCC_RS_TARGET
DCC_PY_LIVECONFIG
CARGO_TARGET_DIR
RUSTC_WRAPPER
deps =
pytest
pytest-rerunfailures
@@ -30,9 +28,10 @@ deps = auditwheel
commands =
python tests/auditwheels.py {toxworkdir}/wheelhouse
[testenv:lint]
skipsdist = True
skip_install = True
usedevelop = True
deps =
flake8
# pygments required by rst-lint
@@ -44,32 +43,22 @@ commands =
rst-lint --encoding 'utf-8' README.rst
[testenv:doc]
changedir=doc
basepython = python3.5
deps =
sphinx==2.2.0
breathe
changedir = doc
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
[testenv:lintdoc]
skipsdist = True
usedevelop = True
deps =
{[testenv:lint]deps}
{[testenv:doc]deps}
commands =
{[testenv:lint]commands}
{[testenv:doc]commands}
sphinx-build -w docker-toxdoc-warnings.log -b html . _build/html
[pytest]
addopts = -v -ra --strict-markers
python_files = tests/test_*.py
addopts = -v -rs --reruns 3 --reruns-delay 2
python_files = tests/test_*.py
norecursedirs = .tox
xfail_strict=true
timeout = 90
timeout = 60
timeout_method = thread
[flake8]

View File

@@ -23,7 +23,7 @@ if [ $? != 0 ]; then
fi
pushd python
if [ -e "./liveconfig" -a -z "$DCC_PY_LIVECONFIG" ]; then
if [ -e "./liveconfig" && -z "$DCC_PY_LIVECONFIG" ]; then
export DCC_PY_LIVECONFIG=liveconfig
fi
tox "$@"

View File

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

View File

@@ -1,80 +0,0 @@
#!/usr/bin/env python3
# Examples:
#
# Original server that doesn't use SSL:
# ./proxy.py 8080 imap.nauta.cu 143
# ./proxy.py 8081 smtp.nauta.cu 25
#
# Original server that uses SSL:
# ./proxy.py 8080 testrun.org 993 --ssl
# ./proxy.py 8081 testrun.org 465 --ssl
from datetime import datetime
import argparse
import selectors
import ssl
import socket
import socketserver
class Proxy(socketserver.ThreadingTCPServer):
allow_reuse_address = True
def __init__(self, proxy_host, proxy_port, real_host, real_port, use_ssl):
self.real_host = real_host
self.real_port = real_port
self.use_ssl = use_ssl
super().__init__((proxy_host, proxy_port), RequestHandler)
class RequestHandler(socketserver.BaseRequestHandler):
def handle(self):
print('{} - {} CONNECTED.'.format(datetime.now(), self.client_address))
total = 0
real_server = (self.server.real_host, self.server.real_port)
with socket.create_connection(real_server) as sock:
if self.server.use_ssl:
context = ssl.create_default_context()
sock = context.wrap_socket(
sock, server_hostname=real_server[0])
forward = {self.request: sock, sock: self.request}
sel = selectors.DefaultSelector()
sel.register(self.request, selectors.EVENT_READ,
self.client_address)
sel.register(sock, selectors.EVENT_READ, real_server)
active = True
while active:
events = sel.select()
for key, mask in events:
print('\n{} - {} wrote:'.format(datetime.now(), key.data))
data = key.fileobj.recv(1024)
received = len(data)
total += received
print(data)
print('{} Bytes\nTotal: {} Bytes'.format(received, total))
if data:
forward[key.fileobj].sendall(data)
else:
print('\nCLOSING CONNECTION.\n\n')
forward[key.fileobj].close()
key.fileobj.close()
active = False
if __name__ == '__main__':
p = argparse.ArgumentParser(description='Simple Python Proxy')
p.add_argument(
"proxy_port", help="the port where the proxy will listen", type=int)
p.add_argument('host', help="the real host")
p.add_argument('port', help="the port of the real host", type=int)
p.add_argument("--ssl", help="use ssl to connect to the real host",
action="store_true")
args = p.parse_args()
with Proxy('', args.proxy_port, args.host, args.port, args.ssl) as proxy:
proxy.serve_forever()

View File

@@ -1,65 +0,0 @@
#!/usr/bin/env python
import os
import sys
import re
import pathlib
import subprocess
rex = re.compile(r'version = "(\S+)"')
def read_toml_version(relpath):
p = pathlib.Path(relpath)
assert p.exists()
for line in open(str(p)):
m = rex.match(line)
if m is not None:
return m.group(1)
raise ValueError("no version found in {}".format(relpath))
def replace_toml_version(relpath, newversion):
p = pathlib.Path(relpath)
assert p.exists()
tmp_path = str(p) + "_tmp"
with open(tmp_path, "w") as f:
for line in open(str(p)):
m = rex.match(line)
if m is not None:
f.write('version = "{}"\n'.format(newversion))
else:
f.write(line)
os.rename(tmp_path, str(p))
if __name__ == "__main__":
if len(sys.argv) < 2:
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
print("{}: {}".format(x, read_toml_version(x)))
raise SystemExit("need argument: new version, example: 1.25.0")
newversion = sys.argv[1]
if newversion.count(".") < 2:
raise SystemExit("need at least two dots in version")
core_toml = read_toml_version("Cargo.toml")
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
assert core_toml == ffi_toml, (core_toml, ffi_toml)
for line in open("CHANGELOG.md"):
## 1.25.0
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break
else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
replace_toml_version("Cargo.toml", newversion)
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
subprocess.call(["cargo", "check"])
subprocess.call(["git", "add", "-u"])
# subprocess.call(["cargo", "update", "-p", "deltachat"])
print("after commit make sure to: ")
print("")
print(" git tag {}".format(newversion))
print("")

153
spec.md
View File

@@ -1,6 +1,6 @@
# Chat-over-Email specification
Version 0.30.0
Version 0.19.0
This document describes how emails can be used
to implement typical messenger functions
@@ -17,9 +17,6 @@ while staying compatible to existing MUAs.
- [Change group name](#change-group-name)
- [Set group image](#set-group-image)
- [Set profile image](#set-profile-image)
- [Locations](#locations)
- [User locations](#user-locations)
- [Points of interest](#points-of-interest)
- [Miscellaneous](#miscellaneous)
@@ -32,7 +29,8 @@ Messages SHOULD be encrypted by the
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
by the [Memoryhole](https://github.com/autocrypt/memoryhole) standard.
If Memoryhole is not used,
the subject of encrypted messages SHOULD be replaced by the string `...`.
the subject of encrypted messages SHOULD be replaced by the string
`Chat: Encrypted message` where the part after the colon MAY be localized.
# Outgoing messages
@@ -115,13 +113,11 @@ but MUAs typically expose the sender in the UI.
Groups are chats with usually more than one recipient,
each defined by an email-address.
The sender plus the recipients are the group members.
All group members form the member list.
To allow different groups with the same members,
groups are identified by a group-id.
The group-id MUST be created only from the characters
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`
and MUST have a length of at least 11 characters.
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`.
Groups MUST have a group-name.
The group-name is any non-zero-length UTF-8 string.
@@ -138,7 +134,8 @@ The group-name MUST be written to `Chat-Group-Name` header
to join a group chat on a second device any time).
The `Subject` header of outgoing group messages
SHOULD be set to the group-name.
SHOULD start with the characters `Chat:`
followed by the group-name and a colon followed by an excerpt of the message.
To identify the group-id on replies from normal MUAs,
the group-id MUST also be added to the message-id of outgoing messages.
@@ -147,16 +144,16 @@ The message-id MUST have the format `Gr.<group-id>.<unique data>`.
From: member1@domain
To: member2@domain, member3@domain
Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ
Chat-Group-ID: 1234xyZ
Chat-Group-Name: My Group
Message-ID: Gr.12345uvwxyZ.0001@domain
Message-ID: Gr.1234xyZ.0001@domain
Subject: Chat: My Group: Hello group ...
Hello group - this group contains three members
Messengers adding the member list in the form `Name <email-address>`
MUST take care only to 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).
@@ -179,22 +176,12 @@ to a normal single-user chat with the email-address given in `From`.
## Add and remove members
Messenger clients MUST init the member list
from the `From`/`To` headers on the first group message.
When a member is added later,
a `Chat-Group-Member-Added` action header must be sent
with the value set to the email-address of the added member.
When receiving a `Chat-Group-Member-Added` header, however,
_all missing_ members the `From`/`To` headers has to be added.
This is to mitigate problems when receiving messages
in different orders, esp. on creating new groups.
To remove a member, a `Chat-Group-Member-Removed` header must be sent
with the value set to the email-address of the member to remove.
When receiving a `Chat-Group-Member-Removed` header,
only exaxtly the given member has to be removed from the member list.
Messenger clients MUST construct the member list
from the `From`/`To` headers only on the first group message
or if they see a `Chat-Group-Member-Added`
or `Chat-Group-Member-Removed` action header.
Both headers MUST have the email-address
of the added or removed member as the value.
Messenger clients MUST NOT construct the member list
on other group messages
(this is to avoid accidentally altered To-lists in normal MUAs;
@@ -209,10 +196,10 @@ and the message SHOULD appear as a message or action from the sender.
From: member1@domain
To: member2@domain, member3@domain, member4@domain
Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ
Chat-Group-ID: 1234xyZ
Chat-Group-Name: My Group
Chat-Group-Member-Added: member4@domain
Message-ID: Gr.12345uvwxyZ.0002@domain
Message-ID: Gr.1234xyZ.0002@domain
Subject: Chat: My Group: Hello, ...
Hello, I've added member4@domain to our group. Now we have 4 members.
@@ -222,10 +209,10 @@ To remove a member:
From: member1@domain
To: member2@domain, member3@domain
Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ
Chat-Group-ID: 1234xyZ
Chat-Group-Name: My Group
Chat-Group-Member-Removed: member4@domain
Message-ID: Gr.12345uvwxyZ.0003@domain
Message-ID: Gr.1234xyZ.0003@domain
Subject: Chat: My Group: Hello, ...
Hello, I've removed member4@domain from our group. Now we have 3 members.
@@ -246,10 +233,10 @@ and the message SHOULD appear as a message or action from the sender.
From: member1@domain
To: member2@domain, member3@domain
Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ
Chat-Group-ID: 1234xyZ
Chat-Group-Name: Our Group
Chat-Group-Name-Changed: My Group
Message-ID: Gr.12345uvwxyZ.0004@domain
Message-ID: Gr.1234xyZ.0004@domain
Subject: Chat: Our Group: Hello, ...
Hello, I've changed the group name from "My Group" to "Our Group".
@@ -260,11 +247,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
@@ -275,10 +262,10 @@ and the message SHOULD appear as a message or action from the sender.
From: member1@domain
To: member2@domain, member3@domain
Chat-Version: 1.0
Chat-Group-ID: 12345uvwxyZ
Chat-Group-ID: 1234xyZ
Chat-Group-Name: Our Group
Chat-Group-Avatar: image.jpg
Message-ID: Gr.12345uvwxyZ.0005@domain
Chat-Group-Image: image.jpg
Message-ID: Gr.1234xyZ.0005@domain
Subject: Chat: Our Group: Hello, ...
Content-Type: multipart/mixed; boundary="==break=="
@@ -295,25 +282,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.
@@ -321,7 +308,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=="
@@ -337,74 +324,18 @@ 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.
# Locations
Locations can be attachted to messages using
[standard kml-files](https://www.opengeospatial.org/standards/kml/)
with well-known names.
## User locations
To send the location of the sender,
the app can attach a file with the name `location.kml`.
The file can contain one or more locations.
Apps that support location streaming will typically collect some location events
and send them together in one file.
As each location has an independent timestamp,
the apps can show the location as a track.
Note that the `addr` attribute inside the `location.kml` file
MUST match the users email-address.
Otherwise, the file is discarded silently;
this is to protect against getting wrong locations,
eg. forwarded from a normal MUA.
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document addr="ndh@deltachat.de">
<Placemark>
<Timestamp><when>2020-01-11T20:40:19Z</when></Timestamp>
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
</Placemark>
<Placemark>
<Timestamp><when>2020-01-11T20:40:25Z</when></Timestamp>
<Point><coordinates accuracy="5.4">7.654,3.21</coordinates></Point>
</Placemark>
</Document>
</kml>
## Points of interest
To send an "Point of interest", a POI,
use a normal message and attach a file with the name `message.kml`.
In contrast to user locations, this file should contain only one location
and an address-attribute is not needed -
as the location belongs to the message content,
it is fine if the location is detected on forwarding etc.
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<Timestamp><when>2020-01-01T20:40:19Z</when></Timestamp>
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
</Placemark>
</Document>
</kml>
# Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual.
Messengers SHOULD use the header `Chat-Predecessor`
instead of `In-Reply-To` as the latter one results
in infinite threads on typical MUAs.
Messengers SHOULD add a `Chat-Voice-message: 1` header
if an attached audio file is a voice message.
@@ -414,7 +345,7 @@ to specify the duration of attached audio or video files.
The value MUST be the duration in milliseconds.
This allows the receiver to show the time without knowing the file format.
In-Reply-To: Gr.12345uvwxyZ.0005@domain
Chat-Predecessor: foo123@domain
Chat-Voice-Message: 1
Chat-Duration: 10000
@@ -438,4 +369,4 @@ as the sending time of the message as indicated by its Date header,
or the time of first receipt if that date is in the future or unavailable.
Copyright © 2017-2020 Delta Chat contributors.
Copyright © 2017-2019 Delta Chat contributors.

View File

@@ -1,15 +1,13 @@
//! # Autocrypt header module
//!
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
use std::collections::BTreeMap;
use std::ffi::CStr;
use std::str::FromStr;
use std::{fmt, str};
use mmime::mailimf_types::*;
use crate::constants::*;
use crate::contact::*;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, SignedPublicKey};
use crate::key::*;
/// Possible values for encryption preference
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
@@ -42,27 +40,22 @@ impl str::FromStr for EncryptPreference {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mutual" => Ok(EncryptPreference::Mutual),
"nopreference" => Ok(EncryptPreference::NoPreference),
_ => Err(()),
"reset" => Ok(EncryptPreference::Reset),
_ => Ok(EncryptPreference::NoPreference),
}
}
}
/// Autocrypt header
/// Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
#[derive(Debug)]
pub struct Aheader {
pub addr: String,
pub public_key: SignedPublicKey,
pub public_key: Key,
pub prefer_encrypt: EncryptPreference,
}
impl Aheader {
/// Creates new autocrypt header
pub fn new(
addr: String,
public_key: SignedPublicKey,
prefer_encrypt: EncryptPreference,
) -> Self {
pub fn new(addr: String, public_key: Key, prefer_encrypt: EncryptPreference) -> Self {
Aheader {
addr,
public_key,
@@ -70,52 +63,66 @@ impl Aheader {
}
}
pub fn from_headers(
context: &Context,
wanted_from: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Option<Self> {
if let Ok(Some(value)) = headers.get_header_value(HeaderDef::Autocrypt) {
match Self::from_str(&value) {
Ok(header) => {
if addr_cmp(&header.addr, wanted_from) {
return Some(header);
}
}
Err(err) => {
warn!(
context,
"found invalid autocrypt header {}: {:?}", value, err
);
}
}
pub fn from_imffields(wanted_from: &str, header: *const mailimf_fields) -> Option<Self> {
if header.is_null() {
return None;
}
None
let mut fine_header = None;
let mut cur = unsafe { (*(*header).fld_list).first };
while !cur.is_null() {
let field = unsafe { (*cur).data as *mut mailimf_field };
if !field.is_null()
&& unsafe { (*field).fld_type } == MAILIMF_FIELD_OPTIONAL_FIELD as libc::c_int
{
let optional_field = unsafe { (*field).fld_data.fld_optional_field };
if !optional_field.is_null()
&& unsafe { !(*optional_field).fld_name.is_null() }
&& unsafe { CStr::from_ptr((*optional_field).fld_name).to_str().unwrap() }
== "Autocrypt"
{
let value = unsafe {
CStr::from_ptr((*optional_field).fld_value)
.to_str()
.unwrap()
};
match Self::from_str(value) {
Ok(test) => {
if addr_cmp(&test.addr, wanted_from) {
if fine_header.is_none() {
fine_header = Some(test);
} else {
// TODO: figure out what kind of error case this is
return None;
}
}
}
_ => {}
}
}
}
cur = unsafe { (*cur).next };
}
fine_header
}
}
impl fmt::Display for Aheader {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "addr={};", self.addr)?;
if self.prefer_encrypt == EncryptPreference::Mutual {
write!(fmt, " prefer-encrypt=mutual;")?;
}
// adds a whitespace every 78 characters, this allows
// email crate to wrap the lines according to RFC 5322
// TODO replace 78 with enum /rtn
// adds a whitespace every 78 characters, this allows libEtPan to
// wrap the lines according to RFC 5322
// (which may insert a linebreak before every whitespace)
let keydata = self.public_key.to_base64().chars().enumerate().fold(
String::new(),
|mut res, (i, c)| {
if i % 78 == 78 - "keydata=".len() {
res.push(' ')
}
res.push(c);
res
},
);
write!(fmt, " keydata={}", keydata)
let keydata = self.public_key.to_base64(78);
write!(
fmt,
"addr={}; prefer-encrypt={}; keydata={}",
self.addr, self.prefer_encrypt, keydata
)
}
}
@@ -124,9 +131,9 @@ impl str::FromStr for Aheader {
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut attributes: BTreeMap<String, String> = s
.split(';')
.split(";")
.filter_map(|a| {
let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
let attribute: Vec<&str> = a.trim().splitn(2, "=").collect();
if attribute.len() < 2 {
return None;
}
@@ -144,20 +151,34 @@ impl str::FromStr for Aheader {
return Err(());
}
};
let public_key: SignedPublicKey = attributes
.remove("keydata")
.ok_or(())
.and_then(|raw| SignedPublicKey::from_base64(&raw).or(Err(())))
.and_then(|key| key.verify().and(Ok(key)).or(Err(())))?;
let prefer_encrypt = attributes
let public_key = match attributes
.remove("keydata")
.and_then(|raw| Key::from_base64(&raw, KeyType::Public))
{
Some(key) => {
if key.verify() {
key
} else {
return Err(());
}
}
None => {
return Err(());
}
};
let prefer_encrypt = match attributes
.remove("prefer-encrypt")
.and_then(|raw| raw.parse().ok())
.unwrap_or_default();
{
Some(pref) => pref,
None => EncryptPreference::NoPreference,
};
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
// Autocrypt-Level0: unknown attribute, treat the header as invalid
if attributes.keys().any(|k| !k.starts_with('_')) {
if attributes.keys().find(|k| !k.starts_with("_")).is_some() {
return Err(());
}
@@ -173,13 +194,15 @@ impl str::FromStr for Aheader {
mod tests {
use super::*;
const RAWKEY: &str = "xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=";
fn rawkey() -> String {
"xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=".into()
}
#[test]
fn test_from_str() {
let h: Aheader = format!(
"addr=me@mail.com; prefer-encrypt=mutual; keydata={}",
RAWKEY
rawkey()
)
.parse()
.expect("failed to parse");
@@ -188,22 +211,9 @@ mod tests {
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
}
// EncryptPreference::Reset is an internal value, parser should never return it
#[test]
fn test_from_str_reset() {
let raw = format!(
"addr=reset@example.com; prefer-encrypt=reset; keydata={}",
RAWKEY
);
let h: Aheader = raw.parse().expect("failed to parse");
assert_eq!(h.addr, "reset@example.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
}
#[test]
fn test_from_str_non_critical() {
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={}", RAWKEY);
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={}", rawkey());
let h: Aheader = raw.parse().expect("failed to parse");
assert_eq!(h.addr, "me@mail.com");
@@ -214,57 +224,33 @@ mod tests {
fn test_from_str_superflous_critical() {
let raw = format!(
"addr=me@mail.com; _foo=one; _bar=two; other=me; keydata={}",
RAWKEY
rawkey()
);
assert!(raw.parse::<Aheader>().is_err());
}
#[test]
fn test_good_headers() {
let fixed_header = concat!(
"addr=a@b.example.org; prefer-encrypt=mutual; ",
"keydata=xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJg",
" WL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6",
" CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKK",
" bhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1Kv",
" VL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbG",
" UuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaK",
" rc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087",
" LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VN",
" HtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Ddd",
" fxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCv",
" SJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vau",
" f1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+",
" G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjm",
" kRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09",
" /JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHR",
" TR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaK",
" rc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivX",
" urm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9Mtrm",
" ZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb",
" +F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNg",
" wm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc="
);
let fixed_header = "addr=a@b.example.org; prefer-encrypt=mutual; keydata=xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g 4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8 ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu88 80iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTll HOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+ws CJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiML AAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiF Nyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAg dLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72 rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v 81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgC u3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOt kb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKC LhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4s WVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWv BuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiML AAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGr wdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktE k6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0 j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7x egRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=";
let ah = Aheader::from_str(fixed_header).expect("failed to parse");
assert_eq!(ah.addr, "a@b.example.org");
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
assert_eq!(format!("{}", ah), fixed_header);
let rendered = ah.to_string();
assert_eq!(rendered, fixed_header);
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", RAWKEY)).expect("failed to parse");
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", rawkey())).expect("failed to parse");
assert_eq!(ah.addr, "a@b.example.org");
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
Aheader::from_str(&format!(
"addr=a@b.example.org; prefer-encrypt=ignoreUnknownValues; keydata={}",
RAWKEY
rawkey()
))
.expect("failed to parse");
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", RAWKEY))
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", rawkey()))
.expect("failed to parse");
}
@@ -276,30 +262,4 @@ mod tests {
assert!(Aheader::from_str(" ;;").is_err());
assert!(Aheader::from_str("addr=a@t.de; unknwon=1; keydata=jau").is_err());
}
#[test]
fn test_display_aheader() {
assert!(format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
)
.contains("prefer-encrypt=mutual;"));
// According to Autocrypt Level 1 specification,
// only "prefer-encrypt=mutual;" can be used.
// If the setting is nopreference, the whole attribute is omitted.
assert!(!format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::NoPreference
)
)
.contains("prefer-encrypt"));
}
}

View File

@@ -1,640 +0,0 @@
//! # Blob directory management
use std::ffi::OsStr;
use std::fmt;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use self::image::GenericImageView;
use crate::constants::AVATAR_SIZE;
use crate::context::Context;
use crate::events::Event;
extern crate image;
/// Represents a file in the blob directory.
///
/// The object has a name, which will always be valid UTF-8. Having a
/// blob object does not imply the respective file exists, however
/// when using one of the `create*()` methods a unique file is
/// created.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlobObject<'a> {
blobdir: &'a Path,
name: String,
}
impl<'a> BlobObject<'a> {
/// Creates a new blob object with a unique name.
///
/// Creates a new file in the blob directory. The name will be
/// derived from the platform-agnostic basename of the suggested
/// name, followed by a random number and followed by a possible
/// extension. The `data` will be written into the file without
/// race-conditions.
///
/// # Errors
///
/// [BlobError::CreateFailure] is used when the file could not
/// be created. You can expect [BlobError.cause] to contain an
/// underlying error.
///
/// [BlobError::WriteFailure] is used when the file could not
/// be written to. You can expect [BlobError.cause] to contain an
/// underlying error.
pub fn create(
context: &'a Context,
suggested_name: impl AsRef<str>,
data: &[u8],
) -> std::result::Result<BlobObject<'a>, BlobError> {
let blobdir = context.get_blobdir();
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext)?;
file.write_all(data)
.map_err(|err| BlobError::WriteFailure {
blobdir: blobdir.to_path_buf(),
blobname: name.clone(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
let blob = BlobObject {
blobdir,
name: format!("$BLOBDIR/{}", name),
};
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
// Creates a new file, returning a tuple of the name and the handle.
fn create_new_file(dir: &Path, stem: &str, ext: &str) -> Result<(String, fs::File), BlobError> {
let max_attempt = 15;
let mut name = format!("{}{}", stem, ext);
for attempt in 0..max_attempt {
let path = dir.join(&name);
match fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&path)
{
Ok(file) => return Ok((name, file)),
Err(err) => {
if attempt == max_attempt {
return Err(BlobError::CreateFailure {
blobdir: dir.to_path_buf(),
blobname: name,
cause: err,
backtrace: failure::Backtrace::new(),
});
} else {
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
}
}
}
}
// This is supposed to be unreachable, but the compiler doesn't know.
Err(BlobError::CreateFailure {
blobdir: dir.to_path_buf(),
blobname: name,
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
backtrace: failure::Backtrace::new(),
})
}
/// Creates a new blob object with unique name by copying an existing file.
///
/// This creates a new blob as described in [BlobObject::create]
/// but also copies an existing file into it. This is done in a
/// in way which avoids race-conditions when multiple files are
/// concurrently created.
///
/// # Errors
///
/// In addition to the errors in [BlobObject::create] the
/// [BlobError::CopyFailure] is used when the data can not be
/// copied.
pub fn create_and_copy(
context: &'a Context,
src: impl AsRef<Path>,
) -> std::result::Result<BlobObject<'a>, BlobError> {
let mut src_file = fs::File::open(src.as_ref()).map_err(|err| BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: String::from(""),
src: src.as_ref().to_path_buf(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?;
let name_for_err = name.clone();
std::io::copy(&mut src_file, &mut dst_file).map_err(|err| {
{
// Attempt to remove the failed file, swallow errors resulting from that.
let path = context.get_blobdir().join(&name_for_err);
fs::remove_file(path).ok();
}
BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: name_for_err,
src: src.as_ref().to_path_buf(),
cause: err,
backtrace: failure::Backtrace::new(),
}
})?;
let blob = BlobObject {
blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{}", name),
};
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
/// Creates a blob from a file, possibly copying it to the blobdir.
///
/// If the source file is not a path to into the blob directory
/// the file will be copied into the blob directory first. If the
/// source file is already in the blobdir it will not be copied
/// and only be created if it is a valid blobname, that is no
/// subdirectory is used and [BlobObject::sanitise_name] does not
/// modify the filename.
///
/// # Errors
///
/// This merely delegates to the [BlobObject::create_and_copy] and
/// the [BlobObject::from_path] methods. See those for possible
/// errors.
pub fn new_from_path(
context: &Context,
src: impl AsRef<Path>,
) -> std::result::Result<BlobObject, BlobError> {
if src.as_ref().starts_with(context.get_blobdir()) {
BlobObject::from_path(context, src)
} else {
BlobObject::create_and_copy(context, src)
}
}
/// Returns a [BlobObject] for an existing blob from a path.
///
/// The path must designate a file directly in the blobdir and
/// must use a valid blob name. That is after sanitisation the
/// name must still be the same, that means it must be valid UTF-8
/// and not have any special characters in it.
///
/// # Errors
///
/// [BlobError::WrongBlobdir] is used if the path is not in
/// the blob directory.
///
/// [BlobError::WrongName] is used if the file name does not
/// remain identical after sanitisation.
pub fn from_path(
context: &Context,
path: impl AsRef<Path>,
) -> std::result::Result<BlobObject, BlobError> {
let rel_path = path
.as_ref()
.strip_prefix(context.get_blobdir())
.map_err(|_| BlobError::WrongBlobdir {
blobdir: context.get_blobdir().to_path_buf(),
src: path.as_ref().to_path_buf(),
backtrace: failure::Backtrace::new(),
})?;
if !BlobObject::is_acceptible_blob_name(&rel_path) {
return Err(BlobError::WrongName {
blobname: path.as_ref().to_path_buf(),
backtrace: failure::Backtrace::new(),
});
}
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
blobname: path.as_ref().to_path_buf(),
backtrace: failure::Backtrace::new(),
})?;
BlobObject::from_name(context, name.to_string())
}
/// Returns a [BlobObject] for an existing blob.
///
/// The `name` may optionally be prefixed with the `$BLOBDIR/`
/// prefixed, as returned by [BlobObject::as_name]. This is how
/// you want to create a [BlobObject] for a filename read from the
/// database.
///
/// # Errors
///
/// [BlobError::WrongName] is used if the name is not a valid
/// blobname, i.e. if [BlobObject::sanitise_name] does modify the
/// provided name.
pub fn from_name(
context: &'a Context,
name: String,
) -> std::result::Result<BlobObject<'a>, BlobError> {
let name: String = match name.starts_with("$BLOBDIR/") {
true => name.splitn(2, '/').last().unwrap().to_string(),
false => name,
};
if !BlobObject::is_acceptible_blob_name(&name) {
return Err(BlobError::WrongName {
blobname: PathBuf::from(name),
backtrace: failure::Backtrace::new(),
});
}
Ok(BlobObject {
blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{}", name),
})
}
/// Returns the absolute path to the blob in the filesystem.
pub fn to_abs_path(&self) -> PathBuf {
let fname = Path::new(&self.name).strip_prefix("$BLOBDIR/").unwrap();
self.blobdir.join(fname)
}
/// Returns the blob name, as stored in the database.
///
/// This returns the blob in the `$BLOBDIR/<name>` format used in
/// the database. Do not use this unless you're about to store
/// this string in the database or [Params]. Eventually even
/// those conversions should be handled by the type system.
///
/// [Params]: crate::param::Params
pub fn as_name(&self) -> &str {
&self.name
}
/// Returns the filename of the blob.
pub fn as_file_name(&self) -> &str {
self.name.rsplitn(2, '/').next().unwrap()
}
/// The path relative in the blob directory.
pub fn as_rel_path(&self) -> &Path {
Path::new(self.as_file_name())
}
/// Returns the extension of the blob.
///
/// If a blob's filename has an extension, it is always guaranteed
/// to be lowercase.
pub fn suffix(&self) -> Option<&str> {
let ext = self.name.rsplitn(2, '.').next();
if ext == Some(&self.name) {
None
} else {
ext
}
}
/// Create a safe name based on a messy input string.
///
/// The safe name will be a valid filename on Unix and Windows and
/// not contain any path separators. The input can contain path
/// segments separated by either Unix or Windows path separators,
/// the rightmost non-empty segment will be used as name,
/// sanitised for special characters.
///
/// The resulting name is returned as a tuple, the first part
/// being the stem or basename and the second being an extension,
/// including the dot. E.g. "foo.txt" is returned as `("foo",
/// ".txt")` while "bar" is returned as `("bar", "")`.
///
/// The extension part will always be lowercased.
fn sanitise_name(name: &str) -> (String, String) {
let mut name = name.to_string();
for part in name.rsplit('/') {
if !part.is_empty() {
name = part.to_string();
break;
}
}
for part in name.rsplit('\\') {
if !part.is_empty() {
name = part.to_string();
break;
}
}
let opts = sanitize_filename::Options {
truncate: true,
windows: true,
replacement: "",
};
let clean = sanitize_filename::sanitize_with_options(name, opts);
let mut iter = clean.splitn(2, '.');
let 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())
}
}
/// Checks whether a name is a valid blob name.
///
/// This is slightly less strict than stanitise_name, presumably
/// someone already created a file with such a name so we just
/// ensure it's not actually a path in disguise is actually utf-8.
fn is_acceptible_blob_name(name: impl AsRef<OsStr>) -> bool {
let uname = match name.as_ref().to_str() {
Some(name) => name,
None => return false,
};
if uname.find('/').is_some() {
return false;
}
if uname.find('\\').is_some() {
return false;
}
if uname.find('\0').is_some() {
return false;
}
true
}
pub fn recode_to_avatar_size(&self, context: &Context) -> Result<(), BlobError> {
let blob_abs = self.to_abs_path();
let img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
return Ok(());
}
let img = img.thumbnail(AVATAR_SIZE, AVATAR_SIZE);
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
Ok(())
}
}
impl<'a> fmt::Display for BlobObject<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "$BLOBDIR/{}", self.name)
}
}
/// Errors for the [BlobObject].
#[derive(Fail, Debug)]
pub enum BlobError {
CreateFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
cause: std::io::Error,
backtrace: failure::Backtrace,
},
WriteFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
cause: std::io::Error,
backtrace: failure::Backtrace,
},
CopyFailure {
blobdir: PathBuf,
blobname: String,
src: PathBuf,
#[cause]
cause: std::io::Error,
backtrace: failure::Backtrace,
},
RecodeFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
cause: image::ImageError,
backtrace: failure::Backtrace,
},
WrongBlobdir {
blobdir: PathBuf,
src: PathBuf,
backtrace: failure::Backtrace,
},
WrongName {
blobname: PathBuf,
backtrace: failure::Backtrace,
},
}
// Implementing Display is done by hand because the failure
// #[fail(display = "...")] syntax does not allow using
// `blobdir.display()`.
impl fmt::Display for BlobError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Match on the data rather than kind, they are equivalent for
// identifying purposes but contain the actual data we need.
match &self {
BlobError::CreateFailure {
blobdir, blobname, ..
} => write!(
f,
"Failed to create blob {} in {}",
blobname,
blobdir.display()
),
BlobError::WriteFailure {
blobdir, blobname, ..
} => write!(
f,
"Failed to write data to blob {} in {}",
blobname,
blobdir.display()
),
BlobError::CopyFailure {
blobdir,
blobname,
src,
..
} => write!(
f,
"Failed to copy data from {} to blob {} in {}",
src.display(),
blobname,
blobdir.display(),
),
BlobError::RecodeFailure {
blobdir, blobname, ..
} => write!(f, "Failed to recode {} in {}", blobname, blobdir.display(),),
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
f,
"File path {} is not in blobdir {}",
src.display(),
blobdir.display(),
),
BlobError::WrongName { blobname, .. } => {
write!(f, "Blob has a bad name: {}", blobname.display(),)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
#[test]
fn test_create() {
let t = dummy_context();
let blob = BlobObject::create(&t.ctx, "foo", b"hello").unwrap();
let fname = t.ctx.get_blobdir().join("foo");
let data = fs::read(fname).unwrap();
assert_eq!(data, b"hello");
assert_eq!(blob.as_name(), "$BLOBDIR/foo");
assert_eq!(blob.to_abs_path(), t.ctx.get_blobdir().join("foo"));
}
#[test]
fn test_lowercase_ext() {
let t = dummy_context();
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello").unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
}
#[test]
fn test_as_file_name() {
let t = dummy_context();
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
assert_eq!(blob.as_file_name(), "foo.txt");
}
#[test]
fn test_as_rel_path() {
let t = dummy_context();
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
}
#[test]
fn test_suffix() {
let t = dummy_context();
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
assert_eq!(blob.suffix(), Some("txt"));
let blob = BlobObject::create(&t.ctx, "bar", b"world").unwrap();
assert_eq!(blob.suffix(), None);
}
#[test]
fn test_create_dup() {
let t = dummy_context();
BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
let foo_path = t.ctx.get_blobdir().join("foo.txt");
assert!(foo_path.exists());
BlobObject::create(&t.ctx, "foo.txt", b"world").unwrap();
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() {
let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).unwrap(), b"hello");
} else {
let name = fname.to_str().unwrap();
assert!(name.starts_with("foo"));
assert!(name.ends_with(".txt"));
}
}
}
#[test]
fn test_double_ext_preserved() {
let t = dummy_context();
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello").unwrap();
let foo_path = t.ctx.get_blobdir().join("foo.tar.gz");
assert!(foo_path.exists());
BlobObject::create(&t.ctx, "foo.tar.gz", b"world").unwrap();
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() {
let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).unwrap(), b"hello");
} else {
let name = fname.to_str().unwrap();
println!("{}", name);
assert!(name.starts_with("foo"));
assert!(name.ends_with(".tar.gz"));
}
}
}
#[test]
fn test_create_long_names() {
let t = dummy_context();
let s = "1".repeat(150);
let blob = BlobObject::create(&t.ctx, &s, b"data").unwrap();
let blobname = blob.as_name().split('/').last().unwrap();
assert!(blobname.len() < 128);
}
#[test]
fn test_create_and_copy() {
let t = dummy_context();
let src = t.dir.path().join("src");
fs::write(&src, b"boo").unwrap();
let blob = BlobObject::create_and_copy(&t.ctx, &src).unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/src");
let data = fs::read(blob.to_abs_path()).unwrap();
assert_eq!(data, b"boo");
let whoops = t.dir.path().join("whoops");
assert!(BlobObject::create_and_copy(&t.ctx, &whoops).is_err());
let whoops = t.ctx.get_blobdir().join("whoops");
assert!(!whoops.exists());
}
#[test]
fn test_create_from_path() {
let t = dummy_context();
let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/external");
let data = fs::read(blob.to_abs_path()).unwrap();
assert_eq!(data, b"boo");
let src_int = t.ctx.get_blobdir().join("internal");
fs::write(&src_int, b"boo").unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_int).unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
let data = fs::read(blob.to_abs_path()).unwrap();
assert_eq!(data, b"boo");
}
#[test]
fn test_create_from_name_long() {
let t = dummy_context();
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
assert_eq!(
blob.as_name(),
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
);
}
#[test]
fn test_is_blob_name() {
assert!(BlobObject::is_acceptible_blob_name("foo"));
assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
assert!(BlobObject::is_acceptible_blob_name("f".repeat(128)));
assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
}
#[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

@@ -1,12 +1,10 @@
//! # Chat list module
use crate::chat::*;
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
use crate::error::Result;
use crate::lot::Lot;
use crate::message::{Message, MessageState, MsgId};
use crate::message::*;
use crate::stock::StockMessage;
/// An object representing a single chatlist in memory.
@@ -36,7 +34,7 @@ use crate::stock::StockMessage;
#[derive(Debug)]
pub struct Chatlist {
/// Stores pairs of `chat_id, message_id`
ids: Vec<(ChatId, MsgId)>,
ids: Vec<(u32, u32)>,
}
impl Chatlist {
@@ -60,7 +58,7 @@ impl Chatlist {
/// or "Not now".
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
/// archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
/// archived _any_ chat using dc_archive_chat(). The UI should show a link as
/// "Show archived chats", if the user clicks this item, the UI should show a
/// list of all archived chats that can be created by this function hen using
/// the DC_GCL_ARCHIVED_ONLY flag.
@@ -71,7 +69,7 @@ impl Chatlist {
/// The `listflags` is a combination of flags:
/// - if the flag DC_GCL_ARCHIVED_ONLY is set, only archived chats are returned.
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
/// chats
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
@@ -88,12 +86,25 @@ impl Chatlist {
query: Option<&str>,
query_contact_id: Option<u32>,
) -> Result<Self> {
let mut add_archived_link_item = false;
let mut add_archived_link_item = 0;
// select with left join and minimum:
// - the inner select must use `hidden` and _not_ `m.hidden`
// which would refer the outer select and take a lot of time
// - `GROUP BY` is needed several messages may have the same timestamp
// - the list starts with the newest chats
// nb: the query currently shows messages from blocked contacts in groups.
// however, for normal-groups, this is okay as the message is also returned by dc_get_chat_msgs()
// (otherwise it would be hard to follow conversations, wa and tg do the same)
// for the deaddrop, however, they should really be hidden, however, _currently_ the deaddrop is not
// shown at all permanent in the chatlist.
let process_row = |row: &rusqlite::Row| {
let chat_id: ChatId = row.get(0)?;
let msg_id: MsgId = row.get(1).unwrap_or_default();
Ok((chat_id, msg_id))
let chat_id: i32 = row.get(0)?;
// TODO: verify that it is okay for this to be Null
let msg_id: i32 = row.get(1).unwrap_or_default();
Ok((chat_id as u32, msg_id as u32))
};
let process_rows = |rows: rusqlite::MappedRows<_>| {
@@ -101,60 +112,37 @@ impl Chatlist {
.map_err(Into::into)
};
// select with left join and minimum:
//
// - the inner select must use `hidden` and _not_ `m.hidden`
// which would refer the outer select and take a lot of time
// - `GROUP BY` is needed several messages may have the same
// timestamp
// - the list starts with the newest chats
//
// nb: the query currently shows messages from blocked
// contacts in groups. however, for normal-groups, this is
// okay as the message is also returned by dc_get_chat_msgs()
// (otherwise it would be hard to follow conversations, wa and
// tg do the same) for the deaddrop, however, they should
// really be hidden, however, _currently_ the deaddrop is not
// nb: the query currently shows messages from blocked contacts in groups.
// however, for normal-groups, this is okay as the message is also returned by dc_get_chat_msgs()
// (otherwise it would be hard to follow conversations, wa and tg do the same)
// for the deaddrop, however, they should really be hidden, however, _currently_ the deaddrop is not
// shown at all permanent in the chatlist.
let mut ids = if let Some(query_contact_id) = query_contact_id {
// show chats shared with a given contact
context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.timestamp=(
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1))
WHERE c.id>9
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
process_row,
process_rows,
)?
"SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \
ON c.id=m.chat_id \
AND m.timestamp=( SELECT MAX(timestamp) \
FROM msgs WHERE chat_id=c.id \
AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \
AND c.blocked=0 AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?) \
GROUP BY c.id ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;",
params![query_contact_id as i32],
process_row,
process_rows,
)?
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
// show archived chats
context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.timestamp=(
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?))
WHERE c.id>9
AND c.blocked=0
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft],
"SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \
ON c.id=m.chat_id \
AND m.timestamp=( SELECT MAX(timestamp) \
FROM msgs WHERE chat_id=c.id \
AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \
AND c.blocked=0 AND c.archived=1 GROUP BY c.id \
ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;",
params![],
process_row,
process_rows,
)?
@@ -162,70 +150,50 @@ 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
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.timestamp=(
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?))
WHERE c.id>9
AND c.blocked=0
AND c.name LIKE ?
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, str_like_cmd],
"SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \
ON c.id=m.chat_id \
AND m.timestamp=( SELECT MAX(timestamp) \
FROM msgs WHERE chat_id=c.id \
AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \
AND c.blocked=0 AND c.name LIKE ? \
GROUP BY c.id ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;",
params![str_like_cmd],
process_row,
process_rows,
)?
} else {
// show normal chatlist
let mut ids = context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.timestamp=(
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1))
WHERE c.id>9
AND c.blocked=0
AND NOT c.archived=?2
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, ChatVisibility::Archived, ChatVisibility::Pinned],
"SELECT c.id, m.id FROM chats c \
LEFT JOIN msgs m \
ON c.id=m.chat_id \
AND m.timestamp=( SELECT MAX(timestamp) \
FROM msgs WHERE chat_id=c.id \
AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \
AND c.blocked=0 AND c.archived=0 \
GROUP BY c.id \
ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;",
params![],
process_row,
process_rows,
)?;
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),
);
let last_deaddrop_fresh_msg_id = get_last_deaddrop_fresh_msg(context);
if last_deaddrop_fresh_msg_id > 0 {
ids.push((1, last_deaddrop_fresh_msg_id));
}
add_archived_link_item = true;
add_archived_link_item = 1;
}
ids
};
if add_archived_link_item && dc_get_archived_cnt(context) > 0 {
if 0 != add_archived_link_item && dc_get_archived_cnt(context) > 0 {
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
ids.push((DC_CHAT_ID_ALLDONE_HINT, 0));
}
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
ids.push((DC_CHAT_ID_ARCHIVED_LINK, 0));
}
Ok(Chatlist { ids })
@@ -236,7 +204,6 @@ impl Chatlist {
self.ids.len()
}
/// Returns true if chatlist is empty.
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
@@ -244,9 +211,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
}
@@ -254,9 +221,12 @@ impl Chatlist {
/// Get a single message ID of a chatlist.
///
/// To get the message object from the message ID, use dc_get_msg().
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
ensure!(index < self.ids.len(), "Chatlist index out of range");
Ok(self.ids[index].1)
pub fn get_msg_id(&self, index: usize) -> u32 {
if index >= self.ids.len() {
return 0;
}
self.ids[index].1
}
/// Get a summary for a chatlist index.
@@ -278,8 +248,8 @@ impl Chatlist {
// This is because we may want to display drafts here or stuff as
// "is typing".
// Also, sth. as "No messages" would not work if the summary comes from a message.
let mut ret = Lot::new();
let mut ret = Lot::new();
if index >= self.ids.len() {
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return ret;
@@ -288,29 +258,35 @@ impl Chatlist {
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
} else if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0) {
chat_loaded = chat;
&chat_loaded
} else {
return ret;
if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0) {
chat_loaded = chat;
&chat_loaded
} else {
return ret;
}
};
let lastmsg_id = self.ids[index].1;
let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
if lastmsg.from_id != DC_CONTACT_ID_SELF
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok();
}
let lastmsg = if 0 != lastmsg_id {
if let Ok(lastmsg) = dc_msg_load_from_db(context, lastmsg_id) {
if lastmsg.from_id != 1 as libc::c_uint
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok();
}
Some(lastmsg)
Some(lastmsg)
} else {
None
}
} else {
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
{
@@ -321,13 +297,8 @@ impl Chatlist {
ret
}
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
self.ids.iter().position(|(chat_id, _)| chat_id == &id)
}
}
/// Returns the number of archived chats
pub fn dc_get_archived_cnt(context: &Context) -> u32 {
context
.sql
@@ -339,86 +310,19 @@ pub fn dc_get_archived_cnt(context: &Context) -> u32 {
.unwrap_or_default()
}
fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
// We have an index over the state-column, this should be
// sufficient as there are typically only few fresh messages.
context.sql.query_get_value(
context,
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN chats c",
" ON c.id=m.chat_id",
" WHERE m.state=10",
" AND m.hidden=0",
" AND c.blocked=2",
" ORDER BY m.timestamp DESC, m.id DESC;"
),
params![],
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
#[test]
fn test_try_load() {
let t = dummy_context();
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat").unwrap();
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat").unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 3);
assert_eq!(chats.get_chat_id(0), chat_id3);
assert_eq!(chats.get_chat_id(1), chat_id2);
assert_eq!(chats.get_chat_id(2), chat_id1);
// drafts are sorted to the top
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
chat_id2.set_draft(&t.ctx, Some(&mut msg));
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.get_chat_id(0), chat_id2);
// check chatlist query and archive functionality
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None).unwrap();
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
assert_eq!(chats.len(), 0);
chat_id1
.set_visibility(&t.ctx, ChatVisibility::Archived)
.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);
}
fn get_last_deaddrop_fresh_msg(context: &Context) -> u32 {
// We have an index over the state-column, this should be sufficient as there are typically
// only few fresh messages.
context
.sql
.query_get_value(
context,
"SELECT m.id FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
WHERE m.state=10 \
AND m.hidden=0 \
AND c.blocked=2 \
ORDER BY m.timestamp DESC, m.id DESC;",
params![],
)
.unwrap_or_default()
}

View File

@@ -1,16 +1,12 @@
//! # Key-value configuration management
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use crate::blob::BlobObject;
use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::error::Error;
use crate::job::*;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage;
use rusqlite::NO_PARAMS;
/// The available configuration keys.
#[derive(
@@ -23,48 +19,30 @@ pub enum Config {
MailUser,
MailPw,
MailPort,
ImapCertificateChecks,
SendServer,
SendUser,
SendPw,
SendPort,
SmtpCertificateChecks,
ServerFlags,
#[strum(props(default = "INBOX"))]
ImapFolder,
Displayname,
Selfstatus,
Selfavatar,
#[strum(props(default = "0"))]
BccSelf,
#[strum(props(default = "1"))]
E2eeEnabled,
#[strum(props(default = "1"))]
MdnsEnabled,
#[strum(props(default = "1"))]
InboxWatch,
#[strum(props(default = "1"))]
SentboxWatch,
#[strum(props(default = "1"))]
MvboxWatch,
#[strum(props(default = "1"))]
MvboxMove,
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
#[strum(props(default = "0"))]
KeyGenType,
ShowEmails,
SaveMimeHeaders,
ConfiguredAddr,
ConfiguredMailServer,
@@ -72,23 +50,19 @@ pub enum Config {
ConfiguredMailPw,
ConfiguredMailPort,
ConfiguredMailSecurity,
ConfiguredImapCertificateChecks,
ConfiguredSendServer,
ConfiguredSendUser,
ConfiguredSendPw,
ConfiguredSendPort,
ConfiguredSmtpCertificateChecks,
ConfiguredServerFlags,
ConfiguredSendSecurity,
ConfiguredE2EEEnabled,
Configured,
// Deprecated
#[strum(serialize = "sys.version")]
SysVersion,
#[strum(serialize = "sys.msgsize_max_recommended")]
SysMsgsizeMaxRecommended,
#[strum(serialize = "sys.config_keys")]
SysConfigKeys,
}
@@ -98,13 +72,13 @@ impl Context {
pub fn get_config(&self, key: Config) -> Option<String> {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(self, key);
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
let rel_path = self.sql.get_config(self, key);
rel_path.map(|p| dc_get_abs_path(self, &p).to_str().unwrap().to_string())
}
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),
_ => self.sql.get_config(self, key),
};
if value.is_some() {
@@ -118,46 +92,27 @@ impl Context {
}
}
pub fn get_config_int(&self, key: Config) -> i32 {
self.get_config(key)
.and_then(|s| s.parse().ok())
.unwrap_or_default()
}
pub fn get_config_bool(&self, key: Config) -> bool {
self.get_config_int(key) != 0
}
/// Set the given config key.
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
pub fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
pub fn set_config(&self, key: Config, value: Option<&str>) -> Result<(), Error> {
match key {
Config::Selfavatar => {
Config::Selfavatar if value.is_some() => {
let rel_path = std::fs::canonicalize(value.unwrap())?;
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),
}
.set_config(self, key, Some(&rel_path.to_string_lossy()))
}
Config::InboxWatch => {
let ret = self.sql.set_raw_config(self, key, value);
interrupt_inbox_idle(self);
let ret = self.sql.set_config(self, key, value);
interrupt_imap_idle(self);
ret
}
Config::SentboxWatch => {
let ret = self.sql.set_raw_config(self, key, value);
let ret = self.sql.set_config(self, key, value);
interrupt_sentbox_idle(self);
ret
}
Config::MvboxWatch => {
let ret = self.sql.set_raw_config(self, key, value);
let ret = self.sql.set_config(self, key, value);
interrupt_mvbox_idle(self);
ret
}
@@ -169,9 +124,9 @@ impl Context {
value
};
self.sql.set_raw_config(self, key, val)
self.sql.set_config(self, key, val)
}
_ => self.sql.set_raw_config(self, key, value),
_ => self.sql.set_config(self, key, value),
}
}
}
@@ -194,12 +149,6 @@ 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() {
assert_eq!(Config::MailServer.to_string(), "mail_server");
@@ -216,57 +165,4 @@ mod tests {
fn test_default_prop() {
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
}
#[test]
fn test_selfavatar_outside_blobdir() {
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();
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();
assert!(avatar_blob.exists());
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
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);
}
#[test]
fn test_selfavatar_in_blobdir() {
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);
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.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);
}
}

View File

@@ -1,110 +1,81 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::login_param::LoginParam;
use crate::x::*;
use super::read_url::read_url;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Invalid email address: {:?}", _0)]
InvalidEmailAddress(String),
#[fail(display = "XML error at position {}", position)]
InvalidXml {
position: usize,
#[cause]
error: quick_xml::Error,
},
#[fail(display = "Bad or incomplete autoconfig")]
IncompleteAutoconfig(LoginParam),
#[fail(display = "Failed to get URL {}", _0)]
ReadUrlError(#[cause] super::read_url::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<super::read_url::Error> for Error {
fn from(err: super::read_url::Error) -> Error {
Error::ReadUrlError(err)
}
}
#[derive(Debug)]
struct MozAutoconfigure<'a> {
pub in_emailaddr: &'a str,
use super::read_autoconf_file;
/* ******************************************************************************
* Thunderbird's Autoconfigure
******************************************************************************/
/* documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
#[repr(C)]
struct moz_autoconfigure_t<'a> {
pub in_0: &'a LoginParam,
pub in_emaildomain: &'a str,
pub in_emaillocalpart: &'a str,
pub out: LoginParam,
pub out_imap_set: bool,
pub out_smtp_set: bool,
pub tag_server: MozServer,
pub tag_config: MozConfigTag,
pub out_imap_set: libc::c_int,
pub out_smtp_set: libc::c_int,
pub tag_server: libc::c_int,
pub tag_config: libc::c_int,
}
#[derive(Debug, PartialEq)]
enum MozServer {
Undefined,
Imap,
Smtp,
}
#[derive(Debug)]
enum MozConfigTag {
Undefined,
Hostname,
Port,
Sockettype,
Username,
}
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
let mut reader = quick_xml::Reader::from_str(xml_raw);
reader.trim_text(true);
pub unsafe fn moz_autoconfigure(
context: &Context,
url: &str,
param_in: &LoginParam,
) -> Option<LoginParam> {
let xml_raw = read_autoconf_file(context, url);
if xml_raw.is_null() {
return None;
}
// Split address into local part and domain part.
let p = in_emailaddr
.find('@')
.ok_or_else(|| Error::InvalidEmailAddress(in_emailaddr.to_string()))?;
let (in_emaillocalpart, in_emaildomain) = in_emailaddr.split_at(p);
let p = param_in.addr.find("@");
if p.is_none() {
free(xml_raw as *mut libc::c_void);
return None;
}
let (in_emaillocalpart, in_emaildomain) = param_in.addr.split_at(p.unwrap());
let in_emaildomain = &in_emaildomain[1..];
let mut moz_ac = MozAutoconfigure {
in_emailaddr,
let mut reader = quick_xml::Reader::from_str(as_str(xml_raw));
reader.trim_text(true);
let mut buf = Vec::new();
let mut moz_ac = moz_autoconfigure_t {
in_0: param_in,
in_emaildomain,
in_emaillocalpart,
out: LoginParam::new(),
out_imap_set: false,
out_smtp_set: false,
tag_server: MozServer::Undefined,
tag_config: MozConfigTag::Undefined,
out_imap_set: 0,
out_smtp_set: 0,
tag_server: 0,
tag_config: 0,
};
let mut buf = Vec::new();
loop {
let event = reader
.read_event(&mut buf)
.map_err(|error| Error::InvalidXml {
position: reader.buffer_position(),
error,
})?;
match event {
quick_xml::events::Event::Start(ref e) => {
match reader.read_event(&mut buf) {
Ok(quick_xml::events::Event::Start(ref e)) => {
moz_autoconfigure_starttag_cb(e, &mut moz_ac, &reader)
}
quick_xml::events::Event::End(ref e) => moz_autoconfigure_endtag_cb(e, &mut moz_ac),
quick_xml::events::Event::Text(ref e) => {
Ok(quick_xml::events::Event::End(ref e)) => moz_autoconfigure_endtag_cb(e, &mut moz_ac),
Ok(quick_xml::events::Event::Text(ref e)) => {
moz_autoconfigure_text_cb(e, &mut moz_ac, &reader)
}
quick_xml::events::Event::Eof => break,
Err(e) => {
error!(
context,
"Configure xml: Error at position {}: {:?}",
reader.buffer_position(),
e
);
}
Ok(quick_xml::events::Event::Eof) => break,
_ => (),
}
buf.clear();
@@ -115,37 +86,24 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
|| moz_ac.out.send_server.is_empty()
|| moz_ac.out.send_port == 0
{
Err(Error::IncompleteAutoconfig(moz_ac.out))
} else {
Ok(moz_ac.out)
let r = moz_ac.out.to_string();
warn!(context, "Bad or incomplete autoconfig: {}", r,);
free(xml_raw as *mut libc::c_void);
return None;
}
}
pub fn moz_autoconfigure(
context: &Context,
url: &str,
param_in: &LoginParam,
) -> Result<LoginParam> {
let xml_raw = read_url(context, url)?;
let res = parse_xml(&param_in.addr, &xml_raw);
if let Err(err) = &res {
warn!(
context,
"Failed to parse Thunderbird autoconfiguration XML: {}", err
);
}
res
free(xml_raw as *mut libc::c_void);
Some(moz_ac.out)
}
fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
event: &BytesText,
moz_ac: &mut MozAutoconfigure,
moz_ac: &mut moz_autoconfigure_t,
reader: &quick_xml::Reader<B>,
) {
let val = event.unescape_and_decode(reader).unwrap_or_default();
let addr = moz_ac.in_emailaddr;
let addr = &moz_ac.in_0.addr;
let email_local = moz_ac.in_emaillocalpart;
let email_domain = moz_ac.in_emaildomain;
@@ -155,12 +113,12 @@ fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
.replace("%EMAILLOCALPART%", email_local)
.replace("%EMAILDOMAIN%", email_domain);
match moz_ac.tag_server {
MozServer::Imap => match moz_ac.tag_config {
MozConfigTag::Hostname => moz_ac.out.mail_server = val,
MozConfigTag::Port => moz_ac.out.mail_port = val.parse().unwrap_or_default(),
MozConfigTag::Username => moz_ac.out.mail_user = val,
MozConfigTag::Sockettype => {
if moz_ac.tag_server == 1 {
match moz_ac.tag_config {
10 => moz_ac.out.mail_server = val,
11 => moz_ac.out.mail_port = val.parse().unwrap_or_default(),
12 => moz_ac.out.mail_user = val,
13 => {
let val_lower = val.to_lowercase();
if val_lower == "ssl" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
@@ -173,12 +131,13 @@ fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
}
}
_ => {}
},
MozServer::Smtp => match moz_ac.tag_config {
MozConfigTag::Hostname => moz_ac.out.send_server = val,
MozConfigTag::Port => moz_ac.out.send_port = val.parse().unwrap_or_default(),
MozConfigTag::Username => moz_ac.out.send_user = val,
MozConfigTag::Sockettype => {
}
} else if moz_ac.tag_server == 2 {
match moz_ac.tag_config {
10 => moz_ac.out.send_server = val,
11 => moz_ac.out.send_port = val.parse().unwrap_or_default(),
12 => moz_ac.out.send_user = val,
13 => {
let val_lower = val.to_lowercase();
if val_lower == "ssl" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
@@ -191,34 +150,29 @@ fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
}
}
_ => {}
},
MozServer::Undefined => {}
}
}
}
fn moz_autoconfigure_endtag_cb(event: &BytesEnd, moz_ac: &mut MozAutoconfigure) {
fn moz_autoconfigure_endtag_cb(event: &BytesEnd, moz_ac: &mut moz_autoconfigure_t) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "incomingserver" {
if moz_ac.tag_server == MozServer::Imap {
moz_ac.out_imap_set = true;
}
moz_ac.tag_server = MozServer::Undefined;
moz_ac.tag_config = MozConfigTag::Undefined;
moz_ac.tag_server = 0;
moz_ac.tag_config = 0;
moz_ac.out_imap_set = 1;
} else if tag == "outgoingserver" {
if moz_ac.tag_server == MozServer::Smtp {
moz_ac.out_smtp_set = true;
}
moz_ac.tag_server = MozServer::Undefined;
moz_ac.tag_config = MozConfigTag::Undefined;
moz_ac.tag_server = 0;
moz_ac.tag_config = 0;
moz_ac.out_smtp_set = 1;
} else {
moz_ac.tag_config = MozConfigTag::Undefined;
moz_ac.tag_config = 0;
}
}
fn moz_autoconfigure_starttag_cb<B: std::io::BufRead>(
event: &BytesStart,
moz_ac: &mut MozAutoconfigure,
moz_ac: &mut moz_autoconfigure_t,
reader: &quick_xml::Reader<B>,
) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
@@ -235,115 +189,25 @@ fn moz_autoconfigure_starttag_cb<B: std::io::BufRead>(
.unwrap_or_default()
.to_lowercase();
if typ == "imap" && !moz_ac.out_imap_set {
MozServer::Imap
if typ == "imap" && moz_ac.out_imap_set == 0 {
1
} else {
MozServer::Undefined
0
}
} else {
MozServer::Undefined
0
};
moz_ac.tag_config = MozConfigTag::Undefined;
moz_ac.tag_config = 0;
} else if tag == "outgoingserver" {
moz_ac.tag_server = if !moz_ac.out_smtp_set {
MozServer::Smtp
} else {
MozServer::Undefined
};
moz_ac.tag_config = MozConfigTag::Undefined;
moz_ac.tag_server = if moz_ac.out_smtp_set == 0 { 2 } else { 0 };
moz_ac.tag_config = 0;
} else if tag == "hostname" {
moz_ac.tag_config = MozConfigTag::Hostname;
moz_ac.tag_config = 10;
} else if tag == "port" {
moz_ac.tag_config = MozConfigTag::Port;
moz_ac.tag_config = 11;
} else if tag == "sockettype" {
moz_ac.tag_config = MozConfigTag::Sockettype;
moz_ac.tag_config = 13;
} else if tag == "username" {
moz_ac.tag_config = MozConfigTag::Username;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_outlook_autoconfig() {
// Copied from https://autoconfig.thunderbird.net/v1.1/outlook.com on 2019-10-11
let xml_raw =
"<clientConfig version=\"1.1\">
<emailProvider id=\"outlook.com\">
<domain>hotmail.com</domain>
<domain>hotmail.co.uk</domain>
<domain>hotmail.co.jp</domain>
<domain>hotmail.com.br</domain>
<domain>hotmail.de</domain>
<domain>hotmail.fr</domain>
<domain>hotmail.it</domain>
<domain>hotmail.es</domain>
<domain>live.com</domain>
<domain>live.co.uk</domain>
<domain>live.co.jp</domain>
<domain>live.de</domain>
<domain>live.fr</domain>
<domain>live.it</domain>
<domain>live.jp</domain>
<domain>msn.com</domain>
<domain>outlook.com</domain>
<displayName>Outlook.com (Microsoft)</displayName>
<displayShortName>Outlook</displayShortName>
<incomingServer type=\"exchange\">
<hostname>outlook.office365.com</hostname>
<port>443</port>
<username>%EMAILADDRESS%</username>
<socketType>SSL</socketType>
<authentication>OAuth2</authentication>
<owaURL>https://outlook.office365.com/owa/</owaURL>
<ewsURL>https://outlook.office365.com/ews/exchange.asmx</ewsURL>
<useGlobalPreferredServer>true</useGlobalPreferredServer>
</incomingServer>
<incomingServer type=\"imap\">
<hostname>outlook.office365.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<incomingServer type=\"pop3\">
<hostname>outlook.office365.com</hostname>
<port>995</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
<pop3>
<leaveMessagesOnServer>true</leaveMessagesOnServer>
<!-- Outlook.com docs specifically mention that POP3 deletes have effect on the main inbox on webmail and IMAP -->
</pop3>
</incomingServer>
<outgoingServer type=\"smtp\">
<hostname>smtp.office365.com</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
<documentation url=\"http://windows.microsoft.com/en-US/windows/outlook/send-receive-from-app\">
<descr lang=\"en\">Set up an email app with Outlook.com</descr>
</documentation>
</emailProvider>
<webMail>
<loginPage url=\"https://www.outlook.com/\"/>
<loginPageInfo url=\"https://www.outlook.com/\">
<username>%EMAILADDRESS%</username>
<usernameField id=\"i0116\" name=\"login\"/>
<passwordField id=\"i0118\" name=\"passwd\"/>
<loginButton id=\"idSIButton9\" name=\"SI\"/>
</loginPageInfo>
</webMail>
</clientConfig>";
let res = parse_xml("example@outlook.com", xml_raw).expect("XML parsing failed");
assert_eq!(res.mail_server, "outlook.office365.com");
assert_eq!(res.mail_port, 993);
assert_eq!(res.send_server, "smtp.office365.com");
assert_eq!(res.send_port, 587);
moz_ac.tag_config = 12;
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
use crate::context::Context;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "URL request error")]
GetError(#[cause] reqwest::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn read_url(context: &Context, url: &str) -> Result<String> {
info!(context, "Requesting URL {}", url);
match reqwest::blocking::Client::new()
.get(url)
.send()
.and_then(|res| res.text())
{
Ok(res) => Ok(res),
Err(err) => {
info!(context, "Can\'t read URL {}", url);
Err(Error::GetError(err))
}
}
}

View File

@@ -1,35 +1,37 @@
//! # Constants
#![allow(dead_code)]
//! Constants
#![allow(non_camel_case_types, dead_code)]
use deltachat_derive::*;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
}
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
pub enum MoveState {
Undefined = 0,
Pending = 1,
Stay = 2,
Moving = 3,
}
impl Default for MoveState {
fn default() -> Self {
MoveState::Undefined
}
}
// some defaults
const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
pub const DC_MDNS_DEFAULT_ENABLED: i32 = 1;
const DC_INBOX_WATCH_DEFAULT: i32 = 1;
const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum Blocked {
Not = 0,
@@ -43,51 +45,29 @@ impl Default for Blocked {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum ShowEmails {
Off = 0,
AcceptedContacts = 1,
All = 2,
}
impl Default for ShowEmails {
fn default() -> Self {
ShowEmails::Off // also change Config.ShowEmails props(default) on changes
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum KeyGenType {
Default = 0,
Rsa2048 = 1,
Ed25519 = 2,
}
impl Default for KeyGenType {
fn default() -> Self {
KeyGenType::Default
}
}
pub const DC_IMAP_SEEN: u32 = 0x1;
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
pub(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;
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
const DC_GCM_ADDDAYMARKER: usize = 0x01;
pub const DC_GCL_VERIFIED_ONLY: usize = 0x01;
pub const DC_GCL_ADD_SELF: usize = 0x02;
// unchanged user avatars are resent to the recipients every some days
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
/// param1 is a directory where the keys are written to
const DC_IMEX_EXPORT_SELF_KEYS: usize = 1;
/// param1 is a directory where the keys are searched in and read from
const DC_IMEX_IMPORT_SELF_KEYS: usize = 2;
/// param1 is a directory where the backup is written to
const DC_IMEX_EXPORT_BACKUP: usize = 11;
/// param1 is the file with the backup to import
const DC_IMEX_IMPORT_BACKUP: usize = 12;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
@@ -116,8 +96,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum Chattype {
@@ -134,28 +112,20 @@ impl Default for Chattype {
}
pub const DC_MSG_ID_MARKER1: u32 = 1;
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// 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;
pub const DC_CONTACT_ID_INFO: u32 = 2;
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
const DC_CONTACT_ID_DEVICE: u32 = 2;
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;
pub const DC_EMPTY_INBOX: u32 = 0x02;
pub const DC_CREATE_MVBOX: usize = 1;
// Flags for configuring IMAP and SMTP servers.
// These flags are optional
@@ -165,23 +135,23 @@ pub const DC_EMPTY_INBOX: u32 = 0x02;
/// Force OAuth2 authorization. This flag does not skip automatic configuration.
/// Before calling configure() with DC_LP_AUTH_OAUTH2 set,
/// the user has to confirm access at the URL returned by dc_get_oauth2_url().
pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
pub const DC_LP_AUTH_OAUTH2: usize = 0x2;
/// Force NORMAL authorization, this is the default.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
pub const DC_LP_AUTH_NORMAL: usize = 0x4;
/// Connect to IMAP via STARTTLS.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_STARTTLS: i32 = 0x100;
pub const DC_LP_IMAP_SOCKET_STARTTLS: usize = 0x100;
/// Connect to IMAP via SSL.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_SSL: i32 = 0x200;
pub const DC_LP_IMAP_SOCKET_SSL: usize = 0x200;
/// Connect to IMAP unencrypted, this should not be used.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_PLAIN: i32 = 0x400;
pub const DC_LP_IMAP_SOCKET_PLAIN: usize = 0x400;
/// Connect to SMTP via STARTTLS.
/// If this flag is set, automatic configuration is skipped.
@@ -196,9 +166,9 @@ pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
/// if none of these flags are set, the default is chosen
pub const DC_LP_AUTH_FLAGS: i32 = (DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL);
pub const DC_LP_AUTH_FLAGS: usize = (DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL);
/// if none of these flags are set, the default is chosen
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
pub const DC_LP_IMAP_SOCKET_FLAGS: usize =
(DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN);
/// if none of these flags are set, the default is chosen
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
@@ -210,27 +180,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,
Serialize,
Deserialize,
)]
#[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().
@@ -247,11 +200,6 @@ pub enum Viewtype {
/// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
Gif = 21,
/// Message containing a sticker, similar to image.
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
Sticker = 23,
/// Message containing an Audio file.
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
@@ -304,8 +252,13 @@ const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
/// Values for dc_get|set_config("show_emails")
const DC_SHOW_EMAILS_OFF: usize = 0;
const DC_SHOW_EMAILS_ACCEPTED_CONTACTS: usize = 1;
const DC_SHOW_EMAILS_ALL: usize = 2;
// TODO: Strings need some doumentation about used placeholders.
// These constants are used to set stock translation strings
// These constants are used to request strings using #DC_EVENT_GET_STRING.
const DC_STR_NOMESSAGES: usize = 1;
const DC_STR_SELF: usize = 2;
@@ -351,8 +304,7 @@ const DC_STR_MSGACTIONBYME: usize = 63;
const DC_STR_MSGLOCATIONENABLED: usize = 64;
const DC_STR_MSGLOCATIONDISABLED: usize = 65;
const DC_STR_LOCATION: usize = 66;
const DC_STR_STICKER: usize = 67;
const DC_STR_COUNT: usize = 67;
const DC_STR_COUNT: usize = 66;
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;

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::message::MessageState;
use crate::peerstate::*;
use crate::sql;
use crate::stock::StockMessage;
@@ -28,17 +23,15 @@ use crate::stock::StockMessage;
const DC_ORIGIN_MIN_CONTACT_LIST: i32 = 0x100;
/// An object representing a single contact in memory.
///
/// The contact object is not updated.
/// If you want an update, you have to recreate the object.
///
/// The library makes sure
/// only to use names _authorized_ by the contact in `To:` or `Cc:`.
/// *Given-names* as "Daddy" or "Honey" are not used there.
/// _Given-names _as "Daddy" or "Honey" are not used there.
/// For this purpose, internally, two names are tracked -
/// authorized name and given name.
/// authorized-name and given-name.
/// By default, these names are equal, but functions working with contact names
/// only affect the given name.
#[derive(Debug)]
pub struct Contact {
/// The contact ID.
@@ -49,28 +42,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.
/// e.g. in To:-lists. May be empty. It is recommended to use `Contact::get_name`,
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
authname: String,
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr`` to access this field.
addr: String,
/// 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 +65,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,16 +106,24 @@ 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 start a new "normal" chat, defaults to off.
pub fn is_start_new_chat(self) -> bool {
self as i32 >= 0x7FFFFFFF
}
/// Contacts that are verified and known not to be spam.
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
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum Modifier {
pub enum Modifier {
None,
Modified,
Created,
@@ -164,11 +141,24 @@ 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=?;",
pub fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
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);
}
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 +168,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.
@@ -220,7 +199,7 @@ impl Contact {
/// Add a single contact as a result of an _explicit_ user action.
///
/// We assume, the contact name, if any, is entered by the user and is used "as is" therefore,
/// normalize() is *not* called for the name. If the contact is blocked, it is unblocked.
/// normalize() is _not_ called for the name. If the contact is blocked, it is unblocked.
///
/// To add a number of contacts, see `dc_add_address_book()` which is much faster for adding
/// a bunch of addresses.
@@ -250,7 +229,7 @@ impl Contact {
}
/// Mark all messages sent by the given contact
/// as *noticed*. See also dc_marknoticed_chat() and dc_markseen_msgs()
/// as _noticed_. See also dc_marknoticed_chat() and dc_markseen_msgs()
///
/// Calling this function usually results in the event `#DC_EVENT_MSGS_CHANGED`.
pub fn mark_noticed(context: &Context, id: u32) {
@@ -263,8 +242,8 @@ impl Contact {
.is_ok()
{
context.call_cb(Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
chat_id: 0,
msg_id: 0,
});
}
}
@@ -284,8 +263,8 @@ impl Contact {
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
if addr_cmp(addr_normalized, addr_self) {
return DC_CONTACT_ID_SELF;
if addr_normalized == addr_self {
return 1;
}
context.sql.query_get_value(
@@ -300,31 +279,9 @@ impl Contact {
}
/// Lookup a contact and create it if it does not exist yet.
/// The contact is identified by the email-address, a name and an "origin" can be given.
///
/// The "origin" is where the address comes from -
/// from-header, cc-header, addressbook, qr, manual-edit etc.
/// In general, "better" origins overwrite the names of "worse" origins -
/// Eg. if we got a name in cc-header and later in from-header, the name will change -
/// this does not happen the other way round.
///
/// The "best" origin are manually created contacts -
/// names given manually can only be overwritten by further manual edits
/// (until they are set empty again or reset to the name seen in the From-header).
///
/// These manually edited names are _never_ used for sending on the wire -
/// this should avoid sending sth. as "Mama" or "Daddy" to some 3rd party.
/// Instead, for the wire, we use so called "authnames"
/// that can only be set and updated by a From-header.
///
/// The different names used in the function are:
/// - "name": name passed as function argument, belonging to the given origin
/// - "row_name": current name used in the database, typically set to "name"
/// - "row_authname": name as authorized from a contact, set only through a From-header
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
pub(crate) fn add_or_lookup(
pub fn add_or_lookup(
context: &Context,
name: impl AsRef<str>,
addr: impl AsRef<str>,
@@ -343,8 +300,8 @@ impl Contact {
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
if addr_cmp(addr, addr_self) {
return Ok((DC_CONTACT_ID_SELF, sth_modified));
if addr == addr_self {
return Ok((1, sth_modified));
}
if !may_be_valid_addr(&addr) {
@@ -378,9 +335,7 @@ impl Contact {
if !name.as_ref().is_empty() {
if !row_name.is_empty() {
if (origin >= row_origin || row_name == row_authname)
&& name.as_ref() != row_name
{
if origin >= row_origin && name.as_ref() != row_name {
update_name = true;
}
} else {
@@ -389,9 +344,6 @@ impl Contact {
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
update_authname = true;
}
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
// no name given on manual edit, this will update the name to the authname
update_name = true;
}
Ok((row_id, row_name, row_addr, row_origin, row_authname))
@@ -402,22 +354,16 @@ impl Contact {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {
let new_name = if update_name {
if !name.as_ref().is_empty() {
name.as_ref()
} else {
&row_authname
}
} else {
&row_name
};
sql::execute(
context,
&context.sql,
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
params![
new_name,
if update_name {
name.as_ref()
} else {
&row_name
},
if update_addr { addr } else { &row_addr },
if origin > row_origin {
origin
@@ -435,38 +381,26 @@ impl Contact {
.ok();
if update_name {
// Update the contact name also if it is used as a group name.
// This is one of the few duplicated data, however, getting the chat list is easier this way.
sql::execute(
context,
&context.sql,
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
params![new_name, Chattype::Single, row_id]
params![name.as_ref(), Chattype::Single, row_id]
).ok();
}
sth_modified = Modifier::Modified;
}
} 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 { "" }
],
"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;
info!(context, "added contact id={} addr={}", row_id, addr);
} else {
error!(context, "Cannot add contact.");
}
@@ -487,7 +421,7 @@ impl Contact {
/// the event `DC_EVENT_CONTACTS_CHANGED` is sent.
///
/// To add a single contact entered by the user, you should prefer `Contact::create`,
/// however, for adding a bunch of addresses, this function is much faster.
/// however, for adding a bunch of addresses, this function is _much_ faster.
///
/// The `addr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
///
@@ -553,7 +487,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 },
@@ -644,12 +578,7 @@ impl Contact {
let mut self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
if peerstate.is_some()
&& peerstate
.as_ref()
.and_then(|p| p.peek_key(PeerstateVerifiedStatus::Unverified))
.is_some()
{
if peerstate.is_some() && peerstate.as_ref().and_then(|p| p.peek_key(0)).is_some() {
let peerstate = peerstate.as_ref().unwrap();
let p =
context.stock_str(if peerstate.prefer_encrypt == EncryptPreference::Mutual {
@@ -669,25 +598,25 @@ impl Contact {
.map(|k| k.formatted_fingerprint())
.unwrap_or_default();
let fingerprint_other_verified = peerstate
.peek_key(PeerstateVerifiedStatus::BidirectVerified)
.peek_key(2)
.map(|k| k.formatted_fingerprint())
.unwrap_or_default();
let fingerprint_other_unverified = peerstate
.peek_key(PeerstateVerifiedStatus::Unverified)
.peek_key(0)
.map(|k| k.formatted_fingerprint())
.unwrap_or_default();
if loginparam.addr < peerstate.addr {
if peerstate.addr.is_some() && &loginparam.addr < peerstate.addr.as_ref().unwrap() {
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
cat_fingerprint(
&mut ret,
peerstate.addr.clone(),
peerstate.addr.as_ref().unwrap(),
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
} else {
cat_fingerprint(
&mut ret,
peerstate.addr.clone(),
peerstate.addr.as_ref().unwrap(),
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
@@ -750,7 +679,7 @@ impl Contact {
}
Err(err) => {
error!(context, "delete_contact {} failed ({})", contact_id, err);
return Err(err.into());
return Err(err);
}
}
}
@@ -768,17 +697,7 @@ impl Contact {
/// like "Me" in the selected language and the email address
/// defined by dc_set_config().
pub fn get_by_id(context: &Context, contact_id: u32) -> Result<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(())
Contact::load_from_db(context, contact_id)
}
/// Get the ID of the contact.
@@ -791,7 +710,6 @@ impl Contact {
&self.addr
}
/// Get name authorized by the contact.
pub fn get_authname(&self) -> &str {
&self.authname
}
@@ -814,9 +732,6 @@ impl Contact {
if !self.name.is_empty() {
return &self.name;
}
if !self.authname.is_empty() {
return &self.authname;
}
&self.addr
}
@@ -852,11 +767,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
}
@@ -892,14 +804,14 @@ impl Contact {
}
if let Some(peerstate) = peerstate {
if peerstate.verified_key.is_some() {
if peerstate.verified_key().is_some() {
return VerifiedStatus::BidirectVerified;
}
}
let peerstate = Peerstate::from_addr(context, &context.sql, &self.addr);
if let Some(ps) = peerstate {
if ps.verified_key.is_some() {
if ps.verified_key().is_some() {
return VerifiedStatus::BidirectVerified;
}
}
@@ -915,7 +827,7 @@ impl Contact {
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
if !contact.addr.is_empty() {
let normalized_addr = addr_normalize(addr.as_ref());
if contact.addr == normalized_addr {
if &contact.addr == &normalized_addr {
return true;
}
}
@@ -939,8 +851,24 @@ 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 {
if !context.sql.is_open() || contact_id <= 9 {
return false;
}
@@ -964,8 +892,7 @@ impl Contact {
}
}
/// Extracts first name from full name.
fn get_first_name(full_name: &str) -> &str {
fn get_first_name<'a>(full_name: &'a str) -> &'a str {
full_name.splitn(2, ' ').next().unwrap_or_default()
}
@@ -975,7 +902,6 @@ pub fn may_be_valid_addr(addr: &str) -> bool {
res.is_ok()
}
/// Returns address with whitespace trimmed and `mailto:` prefix removed.
pub fn addr_normalize(addr: &str) -> &str {
let norm = addr.trim();
@@ -987,26 +913,26 @@ pub fn addr_normalize(addr: &str) -> &str {
}
fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
if contact_id <= 9 {
return;
}
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
if contact.blocked != new_blocking
&& sql::execute(
if contact.blocked != new_blocking {
if sql::execute(
context,
&context.sql,
"UPDATE contacts SET blocked=? WHERE id=?;",
params![new_blocking as i32, contact_id as i32],
)
.is_ok()
{
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
// non-destructive blocking->unblocking.
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if sql::execute(
{
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
// non-destructive blocking->unblocking.
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if sql::execute(
context,
&context.sql,
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
@@ -1015,35 +941,11 @@ fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
Contact::mark_noticed(context, contact_id);
context.call_cb(Event::ContactsChanged(None));
}
}
}
}
}
pub(crate) 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
}
};
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)
@@ -1061,9 +963,9 @@ pub fn normalize_name(full_name: impl AsRef<str>) -> String {
if len > 0 {
let firstchar = full_name.as_bytes()[0];
let lastchar = full_name.as_bytes()[len - 1];
if firstchar == b'\'' && lastchar == b'\''
|| firstchar == b'\"' && lastchar == b'\"'
|| firstchar == b'<' && lastchar == b'>'
if firstchar == '\'' as u8 && lastchar == '\'' as u8
|| firstchar == '\"' as u8 && lastchar == '\"' as u8
|| firstchar == '<' as u8 && lastchar == '>' as u8
{
full_name = &full_name[1..len - 1];
}
@@ -1108,25 +1010,23 @@ fn cat_fingerprint(
}
}
impl Context {
/// determine whether the specified addr maps to the/a self addr
pub fn is_self_addr(&self, addr: &str) -> Result<bool> {
let self_addr = match self.get_config(Config::ConfiguredAddr) {
Some(s) => s,
None => return Err(Error::NotConfigured),
};
Ok(addr_cmp(self_addr, addr))
}
}
pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
let norm1 = addr_normalize(addr1.as_ref()).to_lowercase();
let norm2 = addr_normalize(addr2.as_ref()).to_lowercase();
let norm1 = addr_normalize(addr1.as_ref());
let norm2 = addr_normalize(addr2.as_ref());
norm1 == norm2
}
pub fn addr_equals_self(context: &Context, addr: impl AsRef<str>) -> bool {
if !addr.as_ref().is_empty() {
let normalized_addr = addr_normalize(addr.as_ref());
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr) {
return normalized_addr == self_addr;
}
}
false
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
book.lines()
.chunks(2)
@@ -1146,8 +1046,6 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
mod tests {
use super::*;
use crate::test_utils::*;
#[test]
fn test_may_be_valid_addr() {
assert_eq!(may_be_valid_addr(""), false);
@@ -1173,10 +1071,6 @@ mod tests {
fn test_normalize_addr() {
assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com");
assert_eq!(addr_normalize(" hello@world.com "), "hello@world.com");
// normalisation preserves case to allow user-defined spelling.
// however, case is ignored on addr_cmp()
assert_ne!(addr_normalize("John@Doe.com"), "john@doe.com");
}
#[test]
@@ -1193,261 +1087,4 @@ mod tests {
vec![("Name one", "Address one"), ("Name two", "Address two")]
)
}
#[test]
fn test_get_contacts() {
let context = dummy_context();
let contacts = Contact::get_all(&context.ctx, 0, Some("some2")).unwrap();
assert_eq!(contacts.len(), 0);
let id = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap();
assert_ne!(id, 0);
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).unwrap();
assert_eq!(contacts.len(), 1);
let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).unwrap();
assert_eq!(contacts.len(), 0);
}
#[test]
fn test_is_self_addr() -> Result<()> {
let t = test_context(None);
assert!(t.ctx.is_self_addr("me@me.org").is_err());
let addr = configure_alice_keypair(&t.ctx);
assert_eq!(t.ctx.is_self_addr("me@me.org")?, false);
assert_eq!(t.ctx.is_self_addr(&addr)?, true);
Ok(())
}
#[test]
fn test_add_or_lookup() {
// add some contacts, this also tests add_address_book()
let t = dummy_context();
let book = concat!(
" Name one \n one@eins.org \n",
"Name two\ntwo@deux.net\n",
"\nthree@drei.sam\n",
"Name two\ntwo@deux.net\n" // should not be added again
);
assert_eq!(Contact::add_address_book(&t.ctx, book).unwrap(), 3);
// check first added contact, this does not modify because of lower origin
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t.ctx, "bla foo", "one@eins.org", Origin::IncomingUnknownTo)
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_id(), contact_id);
assert_eq!(contact.get_name(), "Name one");
assert_eq!(contact.get_display_name(), "Name one");
assert_eq!(contact.get_addr(), "one@eins.org");
assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)");
// modify first added contact
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"Real one",
" one@eins.org ",
Origin::ManuallyCreated,
)
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_name(), "Real one");
assert_eq!(contact.get_addr(), "one@eins.org");
assert!(!contact.is_blocked());
// check third added contact (contact without name)
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t.ctx, "", "three@drei.sam", Origin::IncomingUnknownTo)
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "three@drei.sam");
assert_eq!(contact.get_addr(), "three@drei.sam");
assert_eq!(contact.get_name_n_addr(), "three@drei.sam");
// add name to third contact from incoming message (this becomes authorized name)
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"m. serious",
"three@drei.sam",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)");
assert!(!contact.is_blocked());
// manually edit name of third contact (does not changed authorized name)
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"schnucki",
"three@drei.sam",
Origin::ManuallyCreated,
)
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "m. serious");
assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)");
assert!(!contact.is_blocked());
// check SELF
let contact = Contact::load_from_db(&t.ctx, DC_CONTACT_ID_SELF).unwrap();
assert_eq!(DC_CONTACT_ID_SELF, 1);
assert_eq!(contact.get_name(), t.ctx.stock_str(StockMessage::SelfMsg));
assert_eq!(contact.get_addr(), ""); // we're not configured
assert!(!contact.is_blocked());
}
#[test]
fn test_remote_authnames() {
let t = dummy_context();
// incoming mail `From: bob1 <bob@example.org>` - this should init authname and name
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");
// incoming mail `From: bob2 <bob@example.org>` - this should update authname and name
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");
// manually edit name to "bob3" - authname should be still be "bob2" a given in `From:` above
let contact_id = Contact::create(&t.ctx, "bob3", "bob@example.org").unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
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");
// incoming mail `From: bob4 <bob@example.org>` - this should update authname, manually given name is still "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_remote_authnames_create_empty() {
let t = dummy_context();
// manually create "claire@example.org" without a given name
let contact_id = Contact::create(&t.ctx, "", "claire@example.org").unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire@example.org");
// incoming mail `From: claire1 <claire@example.org>` - this should update authname and name
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"claire1",
"claire@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "claire1");
assert_eq!(contact.get_name(), "claire1");
assert_eq!(contact.get_display_name(), "claire1");
// incoming mail `From: claire2 <claire@example.org>` - this should update authname and name
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"claire2",
"claire@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "claire2");
assert_eq!(contact.get_name(), "claire2");
assert_eq!(contact.get_display_name(), "claire2");
}
#[test]
fn test_remote_authnames_edit_empty() {
let t = dummy_context();
// manually create "dave@example.org"
let contact_id = Contact::create(&t.ctx, "dave1", "dave@example.org").unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// incoming mail `From: dave2 <dave@example.org>` - this should update authname
Contact::add_or_lookup(
&t.ctx,
"dave2",
"dave@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// manually clear the name
Contact::create(&t.ctx, "", "dave@example.org").unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave2");
assert_eq!(contact.get_display_name(), "dave2");
}
#[test]
fn test_addr_cmp() {
assert!(addr_cmp("AA@AA.ORG", "aa@aa.ORG"));
assert!(addr_cmp(" aa@aa.ORG ", "AA@AA.ORG"));
assert!(addr_cmp(" mailto:AA@AA.ORG", "Aa@Aa.orG"));
}
}

View File

@@ -1,12 +1,11 @@
//! Context module
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use libc::uintptr_t;
use crate::chat::*;
use crate::config::Config;
use crate::constants::*;
use crate::contact::*;
use crate::error::*;
@@ -14,12 +13,12 @@ use crate::events::Event;
use crate::imap::*;
use crate::job::*;
use crate::job_thread::JobThread;
use crate::key::Key;
use crate::key::*;
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId};
use crate::message::*;
use crate::param::Params;
use crate::smtp::Smtp;
use crate::smtp::*;
use crate::sql::Sql;
/// Callback function type for [Context]
@@ -30,18 +29,21 @@ 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(DebugStub)]
pub struct Context {
/// Database file path
dbfile: PathBuf,
/// Blob directory path
blobdir: PathBuf,
pub sql: Sql,
pub inbox: Arc<RwLock<Imap>>,
pub perform_inbox_jobs_needed: Arc<RwLock<bool>>,
pub probe_imap_network: Arc<RwLock<bool>>,
pub inbox_thread: Arc<RwLock<JobThread>>,
pub sentbox_thread: Arc<RwLock<JobThread>>,
pub mvbox_thread: Arc<RwLock<JobThread>>,
pub smtp: Arc<Mutex<Smtp>>,
@@ -50,41 +52,22 @@ pub struct Context {
#[debug_stub = "Callback"]
cb: Box<ContextCallback>,
pub os_name: Option<String>,
pub cmdline_sel_chat_id: Arc<RwLock<ChatId>>,
pub(crate) bob: Arc<RwLock<BobStatus>>,
pub last_smeared_timestamp: RwLock<i64>,
pub cmdline_sel_chat_id: Arc<RwLock<u32>>,
pub bob: Arc<RwLock<BobStatus>>,
pub last_smeared_timestamp: Arc<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>>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct RunningState {
pub ongoing_running: bool,
shall_stop_ongoing: bool,
}
/// Return some info about deltachat-core
///
/// This contains information mostly about the library itself, the
/// actual keys and their values which will be present are not
/// guaranteed. Calling [Context::get_info] also includes information
/// about the context on top of the information here.
pub fn get_info() -> HashMap<&'static str, String> {
let mut res = HashMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
res.insert("level", "awesome".into());
res
pub shall_stop_ongoing: bool,
}
impl Context {
/// Creates new context.
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
pretty_env_logger::try_init_timed().ok();
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -109,6 +92,7 @@ impl Context {
let ctx = Context {
blobdir,
dbfile,
inbox: Arc::new(RwLock::new(Imap::new())),
cb,
os_name: Some(os_name),
running_state: Arc::new(RwLock::new(Default::default())),
@@ -117,13 +101,8 @@ impl Context {
smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
oauth2_critical: Arc::new(Mutex::new(())),
bob: Arc::new(RwLock::new(Default::default())),
last_smeared_timestamp: RwLock::new(0),
cmdline_sel_chat_id: Arc::new(RwLock::new(ChatId::new(0))),
inbox_thread: Arc::new(RwLock::new(JobThread::new(
"INBOX",
"configured_inbox_folder",
Imap::new(),
))),
last_smeared_timestamp: Arc::new(RwLock::new(0)),
cmdline_sel_chat_id: Arc::new(RwLock::new(0)),
sentbox_thread: Arc::new(RwLock::new(JobThread::new(
"SENTBOX",
"configured_sentbox_folder",
@@ -137,109 +116,53 @@ impl Context {
probe_imap_network: Arc::new(RwLock::new(false)),
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
};
ensure!(
ctx.sql.open(&ctx, &ctx.dbfile, false),
ctx.sql.open(&ctx, &ctx.dbfile, 0),
"Failed opening sqlite database"
);
Ok(ctx)
}
/// Returns database file path.
pub fn get_dbfile(&self) -> &Path {
self.dbfile.as_path()
}
/// Returns blob directory path.
pub fn get_blobdir(&self) -> &Path {
self.blobdir.as_path()
}
pub fn call_cb(&self, event: Event) {
(*self.cb)(self, event);
pub fn call_cb(&self, event: Event) -> uintptr_t {
(*self.cb)(self, event)
}
/*******************************************************************************
* Ongoing process allocation/free/check
******************************************************************************/
pub fn alloc_ongoing(&self) -> bool {
if self.has_ongoing() {
warn!(self, "There is already another ongoing process running.",);
false
} else {
let s_a = self.running_state.clone();
let mut s = s_a.write().unwrap();
s.ongoing_running = true;
s.shall_stop_ongoing = false;
true
}
}
pub fn free_ongoing(&self) {
let s_a = self.running_state.clone();
let mut s = s_a.write().unwrap();
s.ongoing_running = false;
s.shall_stop_ongoing = true;
}
pub fn has_ongoing(&self) -> bool {
let s_a = self.running_state.clone();
let s = s_a.read().unwrap();
s.ongoing_running || !s.shall_stop_ongoing
}
/// Signal an ongoing process to stop.
pub fn stop_ongoing(&self) {
let s_a = self.running_state.clone();
let mut s = s_a.write().unwrap();
if s.ongoing_running && !s.shall_stop_ongoing {
info!(self, "Signaling the ongoing process to stop ASAP.",);
s.shall_stop_ongoing = true;
} else {
info!(self, "No ongoing process to stop.",);
};
}
pub fn shall_stop_ongoing(&self) -> bool {
self.running_state
.clone()
.read()
.unwrap()
.shall_stop_ongoing
}
/*******************************************************************************
* UI chat/message related API
******************************************************************************/
pub fn get_info(&self) -> HashMap<&'static str, String> {
let unset = "0";
let l = LoginParam::from_database(self, "");
let l2 = LoginParam::from_database(self, "configured_");
let displayname = self.get_config(Config::Displayname);
let displayname = self.sql.get_config(self, "displayname");
let chats = get_chat_cnt(self) as usize;
let real_msgs = message::get_real_msg_cnt(self) as usize;
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self) as usize;
let real_msgs = dc_get_real_msg_cnt(self) as usize;
let deaddrop_msgs = dc_get_deaddrop_msg_cnt(self) as usize;
let contacts = Contact::get_real_cnt(self) as usize;
let is_configured = self.get_config_int(Config::Configured);
let is_configured = self
.sql
.get_config_int(self, "configured")
.unwrap_or_default();
let dbversion = self
.sql
.get_raw_config_int(self, "dbversion")
.get_config_int(self, "dbversion")
.unwrap_or_default();
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled);
let mdns_enabled = self.get_config_int(Config::MdnsEnabled);
let bcc_self = self.get_config_int(Config::BccSelf);
let e2ee_enabled = self
.sql
.get_config_int(self, "e2ee_enabled")
.unwrap_or_else(|| 1);
let mdns_enabled = self
.sql
.get_config_int(self, "mdns_enabled")
.unwrap_or_else(|| 1);
let prv_key_cnt: Option<isize> =
self.sql
@@ -257,25 +180,48 @@ impl Context {
"<Not yet calculated>".into()
};
let inbox_watch = self.get_config_int(Config::InboxWatch);
let sentbox_watch = self.get_config_int(Config::SentboxWatch);
let mvbox_watch = self.get_config_int(Config::MvboxWatch);
let mvbox_move = self.get_config_int(Config::MvboxMove);
let inbox_watch = self
.sql
.get_config_int(self, "inbox_watch")
.unwrap_or_else(|| 1);
let sentbox_watch = self
.sql
.get_config_int(self, "sentbox_watch")
.unwrap_or_else(|| 1);
let mvbox_watch = self
.sql
.get_config_int(self, "mvbox_watch")
.unwrap_or_else(|| 1);
let mvbox_move = self
.sql
.get_config_int(self, "mvbox_move")
.unwrap_or_else(|| 1);
let folders_configured = self
.sql
.get_raw_config_int(self, "folders_configured")
.get_config_int(self, "folders_configured")
.unwrap_or_default();
let configured_sentbox_folder = self
.sql
.get_raw_config(self, "configured_sentbox_folder")
.get_config(self, "configured_sentbox_folder")
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self
.sql
.get_raw_config(self, "configured_mvbox_folder")
.get_config(self, "configured_mvbox_folder")
.unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info();
let mut res = HashMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert(
"sqlite_thread_safe",
unsafe { rusqlite::ffi::sqlite3_threadsafe() }.to_string(),
);
res.insert(
"arch",
(::std::mem::size_of::<*mut libc::c_void>())
.wrapping_mul(8)
.to_string(),
);
res.insert("number_of_chats", chats.to_string());
res.insert("number_of_chat_messages", real_msgs.to_string());
res.insert("messages_in_contact_requests", deaddrop_msgs.to_string());
@@ -284,11 +230,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());
@@ -301,7 +242,6 @@ impl Context {
res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert("bcc_self", bcc_self.to_string());
res.insert(
"private_key_count",
prv_key_cnt.unwrap_or_default().to_string(),
@@ -311,43 +251,38 @@ impl Context {
pub_key_cnt.unwrap_or_default().to_string(),
);
res.insert("fingerprint", fingerprint_str);
res.insert("level", "awesome".into());
res
}
pub fn get_fresh_msgs(&self) -> Vec<MsgId> {
pub fn get_fresh_msgs(&self) -> Vec<u32> {
let show_deaddrop = 0;
self.sql
.query_map(
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" LEFT JOIN chats c",
" ON m.chat_id=c.id",
" WHERE m.state=?",
" AND m.hidden=0",
" AND m.chat_id>?",
" AND ct.blocked=0",
" AND (c.blocked=0 OR c.blocked=?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
),
"SELECT m.id FROM msgs m LEFT JOIN contacts ct \
ON m.from_id=ct.id LEFT JOIN chats c ON m.chat_id=c.id WHERE m.state=? \
AND m.hidden=0 \
AND m.chat_id>? \
AND ct.blocked=0 \
AND (c.blocked=0 OR c.blocked=?) ORDER BY m.timestamp DESC,m.id DESC;",
&[10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|row| row.get::<_, MsgId>(0),
|row| row.get(0),
|rows| {
let mut ret = Vec::new();
for row in rows {
ret.push(row?);
let id: u32 = row?;
ret.push(id);
}
Ok(ret)
},
)
.unwrap_or_default()
.unwrap()
}
#[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<u32> {
let real_query = query.as_ref().trim();
if real_query.is_empty() {
return Vec::new();
@@ -355,44 +290,26 @@ impl Context {
let strLikeInText = format!("%{}%", real_query);
let strLikeBeg = format!("{}%", real_query);
let query = if !chat_id.is_unset() {
concat!(
"SELECT m.id AS id, m.timestamp AS timestamp",
" FROM msgs m",
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" WHERE m.chat_id=?",
" AND m.hidden=0",
" AND ct.blocked=0",
" AND (txt LIKE ? OR ct.name LIKE ?)",
" ORDER BY m.timestamp,m.id;"
)
let query = if 0 != chat_id {
"SELECT m.id, m.timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id WHERE m.chat_id=? \
AND m.hidden=0 \
AND ct.blocked=0 AND (txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp,m.id;"
} else {
concat!(
"SELECT m.id AS id, m.timestamp AS timestamp",
" FROM msgs m",
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" LEFT JOIN chats c",
" ON m.chat_id=c.id",
" WHERE m.chat_id>9",
" AND m.hidden=0",
" AND (c.blocked=0 OR c.blocked=?)",
" AND ct.blocked=0",
" AND (m.txt LIKE ? OR ct.name LIKE ?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
)
"SELECT m.id, m.timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id \
LEFT JOIN chats c ON m.chat_id=c.id WHERE m.chat_id>9 AND m.hidden=0 \
AND (c.blocked=0 OR c.blocked=?) \
AND ct.blocked=0 AND (m.txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp DESC,m.id DESC;"
};
self.sql
.query_map(
query,
params![chat_id, &strLikeInText, &strLikeBeg],
|row| row.get::<_, MsgId>("id"),
params![chat_id as i32, &strLikeInText, &strLikeBeg],
|row| row.get::<_, i32>(0),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
ret.push(id? as u32);
}
Ok(ret)
},
@@ -405,7 +322,7 @@ impl Context {
}
pub fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
let sentbox_name = self.sql.get_raw_config(self, "configured_sentbox_folder");
let sentbox_name = self.sql.get_config(self, "configured_sentbox_folder");
if let Some(name) = sentbox_name {
name == folder_name.as_ref()
} else {
@@ -414,7 +331,7 @@ impl Context {
}
pub fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
let mvbox_name = self.sql.get_raw_config(self, "configured_mvbox_folder");
let mvbox_name = self.sql.get_config(self, "configured_mvbox_folder");
if let Some(name) = mvbox_name {
name == folder_name.as_ref()
@@ -423,32 +340,41 @@ impl Context {
}
}
pub fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
if !self.get_config_bool(Config::MvboxMove) {
pub fn do_heuristics_moves(&self, folder: &str, msg_id: u32) {
if self
.sql
.get_config_int(self, "mvbox_move")
.unwrap_or_else(|| 1)
== 0
{
return;
}
if self.is_mvbox(folder) {
if !self.is_inbox(folder) && !self.is_sentbox(folder) {
return;
}
if let Ok(msg) = Message::load_from_db(self, msg_id) {
if msg.is_setupmessage() {
if let Ok(msg) = dc_msg_new_load(self, msg_id) {
if dc_msg_is_setupmessage(&msg) {
// do not move setup messages;
// there may be a non-delta device that wants to handle it
return;
}
match msg.is_dc_message {
MessengerMessage::No => {}
MessengerMessage::Yes | MessengerMessage::Reply => {
job_add(
self,
Action::MoveMsg,
msg.id.to_u32() as i32,
Params::new(),
0,
);
}
if self.is_mvbox(folder) {
dc_update_msg_move_state(self, msg.rfc724_mid, MoveState::Stay);
}
// 1 = dc message, 2 = reply to dc message
if 0 != msg.is_dc_message {
job_add(
self,
Action::MoveMsg,
msg.id as libc::c_int,
Params::new(),
0,
);
dc_update_msg_move_state(self, msg.rfc724_mid, MoveState::Moving);
}
}
}
@@ -456,8 +382,8 @@ impl Context {
impl Drop for Context {
fn drop(&mut self) {
info!(self, "disconnecting inbox-thread",);
self.inbox_thread.read().unwrap().imap.disconnect(self);
info!(self, "disconnecting INBOX-watch",);
self.inbox.read().unwrap().disconnect(self);
info!(self, "disconnecting sentbox-thread",);
self.sentbox_thread.read().unwrap().imap.disconnect(self);
info!(self, "disconnecting mvbox-thread",);
@@ -478,31 +404,18 @@ impl Default for RunningState {
}
#[derive(Debug, Default)]
pub(crate) struct BobStatus {
pub struct BobStatus {
pub expects: i32,
pub status: i32,
pub qr_scan: Option<Lot>,
}
#[derive(Debug, PartialEq)]
pub(crate) enum PerformJobsNeeded {
Not,
AtOnce,
AvoidDos,
}
impl Default for PerformJobsNeeded {
fn default() -> Self {
Self::Not
}
}
#[derive(Default, Debug)]
pub struct SmtpState {
pub idle: bool,
pub suspended: bool,
pub doing_jobs: bool,
pub(crate) perform_jobs_needed: PerformJobsNeeded,
pub perform_jobs_needed: i32,
pub probe_network: bool,
}
@@ -521,7 +434,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(Box::new(|_, _| ()), "FakeOs".into(), dbfile);
let res = Context::new(Box::new(|_, _| 0), "FakeOs".into(), dbfile);
assert!(res.is_err());
}
@@ -536,7 +449,7 @@ mod tests {
fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new(Box::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());
}
@@ -547,7 +460,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(Box::new(|_, _| ()), "FakeOS".into(), dbfile);
let res = Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile);
assert!(res.is_err());
}
@@ -557,7 +470,7 @@ mod tests {
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile).unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
@@ -567,7 +480,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}
@@ -576,7 +489,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(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}
@@ -591,14 +504,6 @@ mod tests {
let t = dummy_context();
let info = t.ctx.get_info();
assert!(info.get("database_dir").is_some());
}
#[test]
fn test_get_info_no_context() {
let info = get_info();
assert!(info.get("deltachat_core_version").is_some());
assert!(info.get("database_dir").is_none());
assert_eq!(info.get("level").unwrap(), "awesome");
}
}

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

View File

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

1094
src/dc_imex.rs Normal file

File diff suppressed because it is too large Load Diff

1294
src/dc_mimefactory.rs Normal file

File diff suppressed because it is too large Load Diff

1665
src/dc_mimeparser.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

277
src/dc_simplify.rs Normal file
View File

@@ -0,0 +1,277 @@
use crate::dc_dehtml::*;
#[derive(Copy, Clone)]
pub struct Simplify {
pub is_forwarded: bool,
}
/// Return index of footer line in vector of message lines, or vector length if
/// no footer is found.
///
/// Also return whether not-standard (rfc3676, §4.3) footer is found.
fn find_message_footer(lines: &[&str]) -> (usize, bool) {
for ix in 0..lines.len() {
let line = lines[ix];
// quoted-printable may encode `-- ` to `-- =20` which is converted
// back to `-- `
match line.as_ref() {
"-- " | "-- " => return (ix, false),
"--" | "---" | "----" => return (ix, true),
_ => (),
}
}
return (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 {
dc_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)]
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 {
if 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: libc::c_int = 0i32;
let mut content_lines_added: libc::c_int = 0i32;
for l in l_first..l_last {
let line = lines[l];
if is_empty_line(line) {
pending_linebreaks += 1
} else {
if 0 != content_lines_added {
if pending_linebreaks > 2i32 {
pending_linebreaks = 2i32
}
while 0 != pending_linebreaks {
ret += "\n";
pending_linebreaks -= 1
}
}
// the incoming message might contain invalid UTF8
ret += line;
content_lines_added += 1;
pending_linebreaks = 1i32
}
}
if is_cut_at_end && (!is_cut_at_begin || 0 != content_lines_added) {
ret += " [...]";
}
ret
}
}
/**
* 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(""));
}
}

469
src/dc_strencode.rs Normal file
View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,75 +1,38 @@
//! # Error handling
use lettre_email::mime;
use failure::Fail;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Sqlite Error: {:?}", _0)]
Sql(rusqlite::Error),
#[fail(display = "Sqlite Connection Pool Error: {:?}", _0)]
ConnectionPool(r2d2::Error),
#[fail(display = "{:?}", _0)]
Failure(failure::Error),
#[fail(display = "SQL error: {:?}", _0)]
SqlError(#[cause] crate::sql::Error),
#[fail(display = "Sqlite: Connection closed")]
SqlNoConnection,
#[fail(display = "Sqlite: Already open")]
SqlAlreadyOpen,
#[fail(display = "Sqlite: Failed to open")]
SqlFailedToOpen,
#[fail(display = "{:?}", _0)]
Io(std::io::Error),
#[fail(display = "{:?}", _0)]
Message(String),
#[fail(display = "{:?}", _0)]
MessageWithCause(String, #[cause] failure::Error, failure::Backtrace),
#[fail(display = "{:?}", _0)]
Image(image_meta::ImageError),
#[fail(display = "{:?}", _0)]
Utf8(std::str::Utf8Error),
#[fail(display = "{:?}", _0)]
CStringError(crate::dc_tools::CStringError),
#[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)]
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>;
impl From<crate::sql::Error> for Error {
fn from(err: crate::sql::Error) -> Error {
Error::SqlError(err)
}
}
impl From<base64::DecodeError> for Error {
fn from(err: base64::DecodeError) -> Error {
Error::Base64Decode(err)
impl From<rusqlite::Error> for Error {
fn from(err: rusqlite::Error) -> Error {
Error::Sql(err)
}
}
@@ -79,6 +42,12 @@ impl From<failure::Error> for Error {
}
}
impl From<r2d2::Error> for Error {
fn from(err: r2d2::Error) -> Error {
Error::ConnectionPool(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
Error::Io(err)
@@ -97,60 +66,18 @@ impl From<image_meta::ImageError> for Error {
}
}
impl From<crate::dc_tools::CStringError> for Error {
fn from(err: crate::dc_tools::CStringError) -> Error {
Error::CStringError(err)
}
}
impl From<pgp::errors::Error> for Error {
fn from(err: pgp::errors::Error) -> Error {
Error::Pgp(err)
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(err: std::string::FromUtf8Error) -> Error {
Error::FromUtf8(err)
}
}
impl From<crate::blob::BlobError> for Error {
fn from(err: crate::blob::BlobError) -> Error {
Error::BlobError(err)
}
}
impl From<crate::message::InvalidMsgId> for Error {
fn from(_err: crate::message::InvalidMsgId) -> Error {
Error::InvalidMsgId
}
}
impl From<crate::key::SaveKeyError> for Error {
fn from(err: crate::key::SaveKeyError) -> Error {
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
}
}
impl From<crate::pgp::PgpKeygenError> for Error {
fn from(err: crate::pgp::PgpKeygenError) -> Error {
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)
}
}
impl From<lettre_email::error::Error> for Error {
fn from(err: lettre_email::error::Error) -> Error {
Error::LettreError(err)
}
}
impl From<mime::FromStrError> for Error {
fn from(err: mime::FromStrError) -> Error {
Error::FromStr(err)
}
}
#[macro_export]
macro_rules! bail {
($e:expr) => {

View File

@@ -1,11 +1,8 @@
//! # Events specification
use std::path::PathBuf;
use strum::EnumProperty;
use crate::chat::ChatId;
use crate::message::MsgId;
use crate::stock::StockMessage;
impl Event {
/// Returns the corresponding Event id.
@@ -22,45 +19,35 @@ 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
#[strum(props(id = "104"))]
ImapMessageDeleted(String),
/// Emitted when an IMAP message has been moved
#[strum(props(id = "105"))]
ImapMessageMoved(String),
/// Emitted when an IMAP folder was emptied
#[strum(props(id = "106"))]
ImapFolderEmptied(String),
/// Emitted when an new file in the $BLOBDIR was created
#[strum(props(id = "150"))]
NewBlobFile(String),
/// Emitted when an new file in the $BLOBDIR was created
#[strum(props(id = "151"))]
DeletedBlobFile(String),
/// The library-user should write a warning string to the log.
/// Passed to the callback given to dc_context_new().
///
/// This event should not be reported to the end-user using a popup or something like that.
///
/// @return 0
#[strum(props(id = "300"))]
Warning(String),
@@ -73,8 +60,10 @@ pub enum Event {
/// However, for ongoing processes (eg. configure())
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// 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 +80,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 +90,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 +100,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: u32 },
/// 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: u32 },
/// 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: u32 },
/// 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: u32 },
/// 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: u32 },
/// 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,29 +156,33 @@ 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),
/// Inform about the import/export progress started by imex().
/// Inform about the import/export progress started by dc_imex().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
/// @return 0
#[strum(props(id = "2051"))]
ImexProgress(usize),
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
/// A file has been exported. A file has been written by dc_imex().
/// This event may be sent multiple times by a single call to dc_imex().
///
/// A typical purpose for a handler of this event may be to make the file public to some system
/// services.
///
/// @param data2 0
/// @return 0
#[strum(props(id = "2052"))]
ImexFileWritten(PathBuf),
@@ -188,6 +198,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 +210,20 @@ 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
#[strum(props(id = "2062"))]
SecurejoinMemberAdded { chat_id: ChatId, contact_id: u32 },
// the following events are functions that should be provided by the frontends
/// Requeste a localized string from the frontend.
/// @param data1 (int) ID of the string to request, one of the DC_STR_/// constants.
/// @param data2 (int) The count. If the requested string contains a placeholder for a numeric value,
/// the ui may use this value to return different strings on different plural forms.
/// @return (const char*) Null-terminated UTF-8 string.
/// The string will be free()'d by the core,
/// so it must be allocated using malloc() or a compatible function.
/// Return 0 if the ui cannot provide the requested string
/// the core will use a default string in english language then.
#[strum(props(id = "2091"))]
GetString { id: StockMessage, count: usize },
}

View File

@@ -1,96 +0,0 @@
use crate::strum::AsStaticRef;
use mailparse::{MailHeader, MailHeaderMap, MailParseError};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[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,
ChatGroupAvatar,
ChatUserAvatar,
ChatVoiceMessage,
ChatGroupMemberRemoved,
ChatGroupMemberAdded,
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
SecureJoinGroup,
SecureJoinFingerprint,
SecureJoinInvitenumber,
SecureJoinAuth,
_TestHeader,
}
impl HeaderDef {
/// Returns the corresponding Event id.
pub fn get_headername(&self) -> &'static str {
self.as_static()
}
}
pub trait HeaderDefMap {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError>;
}
impl HeaderDefMap for [MailHeader<'_>] {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError> {
self.get_first_value(headerdef.get_headername())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
/// Test that kebab_case serialization works as expected
fn kebab_test() {
assert_eq!(HeaderDef::From_.get_headername(), "from");
assert_eq!(HeaderDef::_TestHeader.get_headername(), "test-header");
}
#[test]
/// Test that headers are parsed case-insensitively
fn test_get_header_value_case() {
let (headers, _) =
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
assert_eq!(
headers
.get_header_value(HeaderDef::AutocryptSetupMessage)
.unwrap(),
Some("v99".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::From_).unwrap(),
Some("Bob".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::Autocrypt).unwrap(),
None
);
}
}

1694
src/imap.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,104 +0,0 @@
use async_imap::{
error::{Error as ImapError, Result as ImapResult},
Client as ImapClient,
};
use async_native_tls::TlsStream;
use async_std::net::{self, TcpStream};
use super::session::Session;
use crate::login_param::{dc_build_tls, CertificateChecks};
#[derive(Debug)]
pub(crate) enum Client {
Secure(ImapClient<TlsStream<TcpStream>>),
Insecure(ImapClient<TcpStream>),
}
impl Client {
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks);
let tls_stream = tls.connect(domain.as_ref(), stream).await?;
let mut client = ImapClient::new(tls_stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Secure(client))
}
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let mut client = ImapClient::new(stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Insecure(client))
}
pub async fn secure<S: AsRef<str>>(
self,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Client> {
match self {
Client::Insecure(client) => {
let tls = dc_build_tls(certificate_checks);
let client_sec = client.secure(domain, tls).await?;
Ok(Client::Secure(client_sec))
}
// Nothing to do
Client::Secure(_) => Ok(self),
}
}
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
self,
auth_type: S,
authenticator: &A,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
self,
username: U,
password: P,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,172 +0,0 @@
use async_imap::{
error::Result as ImapResult,
types::{Capabilities, Fetch, Mailbox, Name},
Session as ImapSession,
};
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use async_std::prelude::*;
#[derive(Debug)]
pub(crate) enum Session {
Secure(ImapSession<TlsStream<TcpStream>>),
Insecure(ImapSession<TcpStream>),
}
impl Session {
pub async fn capabilities(&mut self) -> ImapResult<Capabilities> {
let res = match self {
Session::Secure(i) => i.capabilities().await?,
Session::Insecure(i) => i.capabilities().await?,
};
Ok(res)
}
pub async fn list(
&mut self,
reference_name: Option<&str>,
mailbox_pattern: Option<&str>,
) -> ImapResult<Vec<Name>> {
let res = match self {
Session::Secure(i) => {
i.list(reference_name, mailbox_pattern)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.list(reference_name, mailbox_pattern)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn create<S: AsRef<str>>(&mut self, mailbox_name: S) -> ImapResult<()> {
match self {
Session::Secure(i) => i.create(mailbox_name).await?,
Session::Insecure(i) => i.create(mailbox_name).await?,
}
Ok(())
}
pub async fn subscribe<S: AsRef<str>>(&mut self, mailbox: S) -> ImapResult<()> {
match self {
Session::Secure(i) => i.subscribe(mailbox).await?,
Session::Insecure(i) => i.subscribe(mailbox).await?,
}
Ok(())
}
pub async fn close(&mut self) -> ImapResult<()> {
match self {
Session::Secure(i) => i.close().await?,
Session::Insecure(i) => i.close().await?,
}
Ok(())
}
pub async fn select<S: AsRef<str>>(&mut self, mailbox_name: S) -> ImapResult<Mailbox> {
let mbox = match self {
Session::Secure(i) => i.select(mailbox_name).await?,
Session::Insecure(i) => i.select(mailbox_name).await?,
};
Ok(mbox)
}
pub async fn fetch<S1, S2>(&mut self, sequence_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let res = match self {
Session::Secure(i) => {
i.fetch(sequence_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.fetch(sequence_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn uid_fetch<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let res = match self {
Session::Secure(i) => {
i.uid_fetch(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.uid_fetch(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn uid_store<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let res = match self {
Session::Secure(i) => {
i.uid_store(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.uid_store(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn uid_mv<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
uid_set: S1,
mailbox_name: S2,
) -> ImapResult<()> {
match self {
Session::Secure(i) => i.uid_mv(uid_set, mailbox_name).await?,
Session::Insecure(i) => i.uid_mv(uid_set, mailbox_name).await?,
}
Ok(())
}
pub async fn uid_copy<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
uid_set: S1,
mailbox_name: S2,
) -> ImapResult<()> {
match self {
Session::Secure(i) => i.uid_copy(uid_set, mailbox_name).await?,
Session::Insecure(i) => i.uid_copy(uid_set, mailbox_name).await?,
}
Ok(())
}
}

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