Compare commits

..

6 Commits
1.52.0 ... sqlx

Author SHA1 Message Date
dignifiedquire
df546b9d2e more type fixes 2020-06-24 12:57:39 +02:00
dignifiedquire
5c13d2322a improve some typehints 2020-06-24 11:54:43 +02:00
dignifiedquire
2d5caf9d3e fix compilation 2020-06-24 11:06:26 +02:00
dignifiedquire
876e3ed58e update deps 2020-06-24 10:55:31 +02:00
dignifiedquire
cdb5f0d536 refactor(sql): switch execute to sqlx 2020-06-24 10:54:51 +02:00
dignifiedquire
0d791bb6b3 feat: start preparations for sqlx, split out migrations 2020-06-24 10:30:34 +02:00
179 changed files with 13320 additions and 33211 deletions

View File

@@ -7,8 +7,111 @@ executors:
doxygen:
docker:
- image: hrektts/doxygen
python:
docker:
- image: 3.7.7-stretch
restore-workspace: &restore-workspace
attach_workspace:
at: /mnt
restore-cache: &restore-cache
restore_cache:
keys:
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- repo-source-{{ .Branch }}-{{ .Revision }}
commands:
test_target:
parameters:
target:
type: string
steps:
- *restore-workspace
- *restore-cache
- run:
name: Test (<< parameters.target >>)
command: TARGET=<< parameters.target >> ci_scripts/run-rust-test.sh
no_output_timeout: 15m
jobs:
cargo_fetch:
executor: default
steps:
- checkout
- restore_cache:
keys:
- cargo-v3-{{ 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 fetch
- run: rustc +stable --version
- run: rustc +$(cat rust-toolchain) --version
# make sure this git repo doesn't grow too big
- run: git gc
- persist_to_workspace:
root: /mnt
paths:
- crate
- save_cache:
key: cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
paths:
- "~/.cargo"
- "~/.rustup"
rustfmt:
executor: default
steps:
- *restore-workspace
- *restore-cache
- run:
name: Run cargo fmt
command: cargo fmt --all -- --check
test_macos:
macos:
xcode: "10.0.0"
working_directory: ~/crate
steps:
- run:
name: Configure environment variables
command: |
echo 'export PATH="${HOME}/.cargo/bin:${HOME}/.bin:${PATH}"' >> $BASH_ENV
echo 'export CIRCLE_ARTIFACTS="/tmp"' >> $BASH_ENV
- checkout
- run:
name: Install Rust
command: |
curl https://sh.rustup.rs -sSf | sh -s -- -y
- run: rustup install $(cat rust-toolchain)
- run: rustup default $(cat rust-toolchain)
- run: cargo fetch
- run:
name: Test
command: TARGET=x86_64-apple-darwin ci_scripts/run-rust-test.sh
test_x86_64-unknown-linux-gnu:
executor: default
steps:
- test_target:
target: "x86_64-unknown-linux-gnu"
test_i686-unknown-linux-gnu:
executor: default
steps:
- test_target:
target: "i686-unknown-linux-gnu"
test_aarch64-linux-android:
executor: default
steps:
- test_target:
target: "aarch64-linux-android"
build_doxygen:
executor: doxygen
steps:
@@ -35,24 +138,53 @@ jobs:
- py-docs
- wheelhouse
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
clippy:
executor: default
steps:
- *restore-workspace
- *restore-cache
- run:
name: Run cargo clippy
command: cargo clippy
workflows:
version: 2.1
test:
jobs:
# - cargo_fetch
- remote_tests_python:
filters:
tags:
only: /.*/
- remote_python_packaging:
requires:
- remote_tests_python
filters:
branches:
only: master
tags:
only: /.*/
- upload_docs_wheels:
requires:
@@ -61,8 +193,38 @@ workflows:
filters:
branches:
only: master
tags:
only: /.*/
# - rustfmt:
# requires:
# - cargo_fetch
# - clippy:
# requires:
# - cargo_fetch
- build_doxygen:
filters:
branches:
only: master
tags:
only: /.*/
# Linux Desktop 64bit
# - test_x86_64-unknown-linux-gnu:
# requires:
# - cargo_fetch
# Linux Desktop 32bit
# - test_i686-unknown-linux-gnu:
# requires:
# - cargo_fetch
# Android 64bit
# - test_aarch64-linux-android:
# requires:
# - cargo_fetch
# Desktop Apple
# - test_macos:
# requires:
# - cargo_fetch

3
.gitattributes vendored
View File

@@ -12,6 +12,3 @@ test-data/* text=false
*.gif binary
*.ico binary
*.py diff=python
*.rs diff=rust
*.md diff=markdown

View File

@@ -14,11 +14,11 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.50.0
toolchain: 1.43.1
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
@@ -29,61 +29,24 @@ jobs:
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.50.0
toolchain: 1.43.1
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --examples
docs:
name: Rust doc comments
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install rust stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: rust-docs
override: true
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v1
- name: Rustdoc
uses: actions-rs/cargo@v1
with:
command: doc
args: --document-private-items --no-deps
build_and_test:
name: Build and test
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
# macOS disabled due to random failures related to caching
#os: [ubuntu-latest, windows-latest, macOS-latest]
os: [ubuntu-latest, windows-latest]
rust: [1.50.0]
experimental: [false]
# include:
# - os: ubuntu-latest
# rust: nightly
# experimental: true
# - os: windows-latest
# rust: nightly
# experimental: true
# - os: macOS-latest
# rust: nightly
# experimental: true
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [nightly, 1.43.1]
steps:
- uses: actions/checkout@master
@@ -116,10 +79,11 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests --features repl
args: --workspace --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all
args: --workspace

View File

@@ -1,21 +0,0 @@
name: Remote tests
on: [push]
jobs:
remote_tests_python:
name: Remote Python tests
runs-on: ubuntu-latest
env:
CIRCLE_BRANCH: ${{ github.ref }}
CIRCLE_JOB: remote_tests_python
CIRCLE_BUILD_NUM: ${{ github.run_number }}
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
steps:
- uses: actions/checkout@v2
- run: mkdir -m 700 -p ~/.ssh
- run: touch ~/.ssh/id_ed25519
- run: chmod 600 ~/.ssh/id_ed25519
- run: 'echo "$SSH_KEY" | base64 -d > ~/.ssh/id_ed25519'
shell: bash
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
- run: ci_scripts/remote_tests_python.sh

1
.gitignore vendored
View File

@@ -1,6 +1,5 @@
/target
**/*.rs.bk
/build
# ignore vi temporaries
*~

54
ASYNC-API-TODO.txt Normal file
View File

@@ -0,0 +1,54 @@
Delta Chat ASYNC (friedel, bjoern, floris, friedel)
- smtp fake-idle/load jobs gerade noch alle fuenf sekunden , sollte alle zehn minuten (oder gar nicht)
APIs:
dc_context_new # opens the database
dc_open # FFI only
-> drop it and move parameters to dc_context_new()
dc_configure # note: dc_start_jobs() is NOT allowed to run concurrently
dc_imex NEVER goes through the job system
dc_imex import_backup needs to ensure dc_stop_jobs()
dc_start_io # start smtp/imap and job handling subsystems
dc_stop_io # stop smtp/imap and job handling subsystems
dc_is_io_running # return 1 if smtp/imap/jobs susbystem is running
dc_close # FFI only
-> can be dropped
dc_context_unref
for ios share-extension:
Int dc_direct_send() -> try send out without going through jobs system, but queue a job in db if it needs to be retried on failure
0: message was sent
1: message failed to go out, is queued as a job to be retried later
2: message permanently failed?
EVENT handling:
start a callback thread and call get_next_event() which is BLOCKING
it's fine to start this callback thread later, it will see all events.
Note that the core infinitely fills the internal queue if you never drain it.
FFI-get_next_event() returns NULL if the context is unrefed already?
sidenote: how python's callback thread does it currently:
CB-thread runs this while loop:
while not QUITFLAG:
ev = context.get_next_event( )
...
So in order to shutdown properly one has to set QUITFLAG
before calling dc_stop_jobs() and dc_context_unref
event API:
get_data1_int
get_data2_int
get_data3_str
- userdata likely only used for the callbacks, likely can be dropped, needs verification
- iOS needs for the share app to call "try_send_smtp" wihtout a full dc_context_run and without going

View File

@@ -1,457 +1,5 @@
# Changelog
## 1.52.0
- database library changed from rusqlite to sqlx #2089 #2331 #2336 #2340
- add alias support: UIs should check for `dc_msg_get_override_sender_name()`
also in single-chats now and display divergent names and avatars #2297
- parse blockquote-tags for better quote detection #2313
- ignore unknown classical emails from spam folder #2311
- support "Mixed Up” encryption repairing #2321
- fix single chat search #2344
- fix nightly clippy and rustc errors #2341
- update dependencies #2350
- improve ci #2342
- improve python bindings #2332 #2326
## 1.51.0
- breaking change: You have to call `dc_stop_io()`/`dc_start_io()`
before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`:
fix race condition and db corruption
when a message was received during backup #2253
- save subject for messages: new api `dc_msg_get_subject()`,
when quoting, use the subject of the quoted message as the new subject,
instead of the last subject in the chat #2274 #2283
- new apis to get full or html message,
`dc_msg_has_html()` and `dc_get_msg_html()` #2125 #2151 #2264 #2279
- new chat type and apis for the new mailing list support,
`DC_CHAT_TYPE_MAILINGLIST`, `dc_msg_get_real_chat_id()`,
`dc_msg_get_override_sender_name()` #1964 #2181 #2185 #2195 #2211 #2210 #2240
#2241 #2243 #2258 #2259 #2261 #2267 #2270 #2272 #2290
- new api `dc_decide_on_contact_request()`,
deprecated `dc_create_chat_by_msg_id()` and `dc_marknoticed_contact()` #1964
- new flag `DC_GCM_INFO_ONLY` for api `dc_get_chat_msgs()` #2132
- new api `dc_get_chat_encrinfo()` #2186
- new api `dc_contact_get_status()`, returning the recent footer #2218 #2307
- improve contact name update rules,
add api `dc_contact_get_auth_name()` #2206 #2212 #2225
- new api for bots: `dc_msg_set_html()` #2153
- new api for bots: `dc_msg_set_override_sender_name()` #2231
- api removed: `dc_is_io_running()` #2139
- api removed: `dc_contact_get_first_name()` #2165 #2171
- improve compatibility with providers changing the Message-ID
(as Outlook.com) #2250 #2265
- correctly show emails that were sent to an alias and then bounced
- implement Consistent Color Generation (XEP-0392),
that results in contact colors be be changed #2228 #2229 #2239
- fetch recent existing messages
and create corresponding chats after configure #2106
- improve e-mail compatibility
by scanning all folders from time to time #2067 #2152 #2158 #2184 #2215 #2224
- better support videochat-services not supporting random rooms #2191
- export backups as .tar files #2023
- scale avatars based on media_quality, fix avatar rotation #2063
- compare ephemeral timer to parent message to deal with reordering better #2100
- better ephemeral system messages #2183
- read quotes out of html messages #2104
- prepend subject to messages with attachments, if needed #2111
- run housekeeping at least once a day #2114
- resolve MX domain only once per OAuth2 provider #2122
- configure provider based on MX record #2123 #2134
- make transient bad destination address error permanent
after n tries #2126 #2202
- enable strict TLS for known providers by default #2121
- improve and harden secure join #2154 #2161 #2251
- update `dc_get_info()` to return more information #2156
- prefer In-Reply-To/References
over group-id stored in Message-ID #2164 #2172 #2173
- apply gossiped encryption preference to new peerstates #2174
- fix: do not return quoted messages from the trash chat #2221
- fix: allow emojis for location markers #2177
- fix encoding of Chat-Group-Name-Changed messages that could even lead to
messages not being delivered #2141
- fix error when no temporary directory is available #1929
- fix marking read receipts as seen #2117
- fix read-notification for mixed-case addresses #2103
- fix decoding of attachment filenames #2080 #2094 #2102
- fix downloading ranges of message #2061
- fix parsing quoted encoded words in From: header #2193 #2204
- fix import/export race condition #2250
- fix: exclude muted chats from notified-list #2269 #2275
- fix: update uid_next if the server rewind it #2288
- fix: return error on fingerprint mismatch on qr-scan #2295
- fix ci #2217 #2226 #2244 #2245 #2249 #2277 #2286
- try harder on backup opening #2148
- trash messages more thoroughly #2273
- nicer logging #2284
- add CMakeLists.txt #2260
- switch to rust 1.50, update toolchains, deps #2150 #2155 #2165 #2107 #2262 #2271
- improve python bindings #2113 #2115 #2133 #2214
- improve documentation #2143 #2160 #2175 #2146
- refactorings #2110 #2136 #2135 #2168 #2178 #2189 #2190 #2198 #2197 #2201 #2196
#2200 #2230 #2262 #2203
- update provider-database #2299
## 1.50.0
- do not fetch emails in between inbox_watch disabled and enabled again #2087
- fix: do not fetch from INBOX if inbox_watch is disabled #2085
- fix: do not use STARTTLS when PLAIN connection is requested
and do not allow downgrade if STARTTLS is not available #2071
## 1.49.0
- add timestamps to image and video filenames #2068
- forbid quoting messages from another context #2069
- fix: preserve quotes in messages with attachments #2070
## 1.48.0
- `fetch_existing` renamed to `fetch_existing_msgs` and disabled by default
#2035 #2042
- skip fetch existing messages/contacts if config-option `bot` set #2017
- always log why a message is sorted to trash #2045
- display a quote if top posting is detected #2047
- add ephemeral task cancellation to `dc_stop_io()`;
before, there was no way to quickly terminate pending ephemeral tasks #2051
- when saved-messages chat is deleted,
a device-message about recreation is added #2050
- use `max_smtp_rcpt_to` from provider-db,
sending messages to many recipients in configurable chunks #2056
- fix handling of empty autoconfigure files #2027
- fix adding saved messages to wrong chats on multi-device #2034 #2039
- fix hang on android4.4 and other systems
by adding a workaround to executer-blocking-handling bug #2040
- fix secret key export/import roundtrip #2048
- fix mistakenly unarchived chats #2057
- fix outdated-reminder test that fails only 7 days a year,
including halloween :) #2059
- improve python bindings #2021 #2036 #2038
- update provider-database #2037
## 1.47.0
- breaking change: `dc_update_device_chats()` removed;
this is now done automatically during configure
unless the new config-option `bot` is set #1957
- breaking change: split `DC_EVENT_MSGS_NOTICED` off `DC_EVENT_MSGS_CHANGED`
and remove `dc_marknoticed_all_chats()` #1942 #1981
- breaking change: remove unused starring options #1965
- breaking change: `DC_CHAT_TYPE_VERIFIED_GROUP` replaced by
`dc_chat_is_protected()`; also single-chats may be protected now, this may
happen over the wire even if the UI do not offer an option for that #1968
- breaking change: split quotes off message text,
UIs should use at least `dc_msg_get_quoted_text()` to show quotes now #1975
- new api for quote handling: `dc_msg_set_quote()`, `dc_msg_get_quoted_text()`,
`dc_msg_get_quoted_msg()` #1975 #1984 #1985 #1987 #1989 #2004
- require quorum to enable encryption #1946
- speed up and clean up account creation #1912 #1927 #1960 #1961
- configure now collects recent contacts and fetches last messages
unless disabled by `fetch_existing` config-option #1913 #2003
EDIT: `fetch_existing` renamed to `fetch_existing_msgs` in 1.48.0 #2042
- emit `DC_EVENT_CHAT_MODIFIED` on contact rename
and set contact-id on `DC_EVENT_CONTACTS_CHANGED` #1935 #1936 #1937
- add `dc_set_chat_protection()`; the `protect` parameter in
`dc_create_group_chat()` will be removed in an upcoming release;
up to then, UIs using the "verified group" paradigm
should not use `dc_set_chat_protection()` #1968 #2014 #2001 #2012 #2007
- remove unneeded `DC_STR_COUNT` #1991
- mark all failed messages as failed when receiving an NDN #1993
- check some easy cases for bad system clock and outdated app #1901
- fix import temporary directory usage #1929
- fix forcing encryption for reset peers #1998
- fix: do not allow to save drafts in non-writeable chats #1997
- fix: do not show HTML if there is no content and there is an attachment #1988
- fix recovering offline/lost connections, fixes background receive bug #1983
- fix ordering of accounts returned by `dc_accounts_get_all()` #1909
- fix whitespace for summaries #1938
- fix: improve sentbox name guessing #1941
- fix: avoid manual poll impl for accounts events #1944
- fix encoding newlines in param as a preparation for storing quotes #1945
- fix: internal and ffi error handling #1967 #1966 #1959 #1911 #1916 #1917 #1915
- fix ci #1928 #1931 #1932 #1933 #1934 #1943
- update provider-database #1940 #2005 #2006
- update dependencies #1919 #1908 #1950 #1963 #1996 #2010 #2013
## 1.46.0
- breaking change: `dc_configure()` report errors in
`DC_EVENT_CONFIGURE_PROGRESS`: capturing error events is no longer working
#1886 #1905
- breaking change: removed `DC_LP_{IMAP|SMTP}_SOCKET*` from `server_flags`;
added `mail_security` and `send_security` using `DC_SOCKET` enum #1835
- parse multiple servers in Mozilla autoconfig #1860
- try multiple servers for each protocol #1871
- do IMAP and SMTP configuration in parallel #1891
- configuration cleanup and speedup #1858 #1875 #1889 #1904 #1906
- secure-join cleanup, testing, fixing #1876 #1877 #1887 #1888 #1896 #1899 #1900
- do not reset peerstate on encrypted messages,
ignore reordered autocrypt headers #1885 #1890
- always sort message replies after parent message #1852
- add an index to significantly speed up `get_fresh_msg_cnt()` #1881
- improve mimetype guessing for PDF and many other formats #1857 #1861
- improve accepting invalid html #1851
- improve tests, cleanup and ci #1850 #1856 #1859 #1861 #1884 #1894 #1895
- tweak HELO command #1908
- make `dc_accounts_get_all()` return accounts sorted #1909
- fix KML coordinates precision used for location streaming #1872
- fix cancelling import/export #1855
## 1.45.0
- add `dc_accounts_t` account manager object and related api functions #1784
- add capability to import backups as .tar files,
which will become the default in a subsequent release #1749
- try various server domains on configuration #1780 #1838
- recognize .tgs files as stickers #1826
- remove X-Mailer debug header #1819
- improve guessing message types from extension #1818
- fix showing unprotected subjects in encrypted messages #1822
- fix threading in interaction with non-delta-clients #1843
- fix handling if encryption degrades #1829
- fix webrtc-servers names set by the user #1831
- update provider database #1828
- update async-imap to fix Oauth2 #1837
- optimize jpeg assets with trimage #1840
- add tests and documentations #1809 #1820
## 1.44.0
- fix peerstate issues #1800 #1805
- fix a crash related to muted chats #1803
- fix incorrect dimensions sometimes reported for images #1806
- fixed `dc_chat_get_remaining_mute_duration` function #1807
- handle empty tags (e.g. `<br/>`) in HTML mails #1810
- always translate the message about disappearing messages timer change #1813
- improve footer detection in plain text email #1812
- update device chat icon to fix warnings in iOS logs #1802
- fix deletion of multiple messages #1795
## 1.43.0
- improve using own jitsi-servers #1785
- fix smtp-timeout tweaks for larger mails #1797
- more bug fixes and updates #1794 #1792 #1789 #1787
## 1.42.0
- new qr-code type `DC_QR_WEBRTC` #1779
- new `dc_chatlist_get_summary2()` api #1771
- tweak smtp-timeout for larger mails #1782
- optimize read-receipts #1765
- Allow http scheme for DCACCOUNT URLs #1770
- improve tests #1769
- bug fixes #1766 #1772 #1773 #1775 #1776 #1777
## 1.41.0
- new apis to initiate video chats #1718 #1735
- new apis `dc_msg_get_ephemeral_timer()`
and `dc_msg_get_ephemeral_timestamp()`
- new api `dc_chatlist_get_summary2()` #1771
- improve IMAP handling #1703 #1704
- improve ephemeral messages #1696 #1705
- mark location-messages as auto-generated #1715
- multi-device avatar-sync #1716 #1717
- improve python bindings #1732 #1733 #1738 #1769
- Allow http scheme for DCACCOUNT urls #1770
- more fixes #1702 #1706 #1707 #1710 #1719 #1721
#1723 #1734 #1740 #1744 #1748 #1760 #1766 #1773 #1765
- refactorings #1712 #1714 #1757
- update toolchains and dependencies #1726 #1736 #1737 #1742 #1743 #1746
## 1.40.0
- introduce ephemeral messages #1540 #1680 #1683 #1684 #1691 #1692
- `DC_MSG_ID_DAYMARKER` gets timestamp attached #1677 #1685
- improve idle #1690 #1688
- fix message processing issues by sequential processing #1694
- refactorings #1670 #1673
## 1.39.0
- fix handling of `mvbox_watch`, `sentbox_watch`, `inbox_watch` #1654 #1658
@@ -1035,3 +583,4 @@
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

View File

@@ -1,28 +0,0 @@
cmake_minimum_required(VERSION 3.16)
project(deltachat)
find_program(CARGO cargo)
add_custom_command(
OUTPUT
"target/release/libdeltachat.a"
"target/release/libdeltachat.so"
"target/release/pkgconfig/deltachat.pc"
COMMAND PREFIX=${CMAKE_INSTALL_PREFIX} ${CARGO} build --package deltachat_ffi --release
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_custom_target(
lib_deltachat
ALL
DEPENDS
"target/release/libdeltachat.a"
"target/release/libdeltachat.so"
"target/release/pkgconfig/deltachat.pc"
)
include(GNUInstallDirs)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.so" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

2438
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +1,83 @@
[package]
name = "deltachat"
version = "1.52.0"
version = "1.39.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
[profile.dev]
debug = 0
[profile.release]
lto = true
[dependencies]
ansi_term = { version = "0.12.1", optional = true }
anyhow = "1.0.28"
async-imap = "0.4.0"
async-native-tls = { version = "0.3.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
async-std-resolver = "0.19.5"
async-std = { version = "~1.8.0", features = ["unstable"] }
async-tar = "0.3.0"
async-trait = "0.1.31"
backtrace = "0.3.33"
base64 = "0.13"
bitflags = "1.1.0"
byteorder = "1.3.1"
charset = "0.1"
chrono = "0.4.6"
dirs = { version = "3.0.1", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
escaper = "0.1.0"
futures = "0.3.4"
hex = "0.4.0"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
indexmap = "1.3.0"
itertools = "0.10.0"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
log = {version = "0.4.8", optional = true }
mailparse = "0.13.0"
native-tls = "0.2.3"
num_cpus = "1.13.0"
pgp = { version = "0.6.0", default-features = false }
hex = "0.4.0"
sha2 = "0.9.0"
rand = "0.7.0"
smallvec = "1.0.0"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
num-derive = "0.3.0"
num-traits = "0.2.6"
once_cell = "1.4.1"
async-smtp = { version = "0.3" }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
async-imap = "0.3.1"
async-native-tls = { version = "0.3.3" }
async-std = { version = "1.6.1", features = ["unstable"] }
base64 = "0.12"
charset = "0.1"
percent-encoding = "2.0"
pgp = { version = "0.7.0", default-features = false }
pretty_env_logger = { version = "0.4.0", optional = true }
quick-xml = "0.18.1"
rand = "0.7.0"
regex = "1.1.6"
rust-hsluv = "0.1.4"
rustyline = { version = "4.1.0", optional = true }
sanitize-filename = "0.3.0"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.9.3"
sha2 = "0.9.0"
smallvec = "1.0.0"
sqlx = { git = "https://github.com/launchbadge/sqlx", branch = "master", features = ["runtime-async-std-native-tls", "sqlite"] }
# keep in sync with sqlx
libsqlite3-sys = { version = "0.22.0", default-features = false, features = [ "pkg-config", "vcpkg", "bundled" ] }
serde_json = "1.0"
chrono = "0.4.6"
indexmap = "1.3.0"
kamadak-exif = "0.5"
lazy_static = "1.4.0"
regex = "1.1.6"
strum = "0.18.0"
strum_macros = "0.18.0"
backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.8.0"
image-meta = "0.1.0"
quick-xml = "0.18.1"
escaper = "0.1.0"
bitflags = "1.1.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
strum = "0.20.0"
strum_macros = "0.20.1"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
mailparse = "0.12.1"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
futures = "0.3.4"
thiserror = "1.0.14"
toml = "0.5.6"
anyhow = "1.0.28"
async-trait = "0.1.31"
url = "2.1.1"
uuid = { version = "0.8", features = ["serde", "v4"] }
async-std-resolver = "0.19.5"
sqlx = { git = "https://github.com/launchbadge/sqlx", branch = "master", features = ["runtime-async-std", "sqlite", "macros"] }
libsqlite3-sys = { version = "0.18", features = ["bundled", "min_sqlite_version_3_7_16"] }
pretty_env_logger = { version = "0.4.0", optional = true }
log = { version = "0.4.8", optional = true }
rustyline = { version = "4.1.0", optional = true }
ansi_term = { version = "0.12.1", optional = true }
[dev-dependencies]
ansi_term = "0.12.0"
async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
criterion = "0.3"
futures-lite = "1.7.0"
log = "0.4.11"
tempfile = "3.0"
pretty_assertions = "0.6.1"
pretty_env_logger = "0.4.0"
proptest = "0.10"
tempfile = "3.0"
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
smol = "0.1.11"
log = "0.4.8"
[workspace]
members = [
"deltachat-ffi",
"deltachat_derive",
]
[[example]]
@@ -99,13 +91,9 @@ path = "examples/repl/main.rs"
required-features = ["repl"]
[[bench]]
name = "create_account"
harness = false
[features]
default = []
internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term"]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
nightly = ["pgp/nightly"]

View File

@@ -17,7 +17,7 @@ $ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`:
```
$ RUST_LOG=repl=info cargo run --example repl --features repl -- ~/deltachat-db
$ RUST_LOG=info cargo run --example repl --features repl -- ~/deltachat-db
```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
@@ -95,8 +95,7 @@ $ cargo build -p deltachat_ffi --release
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
- `RUST_LOG=repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
SMTP tracing in addition to info messages.
### Expensive tests
@@ -123,7 +122,7 @@ 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/deltachat/go-deltachat/)
- [Go](https://github.com/hugot/go-deltachat/)
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
- **Java** and **Swift** (contained in the Android/iOS repos)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -1,26 +0,0 @@
use async_std::path::PathBuf;
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::accounts::Accounts;
use tempfile::tempdir;
async fn create_accounts(n: u32) {
let dir = tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
for expected_id in 2..n {
let id = accounts.add_account().await.unwrap();
assert_eq!(id, expected_id);
}
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("create 1 account", |b| {
b.iter(|| block_on(async { create_accounts(black_box(1)).await }))
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -1,15 +1,11 @@
# Continuous Integration Scripts for Delta Chat
Continuous Integration, run through [GitHub
Actions](https://docs.github.com/actions),
[CircleCI](https://app.circleci.com/) and an own build machine.
Continuous Integration, run through CircleCI and an own build machine.
## Description of scripts
- `../.github/workflows` contains jobs run by GitHub Actions.
- `../.circleci/config.yml` describing the build jobs that are run
by CircleCI.
by Circle-CI
- `remote_tests_python.sh` rsyncs to a build machine and runs
`run-python-test.sh` remotely on the build machine.

View File

@@ -10,13 +10,8 @@ set -xe
PYDOCDIR=${1:?directory with python docs}
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
SSHTARGET=ci@b1.delta.chat
# if CIRCLE_BRANCH is not set we are called for a tag with empty CIRCLE_BRANCH variable.
export BRANCH=${CIRCLE_BRANCH:master}
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}/wheelhouse
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
# python docs to py.delta.chat
@@ -40,36 +35,23 @@ echo upload wheels
echo -----------------------
# Bundle external shared libraries into the wheels
pushd $WHEELHOUSEDIR
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $SSHTARGET mkdir -p $BUILDDIR
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ci_scripts/cleanup_devpi_indices.py $SSHTARGET:$BUILDDIR
rsync -avz \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
$WHEELHOUSEDIR \
$SSHTARGET:$BUILDDIR
pip3 install -U pip setuptools
pip3 install devpi-client
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
N_BRANCH=${BRANCH//[\/]}
# we rely on the "venv" virtualenv on the remote account to exist
source venv/bin/activate
cd $BUILDDIR
devpi use dc/$N_BRANCH || {
devpi index -c $N_BRANCH
devpi use dc/$N_BRANCH
}
devpi index $N_BRANCH bases=/root/pypi
devpi upload deltachat*
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN
popd
N_BRANCH=${BRANCH//[\/]}
devpi use dc/\$N_BRANCH || {
devpi index -c \$N_BRANCH
devpi use dc/\$N_BRANCH
}
devpi index \$N_BRANCH bases=/root/pypi
devpi upload wheelhouse/deltachat*
# remove devpi non-master dc indices if thy are too old
# this script was copied above
python cleanup_devpi_indices.py
_HERE
# remove devpi non-master dc indices if thy are too old
python ci_scripts/cleanup_devpi_indices.py

View File

@@ -16,6 +16,6 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK 1
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
# Install Rust nightly
ADD deps/build_rust.sh /builder/build_rust.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1

View File

@@ -3,9 +3,9 @@
set -e -x
# Install Rust
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.50.0-x86_64-unknown-linux-gnu -y
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.43.1-x86_64-unknown-linux-gnu -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/1.50.0-x86_64-unknown-linux-gnu/share
rm -rf /root/.rustup/toolchains/1.43.1-x86_64-unknown-linux-gnu/share

View File

@@ -45,6 +45,7 @@ if [ -n "$TESTS" ]; then
# messages and rust's imap code likely has concurrency problems)
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
unset DCC_PY_LIVECONFIG
unset DCC_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
tox --workdir "$TOXWORKDIR" -e auditwheels

View File

@@ -1,9 +1,11 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:-master}
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
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"
@@ -28,13 +30,13 @@ set +x
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
set +x -e
shopt -s huponexit
cd $BUILDDIR
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
set -x
# run everything else inside docker
docker run -e DCC_NEW_TMP_EMAIL \
docker run -e DCC_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh

View File

@@ -1,9 +1,11 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:-master}
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
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"
@@ -22,13 +24,12 @@ echo "--- Running $CIRCLE_JOB remotely"
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
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
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
#we rely on tox/virtualenv being available in the host

View File

@@ -1,9 +1,11 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:-master}
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
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
@@ -18,10 +20,10 @@ echo "--- Running $CIRCLE_JOB remotely"
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
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

View File

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

View File

@@ -4,7 +4,6 @@
# and tox/pytest.
set -e -x
shopt -s huponexit
# for core-building and python install step
export DCC_RS_TARGET=debug

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env bash
set -ex
shopt -s huponexit
#export RUST_TEST_THREADS=1
export RUST_BACKTRACE=1

View File

@@ -15,7 +15,6 @@ cargo build --release -p deltachat_ffi
# Statically link against libdeltachat.a.
export DCC_RS_DEV=$(pwd)
export DCC_RS_TARGET=release
# Configure access to a base python and to several python interpreters
# needed by tox below.
@@ -39,6 +38,7 @@ mkdir -p $TOXWORKDIR
# 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
unset DCC_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
popd

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.52.0"
version = "1.39.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -23,7 +23,6 @@ serde_json = "1.0"
async-std = "1.6.0"
anyhow = "1.0.28"
thiserror = "1.0.14"
rand = "0.7.3"
[features]
default = ["vendored"]

View File

@@ -236,6 +236,12 @@ TAB_SIZE = 4
ALIASES =
# This tag can be used to specify a number of word-keyword mappings (TCL only).
# A mapping has the form "name=value". For example adding "class=itcl::class"
# will allow you to use the command class in the itcl::class meaning.
TCL_SUBST =
# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
# only. Doxygen will then generate output that is more tailored for C. For
# instance, some of the names that are used will be different. The list of all

View File

@@ -1,38 +0,0 @@
<doxygenlayout version="1.0">
<!-- Generated by doxygen 1.8.20 -->
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="yes" title=""/>
<tab type="classes" visible="yes" title="">
<tab type="classlist" visible="no" title="" intro=""/>
<tab type="classindex" visible="no" title=""/>
<tab type="hierarchy" visible="no" title="" intro=""/>
<tab type="classmembers" visible="no" title="" intro=""/>
</tab>
<tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
<tab type="pages" visible="yes" title="" intro=""/>
<tab type="namespaces" visible="yes" title="">
<tab type="namespacelist" visible="yes" title="" intro=""/>
<tab type="namespacemembers" visible="yes" title="" intro=""/>
</tab>
<tab type="interfaces" visible="yes" title="">
<tab type="interfacelist" visible="yes" title="" intro=""/>
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
<tab type="interfacehierarchy" visible="yes" title="" intro=""/>
</tab>
<tab type="structs" visible="yes" title="">
<tab type="structlist" visible="yes" title="" intro=""/>
<tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
</tab>
<tab type="exceptions" visible="yes" title="">
<tab type="exceptionlist" visible="yes" title="" intro=""/>
<tab type="exceptionindex" visible="$ALPHABETICAL_INDEX" title=""/>
<tab type="exceptionhierarchy" visible="yes" title="" intro=""/>
</tab>
<tab type="files" visible="yes" title="">
<tab type="filelist" visible="yes" title="" intro=""/>
<tab type="globals" visible="yes" title="" intro=""/>
</tab>
<tab type="examples" visible="yes" title="" intro=""/>
</navindex>
</doxygenlayout>

View File

@@ -19,10 +19,10 @@ fn main() {
include_str!("deltachat.pc.in"),
name = "deltachat",
description = env::var("CARGO_PKG_DESCRIPTION").unwrap(),
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or("".to_string()),
version = env::var("CARGO_PKG_VERSION").unwrap(),
libs_priv = libs_priv,
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
prefix = env::var("PREFIX").unwrap_or("/usr/local".to_string()),
);
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +1,46 @@
use crate::chat::ChatItem;
use crate::constants::{DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
use crate::location::Location;
use crate::message::MsgId;
/* * the structure behind dc_array_t */
#[derive(Debug, Clone)]
pub enum dc_array_t {
MsgIds(Vec<MsgId>),
Chat(Vec<ChatItem>),
Locations(Vec<Location>),
Uint(Vec<u32>),
}
impl dc_array_t {
pub(crate) fn get_id(&self, index: usize) -> u32 {
pub fn new(capacity: usize) -> Self {
dc_array_t::Uint(Vec::with_capacity(capacity))
}
/// Constructs a new, empty `dc_array_t` holding locations with specified `capacity`.
pub fn new_locations(capacity: usize) -> Self {
dc_array_t::Locations(Vec::with_capacity(capacity))
}
pub fn add_id(&mut self, item: u32) {
if let Self::Uint(array) = self {
array.push(item);
} else {
panic!("Attempt to add id to array of other type");
}
}
pub fn add_location(&mut self, location: Location) {
if let Self::Locations(array) = self {
array.push(location)
} else {
panic!("Attempt to add a location to array of other type");
}
}
pub fn get_id(&self, index: usize) -> u32 {
match self {
Self::MsgIds(array) => array[index].to_u32(),
Self::Chat(array) => match array[index] {
ChatItem::Message { msg_id } => msg_id.to_u32(),
ChatItem::Marker1 => DC_MSG_ID_MARKER1,
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
},
Self::Locations(array) => array[index].location_id,
Self::Uint(array) => array[index],
Self::Uint(array) => array[index] as u32,
}
}
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
match self {
Self::MsgIds(_) => None,
Self::Chat(array) => array.get(index).and_then(|item| match item {
ChatItem::Message { .. } => None,
ChatItem::Marker1 { .. } => None,
ChatItem::DayMarker { timestamp } => Some(*timestamp),
}),
Self::Locations(array) => array.get(index).map(|location| location.timestamp),
Self::Uint(_) => None,
}
}
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
match self {
Self::MsgIds(_) => None,
Self::Chat(_) => None,
Self::Locations(array) => array
.get(index)
.and_then(|location| location.marker.as_deref()),
Self::Uint(_) => None,
}
}
pub(crate) fn get_location(&self, index: usize) -> &Location {
pub fn get_location(&self, index: usize) -> &Location {
if let Self::Locations(array) = self {
&array[index]
} else {
@@ -58,18 +48,55 @@ impl dc_array_t {
}
}
/// Returns the number of elements in the array.
pub(crate) fn len(&self) -> usize {
pub fn is_empty(&self) -> bool {
match self {
Self::Locations(array) => array.is_empty(),
Self::Uint(array) => array.is_empty(),
}
}
/// Returns the number of elements in the array.
pub fn len(&self) -> usize {
match self {
Self::MsgIds(array) => array.len(),
Self::Chat(array) => array.len(),
Self::Locations(array) => array.len(),
Self::Uint(array) => array.len(),
}
}
pub(crate) fn search_id(&self, needle: u32) -> Option<usize> {
(0..self.len()).find(|i| self.get_id(*i) == needle)
pub fn clear(&mut self) {
match self {
Self::Locations(array) => array.clear(),
Self::Uint(array) => array.clear(),
}
}
pub fn search_id(&self, needle: u32) -> Option<usize> {
if let Self::Uint(array) = self {
for (i, &u) in array.iter().enumerate() {
if u == needle {
return Some(i);
}
}
None
} else {
panic!("Attempt to search for id in array of other type");
}
}
pub fn sort_ids(&mut self) {
if let dc_array_t::Uint(v) = self {
v.sort();
} else {
panic!("Attempt to sort array of something other than uints");
}
}
pub fn as_ptr(&self) -> *const u32 {
if let dc_array_t::Uint(v) = self {
v.as_ptr()
} else {
panic!("Attempt to convert array of something other than uints to raw");
}
}
}
@@ -79,18 +106,6 @@ impl From<Vec<u32>> for dc_array_t {
}
}
impl From<Vec<MsgId>> for dc_array_t {
fn from(array: Vec<MsgId>) -> Self {
dc_array_t::MsgIds(array)
}
}
impl From<Vec<ChatItem>> for dc_array_t {
fn from(array: Vec<ChatItem>) -> Self {
dc_array_t::Chat(array)
}
}
impl From<Vec<Location>> for dc_array_t {
fn from(array: Vec<Location>) -> Self {
dc_array_t::Locations(array)
@@ -103,11 +118,12 @@ mod tests {
#[test]
fn test_dc_array() {
let arr: dc_array_t = Vec::<u32>::new().into();
assert!(arr.len() == 0);
let mut arr = dc_array_t::new(7);
assert!(arr.is_empty());
let ids: Vec<u32> = (2..1002).collect();
let arr: dc_array_t = ids.into();
for i in 0..1000 {
arr.add_id(i + 2);
}
assert_eq!(arr.len(), 1000);
@@ -115,15 +131,31 @@ mod tests {
assert_eq!(arr.get_id(i), (i + 2) as u32);
}
assert_eq!(arr.search_id(10), Some(8));
assert_eq!(arr.search_id(1), None);
arr.clear();
assert!(arr.is_empty());
arr.add_id(13);
arr.add_id(7);
arr.add_id(666);
arr.add_id(0);
arr.add_id(5000);
arr.sort_ids();
assert_eq!(arr.get_id(0), 0);
assert_eq!(arr.get_id(1), 7);
assert_eq!(arr.get_id(2), 13);
assert_eq!(arr.get_id(3), 666);
}
#[test]
#[should_panic]
fn test_dc_array_out_of_bounds() {
let ids: Vec<u32> = (2..1002).collect();
let arr: dc_array_t = ids.into();
let mut arr = dc_array_t::new(7);
for i in 0..1000 {
arr.add_id(i + 2);
}
arr.get_id(1000);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -105,9 +105,8 @@ 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| {
let std::ffi::NulError { .. } = err;
CStringError::InteriorNullByte
CString::new(self.as_ref().as_bytes()).map_err(|err| match err {
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
})
}
@@ -123,9 +122,8 @@ 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| {
let std::ffi::NulError { .. } = err;
CStringError::InteriorNullByte
Some(val) => CString::new(val.as_bytes()).map_err(|err| match err {
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
}),
None => Err(CStringError::NotUnicode),
}
@@ -241,7 +239,7 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
/// [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] slice can not outlive
/// Because this returns a reference the [Path] silce can not outlive
/// the original pointer.
///
/// [Path]: std::path::Path

View File

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

View File

@@ -0,0 +1,40 @@
#![recursion_limit = "128"]
extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
// For now, assume (not check) that these macroses are applied to enum without
// data. If this assumption is violated, compiler error will point to
// generated code, which is not very user-friendly.
#[proc_macro_derive(Sqlx)]
pub fn sqlx_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
impl<'q> sqlx::encode::Encode<'q, sqlx::sqlite::Sqlite> for #name {
fn encode_by_ref(&self, buf: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>) -> sqlx::encode::IsNull {
num_traits::ToPrimitive::to_i32(self).expect("invalid type").encode(buf)
}
}
impl<'de> sqlx::decode::Decode<'de, sqlx::sqlite::Sqlite> for #name {
fn decode(value: sqlx::sqlite::SqliteValueRef) -> std::result::Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
let raw: i32 = sqlx::decode::Decode::decode(value)?;
Ok(num_traits::FromPrimitive::from_i32(raw).unwrap_or_default())
}
}
impl sqlx::types::Type<sqlx::sqlite::Sqlite> for #name {
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
<i32 as sqlx::types::Type<_>>::type_info()
}
}
};
gen.into()
}

View File

@@ -5,95 +5,69 @@ Problem: missing eventual group consistency
If group members are concurrently adding new members,
the new members will miss each other's additions, example:
1. Alice and Bob are in a two-member group
- Alice and Bob are in a two-member group
2. Then Alice adds Carol, while concurrently Bob adds Doris
- Alice adds Carol, concurrently Bob adds Doris
Right now, the group has inconsistent memberships:
- Carol will see a three-member group (Alice, Bob, Carol),
Doris will see a different three-member group (Alice, Bob, Doris),
and only Alice and Bob will have all four members.
- Alice and Carol see a (Alice, Carol, Bob) group
- Bob and Doris see a (Bob, Doris, Alice)
This then leads to "sender is unknown" messages in the chat,
for example when Alice receives a message from Doris,
or when Bob receives a message from Carol.
There are also other sources for group membership inconsistency:
- leaving/deleting/adding in larger groups, while being offline,
increases chances for inconsistent group membership
- dropped group-membership messages
- group-membership messages landing in "Spam"
Note that for verified groups any mitigation mechanism likely
needs to make all clients to know who originally added a member.
Note that all these problems (can) also happen with verified groups,
then raising "false alarms" which could lure people to ignore such issues.
solution: memorize+attach (possible encrypted) chat-meta mime messages
----------------------------------------------------------------------
IOW, it's clear we need to do something about it to improve overall
reliability in group-settings.
For reference, please see https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members how MemberAdded/Removed messages are shaped.
- All Chat-Group-Member-Added/Removed messages are recorded in their
full raw (signed and encrypted) mime-format in the DB
Solution: replay group modification messages on inconsistencies
------------------------------------------------------------------
- If an incoming member-add/member-delete messages has a member list
which is, apart from the added/removed member, not consistent
with our own view, broadcast a "Chat-Group-Member-Correction" message to
all members, attaching the original added/removed mime-message for all mismatching
contacts. If we have no relevant add/del information, don't send a
correction message out.
For brevity let's abbreviate "group membership modification" as **GMM**.
- Upong receiving added/removed attachments we don't do the
check_consistency+correction message dance.
This avoids recursion problems and hard-to-reason-about chatter.
Delta chat has explicit GMM messages, typically encrypted to the group members
as seen by the device that sends the GMM. The `Spec <https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members>`_ details the Mime headers and format.
Notes:
If we detect membership inconsistencies we can resend relevant GMM messages
to the respective chat. The receiving devices can process those GMM messages
as if it would be an incoming message. If for example they have already seen
the Message-ID of the GMM message, they will ignore the message. It's
probably useful to record GMM message in their original MIME-format and
not invent a new recording format. Few notes on three aspects:
- mechanism works for both encrypted and unencrypted add/del messages
- **group-membership-tracking**: All valid GMM messages are persisted in
their full raw (signed/encrypted?) MIME-format in the DB. Note that GMM messages
already are in the msgs table, and there is a mime_header column which we could
extend to contain the raw Mime GMM message.
- we already have a "mime_headers" column in the DB for each incoming message.
We could extend it to also include the payload and store mime unconditionally
for member-added/removed messages.
- **consistency_checking**: If an incoming GMM has a member list which is
not consistent with our own view, broadcast a "Group-Member-Correction"
message to all members containing a multipart list of GMMs.
- multiple member-added/removed messages can be attached in a single
correction message
- **correcting_memberships**: Upon receiving a Group-Member-Correction
message we pass the contained GMMs to the "incoming mail pipeline"
(without **consistency_checking** them, to avoid recursion issues)
- it is minimal on the number of overall messages to reach group consistency
(best-case: no extra messages, the ABCD case above: max two extra messages)
- somewhat backward compatible: older clients will probably ignore
messages which are signed by someone who is not the outer From-address.
Alice/Carol and Bob/Doris getting on the same page
++++++++++++++++++++++++++++++++++++++++++++++++++
Recall that Alice/Carol and Bob/Doris had a differening view of
group membership. With the proposed solution, when Bob receives
Alice's "Carol added" message, he will notice that Alice (and thus
also carol) did not know about Doris. Bob's device sends a
"Chat-Group-Member-Correction" message containing his own GMM
when adding Doris. Therefore, the group's membership is healed
for everyone in a single broadcast message.
Alice might also send a Group-member-Correction message,
so there is a second chance that the group gets to know all GMMs.
Note, for example, that if for some reason Bobs and Carols provider
drop GMM messages between them (spam) that Alice and Doris can heal
it by resending GMM messages whenever they detect them to be out of sync.
- the correction-protocol also helps with dropped messages. If a member
did not see a member-added/removed message, the next member add/removed
message in the group will likely heal group consistency for this member.
- we can quite easily extend the mechanism to also provide the group-avatar or
other meta-information.
Discussions of variants
++++++++++++++++++++++++
- instead of acting on GMM messages we could send corrections
for any received message that addresses inconsistent group members but
a) this could delay group-membership healing
- instead of acting on MemberAdded/Removed message we could send
corrections for any received message that addresses inconsistent group members but
a) this would delay group-membership healing
b) could lead to a lot of members sending corrections
c) means we might rely on "To-Addresses" which we also like to strike
at least for protected chats.
- instead of broadcasting correction messages we could only send it to
the sender of the inconsistent member-added/removed message.
@@ -109,3 +83,44 @@ Discussions of variants
while both being in an offline or bad-connection situation).
solution2: repeat member-added/removed messages
---------------------------------------------------
Introduce a new Chat-Group-Member-Changed header and deprecate Chat-Group-Member-Added/Removed
but keep sending out the old headers until the new protocol is sufficiently deployed.
The new Chat-Group-Member-Changed header contains a Time-to-Live number (TTL)
which controls repetition of the signed "add/del e-mail address" payload.
Example::
Chat-Group-Member-Changed: TTL add "somedisplayname" someone@example.org
owEBYQGe/pANAwACAY47A6J5t3LWAcsxYgBeTQypYWRkICJzb21lZGlzcGxheW5h
bWUiIHNvbWVvbmVAZXhhbXBsZS5vcmcgCokBHAQAAQIABgUCXk0MqQAKCRCOOwOi
ebdy1hfRB/wJ74tgFQulicthcv9n+ZsqzwOtBKMEVIHqJCzzDB/Hg/2z8ogYoZNR
iUKKrv3Y1XuFvdKyOC+wC/unXAWKFHYzY6Tv6qDp6r+amt+ad+8Z02q53h9E55IP
FUBdq2rbS8hLGjQB+mVRowYrUACrOqGgNbXMZjQfuV7fSc7y813OsCQgi3tjstup
b+uduVzxCp3PChGhcZPs3iOGCnQvSB8VAaLGMWE2d7nTo/yMQ0Jx69x5qwfXogTk
mTt5rOJyrosbtf09TMKFzGgtqBcEqHLp3+mQpZQ+WHUKAbsRa8Jc9DOUOSKJ8SNM
clKdskprY+4LY0EBwLD3SQ7dPkTITCRD
=P6GG
TTL is set to "2" on an initial Chat-Group-Member-Changed add/del message.
Receivers will apply the add/del change to the group-membership,
decrease the TTL by 1, and if TTL>0 re-sent the header.
The "add|del e-mail address" payload is pgp-signed and repeated verbatim.
This allows to propagate, in a cryptographically secured way,
who added a member. This is particularly important for allowing
to show in verified groups who added a member (planned).
Disadvantage to solution 1:
- requires to specify encoding and precise rules for what/how is signed.
- causes O(N^2) extra messages
- Not easily extendable for other things (without introducing a new
header / encoding)

View File

@@ -1,66 +0,0 @@
simplify/streamline mark-seen/delete/move/send-mdn job handling
---------------------------------------------------------------
Idea: Introduce a new "msgwork" sql table that looks very
much like the jobs table but has a primary key "msgid"
and no job id and no foreign-id anymore. This opens up
bulk-processing by looking at the whole table and combining
flag-setting to reduce imap-roundtrips and select-folder calls.
Concretely, these IMAP jobs:
DeleteMsgOnImap
MarkseenMsgOnImap
MoveMsg
Would be replaced by a few per-message columns in the new msgwork table:
- needs_mark_seen: (bool) message shall be marked as seen on imap
- needs_to_move: (bool) message should be moved to mvbox_folder
- deletion_time: (target_time or 0) message shall be deleted at specified time
- needs_send_mdn: (bool) MDN shall be sent
The various places that currently add the (replaced) jobs
would now add/modify the respective message record in the message-work table.
Looking at a single message-work entry conceptually looks like this::
if msg.server_uid==0:
return RetryLater # nothing can be done without server_uid
if msg.deletion_time > current_time:
imap.mark_delete(msg) # might trigger early exit with a RetryLater/Failed
clear(needs_deletion)
clear(mark_seen)
if needs_mark_seen:
imap.mark_seen(msg) # might trigger early exit with a RetryLater/Failed
clear(needs_mark_seen)
if needs_send_mdn:
schedule_smtp_send_mdn(msg)
clear(needs_send_mdn)
if any_flag_set():
retrylater
# remove msgwork entry from table
Notes/Questions:
- it's unclear how much we need per-message retry-time tracking/backoff
- drafting bulk processing algo is useful before
going for the implementation, i.e. including select_folder calls etc.
- maybe it's better to not have bools for the flags but
0 (no change)
1 (set the imap flag)
2 (clear the imap flag)
and design such that we can cover all imap flags.
- It might not be neccessary to keep needs_send_mdn state in this table
if this can be decided rather when we succeed with mark_seen/mark_delete.

View File

@@ -1,30 +1,24 @@
extern crate dirs;
use std::str::FromStr;
use anyhow::{bail, ensure, Error};
use anyhow::{bail, ensure};
use async_std::path::Path;
use deltachat::chat::{
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
};
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::error::Error;
use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::lot::LotState;
use deltachat::message::{self, ContactRequestDecision, Message, MessageState, MsgId};
use deltachat::message::{self, Message, MessageState, MsgId};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::EventType;
use deltachat::Event;
use deltachat::{config, provider};
use std::fs;
use std::time::{Duration, SystemTime};
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
@@ -34,7 +28,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 1 {
context
.sql()
.execute(sqlx::query("DELETE FROM jobs;"))
.execute("DELETE FROM jobs;", paramsx![])
.await
.unwrap();
println!("(1) Jobs reset.");
@@ -42,7 +36,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 2 {
context
.sql()
.execute(sqlx::query("DELETE FROM acpeerstates;"))
.execute("DELETE FROM acpeerstates;", paramsx![])
.await
.unwrap();
println!("(2) Peerstates reset.");
@@ -50,7 +44,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 4 {
context
.sql()
.execute(sqlx::query("DELETE FROM keypairs;"))
.execute("DELETE FROM keypairs;", paramsx![])
.await
.unwrap();
println!("(4) Private keypairs reset.");
@@ -58,40 +52,41 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 8 {
context
.sql()
.execute(sqlx::query("DELETE FROM contacts WHERE id>9;"))
.execute("DELETE FROM contacts WHERE id>9;", paramsx![])
.await
.unwrap();
context
.sql()
.execute(sqlx::query("DELETE FROM chats WHERE id>9;"))
.execute("DELETE FROM chats WHERE id>9;", paramsx![])
.await
.unwrap();
context
.sql()
.execute(sqlx::query("DELETE FROM chats_contacts;"))
.execute("DELETE FROM chats_contacts;", paramsx![])
.await
.unwrap();
context
.sql()
.execute(sqlx::query("DELETE FROM msgs WHERE id>9;"))
.execute("DELETE FROM msgs WHERE id>9;", paramsx![])
.await
.unwrap();
context
.sql()
.execute(sqlx::query(
.execute(
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
))
paramsx![],
)
.await
.unwrap();
context
.sql()
.execute(sqlx::query("DELETE FROM leftgrps;"))
.execute("DELETE FROM leftgrps;", paramsx![])
.await
.unwrap();
println!("(8) Rest but server config reset.");
}
context.emit_event(EventType::MsgsChanged {
context.emit_event(Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
@@ -119,11 +114,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
real_spec = spec.to_string();
context
.sql()
.set_raw_config("import_spec", Some(&real_spec))
.set_raw_config(context, "import_spec", Some(&real_spec))
.await
.unwrap();
} else {
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
let rs = context.sql().get_raw_config("import_spec").await;
if rs.is_none() {
error!(context, "Import: No file or folder given.");
return false;
@@ -162,7 +157,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
if read_cnt > 0 {
context.emit_event(EventType::MsgsChanged {
context.emit_event(Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
@@ -174,11 +169,8 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let contact = Contact::get_by_id(context, msg.get_from_id())
.await
.expect("invalid contact");
let contact_name = if let Some(name) = msg.get_override_sender_name() {
format!("~{}", name)
} else {
contact.get_display_name().to_string()
};
let contact_name = contact.get_name();
let contact_id = contact.get_id();
let statestr = match msg.get_state() {
@@ -191,7 +183,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text();
println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{} [{}]",
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]",
prefix.as_ref(),
msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" },
@@ -199,8 +191,8 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
&contact_name,
contact_id,
msgtext.unwrap_or_default(),
if msg.has_html() { "[HAS-HTML]" } else { "" },
if msg.get_from_id() == 1 {
if msg.is_starred() { "" } else { "" },
if msg.get_from_id() == 1 as libc::c_uint {
""
} else if msg.get_state() == MessageState::InSeen {
"[SEEN]"
@@ -210,15 +202,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
msg.get_videochat_url().unwrap_or_default(),
msg.get_videochat_type().unwrap_or_default()
)
} else {
"".to_string()
},
if msg.is_forwarded() {
"[FORWARDED]"
} else {
@@ -232,7 +215,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
let mut lines_out = 0;
for &msg_id in msglist {
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
if msg_id.is_daymarker() {
println!(
"--------------------------------------------------------------------------------"
);
@@ -258,11 +241,15 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error>
}
async fn log_contactlist(context: &Context, contacts: &[u32]) {
let mut contacts = contacts.to_vec();
if !contacts.contains(&1) {
contacts.push(1);
}
for contact_id in contacts {
let line;
let mut line2 = "".to_string();
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
let name = contact.get_display_name();
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
let name = contact.get_name();
let addr = contact.get_addr();
let verified_state = contact.is_verified(context).await;
let verified_str = if VerifiedStatus::Unverified != verified_state {
@@ -288,17 +275,15 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
"addr unset"
}
);
let peerstate = Peerstate::from_addr(context, &addr)
.await
.expect("peerstate error");
if peerstate.is_some() && *contact_id != 1 {
let peerstate = Peerstate::from_addr(context, &addr).await;
if peerstate.is_ok() && contact_id != 1 as libc::c_uint {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
);
}
println!("Contact#{}: {}{}", *contact_id, line, line2);
println!("Contact#{}: {}{}", contact_id, line, line2);
}
}
}
@@ -358,8 +343,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
listarchived\n\
chat [<chat-id>|0]\n\
createchat <contact-id>\n\
createchatbymsg <msg-id>\n\
creategroup <name>\n\
createprotected <name>\n\
createverified <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
groupname <name>\n\
@@ -370,10 +356,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
dellocations\n\
getlocations [<contact-id>]\n\
send <text>\n\
send-garbage\n\
sendimage <file> [<text>]\n\
sendfile <file> [<text>]\n\
sendhtml <file for html-part> [<text for plain-part>]\n\
videochat\n\
draft [<text>]\n\
devicemsg <text>\n\
listmedia\n\
@@ -381,22 +366,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unarchive <chat-id>\n\
pin <chat-id>\n\
unpin <chat-id>\n\
mute <chat-id> [<seconds>]\n\
unmute <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
===========================Contact requests==\n\
decidestartchat <msg-id>\n\
decideblock <msg-id>\n\
decidenotnow <msg-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
msginfo <msg-id>\n\
html <msg-id>\n\
listfresh\n\
forward <msg-id> <chat-id>\n\
markseen <msg-id>\n\
star <msg-id>\n\
unstar <msg-id>\n\
delmsg <msg-id>\n\
===========================Contact commands==\n\
listcontacts [<query>]\n\
@@ -405,18 +383,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
contactinfo <contact-id>\n\
delcontact <contact-id>\n\
cleanupcontacts\n\
block <contact-id>\n\
unblock <contact-id>\n\
listblocked\n\
======================================Misc.==\n\
getqr [<chat-id>]\n\
getbadqr\n\
checkqr <qr-content>\n\
setqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
clear -- clear screen\n\
exit or quit\n\
============================================="
@@ -455,21 +430,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
has_backup(&context, blobdir).await?;
}
"export-backup" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportBackup, &dir).await?;
println!("Exported to {}.", dir.to_string_lossy());
imex(&context, ImexMode::ExportBackup, Some(blobdir)).await?;
}
"import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(&context, ImexMode::ImportBackup, arg1).await?;
imex(&context, ImexMode::ImportBackup, Some(arg1)).await?;
}
"export-keys" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportSelfKeys, &dir).await?;
println!("Exported to {}.", dir.to_string_lossy());
imex(&context, ImexMode::ExportSelfKeys, Some(blobdir)).await?;
}
"import-keys" => {
imex(&context, ImexMode::ImportSelfKeys, arg1).await?;
imex(&context, ImexMode::ImportSelfKeys, Some(blobdir)).await?;
}
"export-setup" => {
let setup_code = create_setup_code(&context);
@@ -513,7 +484,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
context.maybe_network().await;
}
"housekeeping" => {
sql::housekeeping(&context).await.ok_or_log(&context);
sql::housekeeping(&context).await?;
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
@@ -538,18 +509,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
println!(
"{}#{}: {} [{} fresh] {}{}{}",
"{}#{}: {} [{} fresh] {}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
chat.get_id().get_fresh_msg_cnt(&context).await?,
if chat.is_muted() { "🔇" } else { "" },
chat.get_id().get_fresh_msg_cnt(&context).await,
match chat.visibility {
ChatVisibility::Normal => "",
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
if chat.is_protected() { "🛡️" } else { "" },
);
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
let statestr = if chat.visibility == ChatVisibility::Archived {
@@ -584,7 +553,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
);
}
}
if location::is_sending_locations_to_chat(&context, None).await {
if location::is_sending_locations_to_chat(&context, ChatId::new(0)).await {
println!("Location streaming enabled.");
}
println!("{} chats", cnt);
@@ -605,33 +574,21 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let sel_chat = sel_chat.as_ref().unwrap();
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?;
let msglist: Vec<MsgId> = msglist
.into_iter()
.map(|x| match x {
ChatItem::Message { msg_id } => msg_id,
ChatItem::Marker1 => MsgId::new(DC_MSG_ID_MARKER1),
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
})
.collect();
let members = chat::get_chat_contacts(&context, sel_chat.id).await?;
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]).await?;
contact.get_addr().to_string()
} else if sel_chat.get_type() == Chattype::Mailinglist && !members.is_empty() {
"mailinglist".to_string()
} else {
format!("{} member(s)", members.len())
};
println!(
"{}#{}: {} [{}]{}{}{} {}",
"{}#{}: {} [{}]{}{}",
chat_prefix(sel_chat),
sel_chat.get_id(),
sel_chat.get_name(),
subtitle,
if sel_chat.is_muted() { "🔇" } else { "" },
if sel_chat.is_sending_locations() {
"📍"
} else {
@@ -644,11 +601,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
_ => "".to_string(),
},
if sel_chat.is_protected() {
"🛡️"
} else {
""
},
);
log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
@@ -657,69 +609,47 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!(
"{} messages.",
sel_chat.get_id().get_msg_cnt(&context).await?
sel_chat.get_id().get_msg_cnt(&context).await
);
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
}
"createchat" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id: u32 = arg1.parse()?;
let chat_id = chat::create_by_contact_id(&context, contact_id).await?;
let contact_id: libc::c_int = arg1.parse()?;
let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?;
println!("Single#{} created successfully.", chat_id,);
}
"decidestartchat" | "createchatbymsg" => {
"createchatbymsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
match message::decide_on_contact_request(
&context,
msg_id,
ContactRequestDecision::StartChat,
)
.await
{
Some(chat_id) => {
let chat = Chat::load_from_db(&context, chat_id).await?;
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id);
}
None => println!("Cannot crate chat."),
}
}
"decidenotnow" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::NotNow)
.await;
}
"decideblock" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::Block)
.await;
let chat_id = chat::create_by_msg_id(&context, msg_id).await?;
let chat = Chat::load_from_db(&context, chat_id).await?;
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id,);
}
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?;
println!("Group#{} created successfully.", chat_id);
}
"createprotected" => {
"createverified" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?;
println!("Group#{} created and protected successfully.", chat_id);
println!("VerifiedGroup#{} created successfully.", chat_id);
}
"addmember" => {
ensure!(sel_chat.is_some(), "No chat selected");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_0: u32 = arg1.parse()?;
let contact_id_0: libc::c_int = arg1.parse()?;
if chat::add_contact_to_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_0,
contact_id_0 as u32,
)
.await
{
@@ -731,11 +661,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"removemember" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_1: u32 = arg1.parse()?;
let contact_id_1: libc::c_int = arg1.parse()?;
chat::remove_contact_from_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_1,
contact_id_1 as u32,
)
.await?;
@@ -770,7 +700,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
contacts.len(),
location::is_sending_locations_to_chat(
&context,
Some(sel_chat.as_ref().unwrap().get_id())
sel_chat.as_ref().unwrap().get_id()
)
.await,
);
@@ -778,15 +708,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"getlocations" => {
ensure!(sel_chat.is_some(), "No chat selected.");
let contact_id: Option<u32> = arg1.parse().ok();
let contact_id = arg1.parse().unwrap_or_default();
let locations = location::get_range(
&context,
Some(sel_chat.as_ref().unwrap().get_id()),
sel_chat.as_ref().unwrap().get_id(),
contact_id,
0,
0,
)
.await?;
.await;
let default_marker = "-".to_string();
for location in &locations {
let marker = location.marker.as_ref().unwrap_or(&default_marker);
@@ -832,7 +762,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let latitude = arg1.parse()?;
let longitude = arg2.parse()?;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
if continue_streaming {
println!("Success, streaming should be continued.");
} else {
@@ -869,36 +799,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendhtml" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No html-file given.");
let path: &Path = arg1.as_ref();
let html = &*fs::read(&path)?;
let html = String::from_utf8_lossy(html);
let mut msg = Message::new(Viewtype::Text);
msg.set_html(Some(html.to_string()));
msg.set_text(Some(if arg2.is_empty() {
path.file_name().unwrap().to_string_lossy().to_string()
} else {
arg2.to_string()
}));
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
}
"listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing.");
let chat = if let Some(ref sel_chat) = sel_chat {
Some(sel_chat.get_id())
sel_chat.get_id()
} else {
None
ChatId::new(0)
};
let msglist = context.search_msgs(chat, arg1).await?;
let msglist = context.search_msgs(chat, arg1).await;
log_msglist(&context, &msglist).await?;
println!("{} messages.", msglist.len());
@@ -914,7 +824,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.unwrap()
.get_id()
.set_draft(&context, Some(&mut draft))
.await?;
.await;
println!("Draft saved.");
} else {
sel_chat
@@ -922,7 +832,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.unwrap()
.get_id()
.set_draft(&context, None)
.await?;
.await;
println!("Draft deleted.");
}
}
@@ -935,6 +845,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"updatedevicechats" => {
context.update_device_chats().await?;
}
"listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -966,39 +879,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"mute" | "unmute" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
let duration = match arg0 {
"mute" => {
if arg2.is_empty() {
MuteDuration::Forever
} else {
SystemTime::now()
.checked_add(Duration::from_secs(arg2.parse()?))
.map_or(MuteDuration::Forever, MuteDuration::Until)
}
}
"unmute" => MuteDuration::NotMuted,
_ => unreachable!("arg0={:?}", arg0),
};
chat::set_muted(&context, chat_id, duration).await?;
}
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id
.set_protection(
&context,
match arg0 {
"protect" => ProtectionStatus::Protected,
"unprotect" => ProtectionStatus::Unprotected,
_ => unreachable!("arg0={:?}", arg0),
_ => panic!("Unexpected command (This should never happen)"),
},
)
.await?;
@@ -1014,18 +895,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let res = message::get_msg_info(&context, id).await?;
println!("{}", res);
}
"html" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
let file = dirs::home_dir()
.unwrap_or_default()
.join(format!("msg-{}.html", id.to_u32()));
let html = id.get_html(&context).await?.unwrap_or_default();
fs::write(&file, html)?;
println!("HTML written to: {:#?}", file);
}
"listfresh" => {
let msglist = context.get_fresh_msgs().await?;
let msglist = context.get_fresh_msgs().await;
log_msglist(&context, &msglist).await?;
print!("{} fresh messages.", msglist.len());
@@ -1047,6 +918,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
msg_ids[0] = MsgId::new(arg1.parse()?);
message::markseen_msgs(&context, msg_ids).await;
}
"star" | "unstar" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = vec![MsgId::new(0); 1];
msg_ids[0] = MsgId::new(arg1.parse()?);
message::star_msgs(&context, msg_ids, arg0 == "star").await;
}
"delmsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut ids = [MsgId::new(0); 1];
@@ -1057,9 +934,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let contacts = Contact::get_all(
&context,
if arg0 == "listverified" {
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
0x1 | 0x2
} else {
DC_GCL_ADD_SELF
0x2
},
Some(arg1),
)
@@ -1080,14 +957,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"contactinfo" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id: u32 = arg1.parse()?;
let contact_id = arg1.parse()?;
let contact = Contact::get_by_id(&context, contact_id).await?;
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).await? {
match contact.get_profile_image(&context).await {
Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(),
}
@@ -1117,21 +994,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
Contact::delete(&context, arg1.parse()?).await?;
}
"block" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
Contact::block(&context, contact_id).await;
}
"unblock" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
Contact::unblock(&context, contact_id).await;
}
"listblocked" => {
let contacts = Contact::get_all_blocked(&context).await?;
log_contactlist(&context, &contacts).await;
println!("{} blocked contacts.", contacts.len());
}
"checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let res = check_qr(&context, arg1).await;
@@ -1152,7 +1014,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1).await {
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);
@@ -1172,11 +1034,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
// let event = arg1.parse()?;
// let event = EventType::from_u32(event).ok_or(format_err!("EventType::from_u32({})", event))?;
// let event = Event::from_u32(event).ok_or(format_err!("Event::from_u32({})", event))?;
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
// println!(
// "Sending event {:?}({}), received value {}.",
// event, event as usize, r,
// event, event as usize, r as libc::c_int,
// );
// }
"fileinfo" => {
@@ -1199,6 +1061,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
seconds, device_cnt, server_cnt
);
}
"emptyserver" => {
ensure!(!arg1.is_empty(), "Argument <flags> missing");
message::dc_empty_server(&context, arg1.parse()?).await;
}
"" => (),
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
}

View File

@@ -19,7 +19,7 @@ use deltachat::config;
use deltachat::context::*;
use deltachat::oauth2::*;
use deltachat::securejoin::*;
use deltachat::EventType;
use deltachat::Event;
use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType;
@@ -34,35 +34,35 @@ mod cmdline;
use self::cmdline::*;
/// Event Handler
fn receive_event(event: EventType) {
fn receive_event(event: Event) {
let yellow = Color::Yellow.normal();
match event {
EventType::Info(msg) => {
Event::Info(msg) => {
/* do not show the event as this would fill the screen */
info!("{}", msg);
}
EventType::SmtpConnected(msg) => {
Event::SmtpConnected(msg) => {
info!("[SMTP_CONNECTED] {}", msg);
}
EventType::ImapConnected(msg) => {
Event::ImapConnected(msg) => {
info!("[IMAP_CONNECTED] {}", msg);
}
EventType::SmtpMessageSent(msg) => {
Event::SmtpMessageSent(msg) => {
info!("[SMTP_MESSAGE_SENT] {}", msg);
}
EventType::Warning(msg) => {
Event::Warning(msg) => {
warn!("{}", msg);
}
EventType::Error(msg) => {
Event::Error(msg) => {
error!("{}", msg);
}
EventType::ErrorNetwork(msg) => {
Event::ErrorNetwork(msg) => {
error!("[NETWORK] msg={}", msg);
}
EventType::ErrorSelfNotInGroup(msg) => {
Event::ErrorSelfNotInGroup(msg) => {
error!("[SELF_NOT_IN_GROUP] {}", msg);
}
EventType::MsgsChanged { chat_id, msg_id } => {
Event::MsgsChanged { chat_id, msg_id } => {
info!(
"{}",
yellow.paint(format!(
@@ -71,44 +71,34 @@ fn receive_event(event: EventType) {
))
);
}
EventType::ContactsChanged(_) => {
Event::ContactsChanged(_) => {
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
}
EventType::LocationChanged(contact) => {
Event::LocationChanged(contact) => {
info!(
"{}",
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
);
}
EventType::ConfigureProgress { progress, comment } => {
if let Some(comment) = comment {
info!(
"{}",
yellow.paint(format!(
"Received CONFIGURE_PROGRESS({} ‰, {})",
progress, comment
))
);
} else {
info!(
"{}",
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
);
}
Event::ConfigureProgress(progress) => {
info!(
"{}",
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
);
}
EventType::ImexProgress(progress) => {
Event::ImexProgress(progress) => {
info!(
"{}",
yellow.paint(format!("Received IMEX_PROGRESS({} ‰)", progress))
);
}
EventType::ImexFileWritten(file) => {
Event::ImexFileWritten(file) => {
info!(
"{}",
yellow.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display()))
);
}
EventType::ChatModified(chat) => {
Event::ChatModified(chat) => {
info!(
"{}",
yellow.paint(format!("Received CHAT_MODIFIED({})", chat))
@@ -168,14 +158,12 @@ const DB_COMMANDS: [&str; 9] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 34] = [
const CHAT_COMMANDS: [&str; 26] = [
"listchats",
"listarchived",
"chat",
"createchat",
"decidestartchat",
"decideblock",
"decidenotnow",
"createchatbymsg",
"creategroup",
"createverified",
"addmember",
@@ -190,38 +178,31 @@ const CHAT_COMMANDS: [&str; 34] = [
"send",
"sendimage",
"sendfile",
"sendhtml",
"videochat",
"draft",
"listmedia",
"archive",
"unarchive",
"pin",
"unpin",
"mute",
"unmute",
"protect",
"unprotect",
"delchat",
];
const MESSAGE_COMMANDS: [&str; 6] = [
const MESSAGE_COMMANDS: [&str; 8] = [
"listmsgs",
"msginfo",
"listfresh",
"forward",
"markseen",
"star",
"unstar",
"delmsg",
];
const CONTACT_COMMANDS: [&str; 9] = [
const CONTACT_COMMANDS: [&str; 6] = [
"listcontacts",
"listverified",
"addcontact",
"contactinfo",
"delcontact",
"cleanupcontacts",
"block",
"unblock",
"listblocked",
];
const MISC_COMMANDS: [&str; 10] = [
"getqr",
@@ -290,12 +271,12 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf(), 0).await?;
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf()).await?;
let events = context.get_event_emitter();
async_std::task::spawn(async move {
while let Some(event) = events.recv().await {
receive_event(event.typ);
receive_event(event);
}
});
@@ -308,7 +289,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
.output_stream(OutputStreamType::Stdout)
.build();
let mut selected_chat = ChatId::default();
let (reader_s, reader_r) = async_std::channel::bounded(100);
let (reader_s, reader_r) = async_std::sync::channel(100);
let input_loop = async_std::task::spawn_blocking(move || {
let h = DcHelper {
completer: FilenameCompleter::new(),
@@ -331,7 +312,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
Ok(line) => {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str());
async_std::task::block_on(reader_s.send(line)).unwrap();
async_std::task::block_on(reader_s.send(line));
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
println!("Exiting...");
@@ -390,7 +371,7 @@ async fn handle_cmd(
ctx.configure().await?;
}
"oauth2" => {
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
if let Some(addr) = ctx.get_config(config::Config::Addr).await {
let oauth2_url =
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
if oauth2_url.is_none() {
@@ -408,8 +389,9 @@ async fn handle_cmd(
}
"getqr" | "getbadqr" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
if let Some(mut qr) = dc_get_securejoin_qr(&ctx, group).await {
if let Some(mut qr) =
dc_get_securejoin_qr(&ctx, ChatId::new(arg1.parse().unwrap_or_default())).await
{
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
@@ -427,7 +409,7 @@ async fn handle_cmd(
"joinqr" => {
ctx.start_io().await;
if !arg0.is_empty() {
dc_join_securejoin(&ctx, arg1).await?;
dc_join_securejoin(&ctx, arg1).await;
}
}
"exit" | "quit" => return Ok(ExitResult::Exit),

View File

@@ -6,20 +6,20 @@ use deltachat::config;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::message::Message;
use deltachat::EventType;
use deltachat::Event;
fn cb(event: EventType) {
fn cb(event: Event) {
match event {
EventType::ConfigureProgress { progress, .. } => {
Event::ConfigureProgress(progress) => {
log::info!("progress: {}", progress);
}
EventType::Info(msg) => {
Event::Info(msg) => {
log::info!("{}", msg);
}
EventType::Warning(msg) => {
Event::Warning(msg) => {
log::warn!("{}", msg);
}
EventType::Error(msg) | EventType::ErrorNetwork(msg) => {
Event::Error(msg) | Event::ErrorNetwork(msg) => {
log::error!("{}", msg);
}
event => {
@@ -36,7 +36,7 @@ async fn main() {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
log::info!("creating database {:?}", dbfile);
let ctx = Context::new("FakeOs".into(), dbfile.into(), 0)
let ctx = Context::new("FakeOs".into(), dbfile.into())
.await
.expect("Failed to create context");
let info = ctx.get_info().await;
@@ -45,7 +45,7 @@ async fn main() {
let events = ctx.get_event_emitter();
let events_spawn = async_std::task::spawn(async move {
while let Some(event) = events.recv().await {
cb(event.typ);
cb(event);
}
});

View File

@@ -1,29 +1,6 @@
1.51.0
------
- adapt python bindings and APIs to core51 release
(see CHANGELOG of https://github.com/deltachat/deltachat-core-rust/blob/1.51.0/CHANGELOG.md#1510
for more details on all core changes)
1.44.0
------
- fix Chat.get_mute_duration()
1.40.1
0.900.0 (DRAFT)
---------------
- emit "ac_member_removed" event (with 'actor' being the removed contact)
for when a user leaves a group.
- fix create_contact(addr) when addr is the self-contact.
1.40.0
---------------
- uses latest 1.40+ Delta Chat core
- refactored internals to use plugin-approach
- introduced PerAccount and Global hooks that plugins can implement
@@ -33,7 +10,6 @@
- introduced two documented examples for an echo and a group-membership
tracking plugin.
0.800.0
-------

View File

@@ -7,14 +7,76 @@ which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
a low-level Chat/Contact/Message API to user interfaces and bots.
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
without any "build-from-source" steps.
Otherwise you need to `compile the Delta Chat bindings yourself <#sourceinstall>`_.
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation.html>`_,
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
then create a fresh Python virtual environment and activate it in your shell::
virtualenv venv # or: python -m venv
@@ -41,78 +103,6 @@ To verify it worked::
`in contact with us <https://delta.chat/en/contribute>`_.
Running tests
=============
After successful binding installation you can install a few more
Python packages before running the tests::
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
pytest -v tests
This will run all "offline" tests and skip all functional
end-to-end tests that require accounts on real e-mail servers.
.. _livetests:
running "live" tests with temporary accounts
---------------------------------------------
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL``::
export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=1h_4w4r8h7y9nmcdsy
With this, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
These accounts exists for one 1hour and then are removed completely.
One hour is enough to invoke pytest and run all offline and online tests:
pytest
# or if you have installed pytest-xdist for parallel test execution
pytest -n6
Each test run creates new accounts.
.. _sourceinstall:
Installing bindings from source (Updated: July 2020)
=========================================================
Install Rust and Cargo first.
The easiest is probably to use `rustup <https://rustup.rs/>`_.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then clone the deltachat-core-rust repo::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
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::
python install_python_bindings.py
The core compilation and bindings building might take a while,
depending on the speed of 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.
Code examples
=============

View File

@@ -9,7 +9,8 @@
</ul>
<b>external links:</b>
<ul>
<li><a href="https://github.com/deltachat/deltachat-core-rust">github repository</a></li>
<li><a href="https://github.com/deltachat/deltachat-core">github repository</a></li>
<!-- <li><a href="https://lists.codespeak.net/postorius/lists/muacrypt.lists.codespeak.net">Mailing list</></li> <-->
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
</ul>

View File

@@ -32,16 +32,16 @@ class GroupTrackingPlugin:
print("chat member: {}".format(member.addr))
@account_hookimpl
def ac_member_added(self, chat, contact, actor, message):
def ac_member_added(self, chat, contact, message):
print("ac_member_added {} to chat {} from {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr))
contact.addr, chat.id, message.get_sender_contact().addr))
for member in chat.get_contacts():
print("chat member: {}".format(member.addr))
@account_hookimpl
def ac_member_removed(self, chat, contact, actor, message):
def ac_member_removed(self, chat, contact, message):
print("ac_member_removed {} from chat {} by {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr))
contact.addr, chat.id, message.get_sender_contact().addr))
def main(argv=None):

View File

@@ -69,11 +69,11 @@ def test_group_tracking_plugin(acfactory, lp):
lp.sec("now looking at what the bot received")
botproc.fnmatch_lines("""
*ac_member_added {}*from*{}*
""".format(contact3.addr, ac1.get_config("addr")))
*ac_member_added {}*
""".format(contact3.addr))
lp.sec("contact successfully added, now removing")
ch.remove_contact(contact3)
botproc.fnmatch_lines("""
*ac_member_removed {}*from*{}*
""".format(contact3.addr, ac1.get_config("addr")))
*ac_member_removed {}*
""".format(contact3.addr))

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python3
#!/usr/bin/env python
"""
setup a python binding development in-place install with cargo debug symbols.
@@ -17,11 +17,8 @@ if __name__ == "__main__":
os.environ["DCC_RS_DEV"] = dn
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
if target == 'release':
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
cmd.append("--release")
print("running:", " ".join(cmd))
subprocess.check_call(cmd)
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)

View File

@@ -77,7 +77,6 @@ def run_cmdline(argv=None, account_plugins=None):
ac.set_config("mvbox_move", "0")
ac.set_config("mvbox_watch", "0")
ac.set_config("sentbox_watch", "0")
ac.set_config("bot", "1")
configtracker = ac.configure()
configtracker.wait_finish()

View File

@@ -145,12 +145,9 @@ def extract_defines(flags):
| DC_STR
| DC_CONTACT_ID
| DC_GCL
| DC_GCM
| DC_SOCKET
| DC_CHAT
| DC_PROVIDER
| DC_KEY_GEN
| DC_IMEX
) # End of prefix matching
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
) # Close the capturing group, this contains

View File

@@ -179,6 +179,17 @@ class Account(object):
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_latest_backupfile(self, backupdir):
""" return the latest backup file in a given directory.
"""
@@ -205,7 +216,7 @@ class Account(object):
def create_contact(self, obj, name=None):
""" create a (new) Contact or return an existing one.
Calling this method will always result in the same
Calling this method will always resulut in the same
underlying contact id. If there already is a Contact
with that e-mail address, it is unblocked and its display
`name` is updated if specified.
@@ -214,19 +225,6 @@ class Account(object):
:param name: (optional) display name for this contact
:returns: :class:`deltachat.contact.Contact` instance.
"""
(name, addr) = self.get_contact_addr_and_name(obj, name)
name = as_dc_charpointer(name)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id)
def get_contact(self, obj):
if isinstance(obj, Contact):
return obj
(_, addr) = self.get_contact_addr_and_name(obj)
return self.get_contact_by_addr(addr)
def get_contact_addr_and_name(self, obj, name=None):
if isinstance(obj, Account):
if not obj.is_configured():
raise ValueError("can only add addresses from configured accounts")
@@ -242,7 +240,14 @@ class Account(object):
if name is None and displayname:
name = displayname
return (name, addr)
return self._create_contact(addr, name)
def _create_contact(self, addr, name):
addr = as_dc_charpointer(addr)
name = as_dc_charpointer(name)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL, contact_id
return Contact(self, contact_id)
def delete_contact(self, contact):
""" delete a Contact.
@@ -270,17 +275,6 @@ class Account(object):
"""
return Contact(self, contact_id)
def get_blocked_contacts(self):
""" return a list of all blocked contacts.
:returns: list of :class:`deltachat.contact.Contact` objects.
"""
dc_array = ffi.gc(
lib.dc_get_blocked_contacts(self._dc_context),
lib.dc_array_unref
)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_contacts(self, query=None, with_self=False, only_verified=False):
""" get a (filtered) list of contacts.
@@ -354,9 +348,6 @@ class Account(object):
def get_deaddrop_chat(self):
return Chat(self, const.DC_CHAT_ID_DEADDROP)
def get_device_chat(self):
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
def get_message_by_id(self, msg_id):
""" return Message instance.
:param msg_id: integer id of this message.
@@ -412,23 +403,23 @@ class Account(object):
Note that the account does not have to be started.
"""
return self._export(path, imex_cmd=const.DC_IMEX_EXPORT_SELF_KEYS)
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.
Note that the account has to be stopped; call stop_io() if necessary.
Note that the account does not have to be started.
"""
export_files = self._export(path, const.DC_IMEX_EXPORT_BACKUP)
export_files = self._export(path, 11)
if len(export_files) != 1:
raise RuntimeError("found more than one new file")
return export_files[0]
def _export(self, path, imex_cmd):
with self.temp_plugin(ImexTracker()) as imex_tracker:
self.imex(path, imex_cmd)
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
return imex_tracker.wait_finish()
def import_self_keys(self, path):
@@ -438,7 +429,7 @@ class Account(object):
Note that the account does not have to be started.
"""
self._import(path, imex_cmd=const.DC_IMEX_IMPORT_SELF_KEYS)
self._import(path, imex_cmd=2)
def import_all(self, path):
"""import delta chat state from the specified backup `path` (a file).
@@ -446,22 +437,21 @@ class Account(object):
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=const.DC_IMEX_IMPORT_BACKUP)
self._import(path, imex_cmd=12)
def _import(self, path, imex_cmd):
with self.temp_plugin(ImexTracker()) as imex_tracker:
self.imex(path, imex_cmd)
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
imex_tracker.wait_finish()
def imex(self, path, imex_cmd):
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
def initiate_key_transfer(self):
"""return setup code after a Autocrypt setup message
has been successfully sent to our own e-mail address ("self-sent message").
If sending out was unsuccessful, a RuntimeError is raised.
"""
self.check_is_configured()
if not self.is_started():
raise RuntimeError("IO not running, can not send out")
res = lib.dc_initiate_key_transfer(self._dc_context)
if res == ffi.NULL:
raise RuntimeError("could not send out autocrypt setup message")
@@ -568,9 +558,6 @@ class Account(object):
You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the
account is started.
If you are using this from a test, you may want to call
wait_all_initial_fetches() afterwards.
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
:raises ConfigureFailed: if the account could not be configured.
@@ -580,34 +567,12 @@ class Account(object):
raise ValueError("account not configured, cannot start io")
lib.dc_start_io(self._dc_context)
def maybe_network(self):
"""This function should be called when there is a hint
that the network is available again,
e.g. as a response to system event reporting network availability.
The library will try to send pending messages out immediately.
Moreover, to have a reliable state
when the app comes to foreground with network available,
it may be reasonable to call the function also at that moment.
It is okay to call the function unconditionally when there is
network available, however, calling the function
_without_ having network may interfere with the backoff algorithm
and will led to let the jobs fail faster, with fewer retries
and may avoid messages being sent out.
Finally, if the context was created by the dc_accounts_t account manager
(currently not implemented in the Python bindings),
use dc_accounts_maybe_network() instead of this function
"""
lib.dc_maybe_network(self._dc_context)
def configure(self, reconfigure=False):
def configure(self):
""" Start configuration process and return a Configtracker instance
on which you can block with wait_finish() to get a True/False success
value for the configuration process.
"""
assert self.is_configured() == reconfigure
assert not self.is_configured()
if not self.get_config("addr") or not self.get_config("mail_pw"):
raise MissingCredentials("addr or mail_pwd not set in config")
configtracker = ConfigureTracker(self)
@@ -615,6 +580,9 @@ class Account(object):
lib.dc_configure(self._dc_context)
return configtracker
def is_started(self):
return self._event_thread.is_alive() and bool(lib.dc_is_io_running(self._dc_context))
def wait_shutdown(self):
""" wait until shutdown of this account has completed. """
self._shutdown_event.wait()
@@ -624,8 +592,11 @@ class Account(object):
self.log("stop_ongoing")
self.stop_ongoing()
self.log("dc_stop_io (stop core IO scheduler)")
lib.dc_stop_io(self._dc_context)
if bool(lib.dc_is_io_running(self._dc_context)):
self.log("dc_stop_io (stop core IO scheduler)")
lib.dc_stop_io(self._dc_context)
else:
self.log("stop_scheduler called on non-running context")
def shutdown(self):
""" shutdown and destroy account (stop callback thread, close and remove
@@ -636,24 +607,12 @@ class Account(object):
self.stop_io()
self.log("remove dc_context references")
# if _dc_context is unref'ed the event thread should quickly
# receive the termination signal. However, some python code might
# still hold a reference and so we use a secondary signal
# to make sure the even thread terminates if it receives any new
# event, indepedently from waiting for the core to send NULL to
# get_next_event().
self._event_thread.mark_shutdown()
# the dc_context_unref triggers get_next_event to return ffi.NULL
# which in turns makes the event thread finish execution
self._dc_context = None
self.log("wait for event thread to finish")
try:
self._event_thread.wait(timeout=2)
except RuntimeError as e:
self.log("Waiting for event thread failed: {}".format(e))
if self._event_thread.is_alive():
self.log("WARN: event thread did not terminate yet, ignoring.")
self._event_thread.wait()
self._shutdown_event.set()

View File

@@ -57,7 +57,10 @@ class Chat(object):
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
"""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
return lib.dc_chat_get_type(self._dc_chat) in (
const.DC_CHAT_TYPE_GROUP,
const.DC_CHAT_TYPE_VERIFIED_GROUP
)
def is_deaddrop(self):
""" return true if this chat is a deaddrop chat.
@@ -82,20 +85,12 @@ class Chat(object):
"""
return not lib.dc_chat_is_unpromoted(self._dc_chat)
def can_send(self):
"""Check if messages can be sent to a give chat.
This is not true eg. for the deaddrop or for the device-talk
def is_verified(self):
""" return True if this chat is a verified group.
:returns: True if the chat is writable, False otherwise
:returns: True if chat is verified, False otherwise.
"""
return lib.dc_chat_can_send(self._dc_chat)
def is_protected(self):
""" return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise.
"""
return lib.dc_chat_is_protected(self._dc_chat)
return lib.dc_chat_is_verified(self._dc_chat)
def get_name(self):
""" return name of this chat.
@@ -142,23 +137,7 @@ class Chat(object):
:param duration:
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
"""
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
def get_ephemeral_timer(self):
""" get ephemeral timer.
:returns: ephemeral timer value in seconds
"""
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
def set_ephemeral_timer(self, timer):
""" set ephemeral timer.
:param: timer value in seconds
:returns: None
"""
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
def get_type(self):
""" (deprecated) return type of this chat.
@@ -167,13 +146,6 @@ class Chat(object):
"""
return lib.dc_chat_get_type(self._dc_chat)
def get_encryption_info(self):
"""Return encryption info for this chat.
:returns: a string with encryption preferences of all chat members"""
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
return from_dc_charpointer(res)
def get_join_qr(self):
""" get/create Join-Group QR Code as ascii-string.
@@ -254,19 +226,17 @@ class Chat(object):
return Message.from_db(self.account, sent_id)
def prepare_message(self, msg):
""" prepare a message for sending.
""" create a new prepared message.
:param msg: the message to be prepared.
:returns: a :class:`deltachat.message.Message` instance.
This is the same object that was passed in, which
has been modified with the new state of the core.
:returns: :class:`deltachat.message.Message` instance.
"""
msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
if msg_id == 0:
raise ValueError("message could not be prepared")
# modify message in place to avoid bad state for the caller
msg._dc_msg = Message.from_db(self.account, msg_id)._dc_msg
return msg
# invalidate passed in message which is not safe to use anymore
msg._dc_msg = msg.id = None
return Message.from_db(self.account, msg_id)
def prepare_message_file(self, path, mime_type=None, view_type="file"):
""" prepare a message for sending and return the resulting Message instance.
@@ -380,7 +350,7 @@ class Chat(object):
:raises ValueError: if contact could not be removed
:returns: None
"""
contact = self.account.get_contact(obj)
contact = self.account.create_contact(obj)
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError("could not remove contact {!r} from chat".format(contact))
@@ -504,23 +474,18 @@ class Chat(object):
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)
),
marker=from_dc_charpointer(lib.dc_array_get_marker(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, marker):
def __init__(self, latitude, longitude, accuracy, timestamp):
assert isinstance(timestamp, datetime)
self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
self.timestamp = timestamp
self.marker = marker
def __eq__(self, other):
return self.__dict__ == other.__dict__

View File

@@ -51,18 +51,6 @@ class Contact(object):
""" Return True if the contact is blocked. """
return lib.dc_contact_is_blocked(self._dc_contact)
def set_blocked(self, block=True):
""" [Deprecated, use block/unblock methods] Block or unblock a contact. """
return lib.dc_block_contact(self.account._dc_context, self.id, block)
def block(self):
""" Block this contact. Message will not be seen/retrieved from this contact. """
return lib.dc_block_contact(self.account._dc_context, self.id, True)
def unblock(self):
""" Unblock this contact. Messages from this contact will be retrieved (again)."""
return lib.dc_block_contact(self.account._dc_context, self.id, False)
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)
@@ -77,14 +65,6 @@ class Contact(object):
return None
return from_dc_charpointer(dc_res)
@property
def status(self):
"""Get contact status.
:returns: contact status, empty string if it doesn't exist.
"""
return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact))
def create_chat(self):
""" create or get an existing 1:1 chat object for the specified contact or contact id.

View File

@@ -17,8 +17,7 @@ def iter_array(dc_array_t, constructor):
def from_dc_charpointer(obj):
if obj != ffi.NULL:
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
class DCLot:

View File

@@ -9,9 +9,7 @@ import ssl
import pathlib
from imapclient import IMAPClient
from imapclient.exceptions import IMAPClientError
import imaplib
import deltachat
from deltachat import const
SEEN = b'\\Seen'
@@ -26,29 +24,12 @@ def dc_account_extra_configure(account):
""" Reset the account (we reuse accounts across tests)
and make 'account.direct_imap' available for direct IMAP ops.
"""
try:
if not hasattr(account, "direct_imap"):
imap = DirectImap(account)
for folder in imap.list_folders():
if folder.lower() == "inbox" or folder.lower() == "deltachat":
assert imap.select_folder(folder)
imap.delete(ALL, expunge=True)
else:
imap.conn.delete_folder(folder)
# We just deleted the folder, so we have to make DC forget about it, too
if account.get_config("configured_sentbox_folder") == folder:
account.set_config("configured_sentbox_folder", None)
if account.get_config("configured_spam_folder") == folder:
account.set_config("configured_spam_folder", None)
setattr(account, "direct_imap", imap)
except Exception as e:
# Uncaught exceptions here would lead to a timeout without any note written to the log
account.log("=============================== CAN'T RESET ACCOUNT: ===============================")
account.log("===================", e, "===================")
imap = DirectImap(account)
if imap.select_config_folder("mvbox"):
imap.delete(ALL, expunge=True)
assert imap.select_config_folder("inbox")
imap.delete(ALL, expunge=True)
setattr(account, "direct_imap", imap)
@deltachat.global_hookimpl
@@ -68,31 +49,18 @@ class DirectImap:
self.connect()
def connect(self):
host = self.account.get_config("configured_mail_server")
port = int(self.account.get_config("configured_mail_port"))
security = int(self.account.get_config("configured_mail_security"))
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
host = self.account.get_config("configured_mail_server")
user = self.account.get_config("addr")
pw = self.account.get_config("mail_pw")
if security == const.DC_SOCKET_PLAIN:
ssl_context = None
else:
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
if security == const.DC_SOCKET_STARTTLS:
self.conn = IMAPClient(host, port, ssl=False)
self.conn.starttls(ssl_context)
elif security == const.DC_SOCKET_PLAIN:
self.conn = IMAPClient(host, port, ssl=False)
elif security == const.DC_SOCKET_SSL:
self.conn = IMAPClient(host, port, ssl_context=ssl_context)
self.conn = IMAPClient(host, ssl_context=ssl_context)
self.conn.login(user, pw)
self.select_folder("INBOX")
@@ -107,12 +75,6 @@ class DirectImap:
except (OSError, IMAPClientError):
print("Could not logout direct_imap conn")
def create_folder(self, foldername):
try:
self.conn.create_folder(foldername)
except imaplib.IMAP4.error as e:
print("Can't create", foldername, "probably it already exists:", str(e))
def select_folder(self, foldername):
assert not self._idling
return self.conn.select_folder(foldername)
@@ -146,15 +108,6 @@ class DirectImap:
def get_all_messages(self):
assert not self._idling
# Flush unsolicited responses. IMAPClient has problems
# dealing with them: https://github.com/mjs/imapclient/issues/334
# When this NOOP was introduced, next FETCH returned empty
# result instead of a single message, even though IMAP server
# can only return more untagged responses than required, not
# less.
self.conn.noop()
return self.conn.fetch(ALL, [FLAGS])
def get_unread_messages(self):
@@ -206,13 +159,9 @@ class DirectImap:
log("---------", imapfolder, len(messages), "messages ---------")
# get message content without auto-marking it as seen
# fetching 'RFC822' would mark it as seen.
requested = [b'BODY.PEEK[]', FLAGS]
requested = [b'BODY.PEEK[HEADER]', FLAGS]
for uid, data in self.conn.fetch(messages, requested).items():
body_bytes = data[b'BODY[]']
if not body_bytes:
log("Message", uid, "has empty body")
continue
body_bytes = data[b'BODY[HEADER]']
flags = data[FLAGS]
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
path.mkdir(parents=True, exist_ok=True)
@@ -243,7 +192,6 @@ class DirectImap:
raise TimeoutError
if terminate:
self.idle_done()
self.account.log("imap-direct: idle_check returned {!r}".format(res))
return res
def idle_wait_for_seen(self):
@@ -263,9 +211,3 @@ class DirectImap:
res = self.conn.idle_done()
self._idling = False
return res
def append(self, folder, msg):
if msg.startswith("\n"):
msg = msg[1:]
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
self.conn.append(folder, msg)

View File

@@ -1,7 +1,6 @@
import threading
import time
import re
import os
from queue import Queue, Empty
import deltachat
@@ -49,15 +48,6 @@ class FFIEventLogger:
if self.logid:
locname += "-" + self.logid
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
if os.name == "posix":
WARN = '\033[93m'
ERROR = '\033[91m'
ENDC = '\033[0m'
if message.startswith("DC_EVENT_WARNING"):
s = WARN + s + ENDC
if message.startswith("DC_EVENT_ERROR"):
s = ERROR + s + ENDC
with self._loglock:
print(s, flush=True)
@@ -96,21 +86,13 @@ class FFIEventTracker:
if rex.match(ev.name):
return ev
def get_info_contains(self, regex):
rex = re.compile(regex)
def get_info_matching(self, regex):
rex = re.compile("(?:{}).*".format(regex))
while 1:
ev = self.get_matching("DC_EVENT_INFO")
if rex.search(ev.data2):
if rex.match(ev.data2):
return ev
def get_info_regex_groups(self, regex, check_error=True):
rex = re.compile(regex)
while 1:
ev = self.get_matching("DC_EVENT_INFO", check_error=check_error)
m = rex.match(ev.data2)
if m is not None:
return m.groups()
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))
@@ -129,15 +111,6 @@ class FFIEventTracker:
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
break
def wait_all_initial_fetches(self):
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
so that new messages are not mistaken for old ones:
- ac1 and ac2 are created
- ac1 sends a message to ac2
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
- therefore no DC_EVENT_INCOMING_MSG is sent"""
self.get_info_contains("Done fetching existing messages")
def wait_next_incoming_message(self):
""" wait for and return next incoming message. """
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
@@ -166,7 +139,6 @@ class EventThread(threading.Thread):
self.account = account
super(EventThread, self).__init__(name="events")
self.setDaemon(True)
self._marked_for_shutdown = False
self.start()
@contextmanager
@@ -175,15 +147,12 @@ class EventThread(threading.Thread):
yield
self.account.log(message + " FINISHED")
def mark_shutdown(self):
self._marked_for_shutdown = True
def wait(self, timeout=None):
def wait(self):
if self == threading.current_thread():
# we are in the callback thread and thus cannot
# wait for the thread-loop to finish.
return
self.join(timeout=timeout)
self.join()
def run(self):
""" get and run events until shutdown. """
@@ -195,12 +164,10 @@ class EventThread(threading.Thread):
lib.dc_get_event_emitter(self.account._dc_context),
lib.dc_event_emitter_unref,
)
while not self._marked_for_shutdown:
while 1:
event = lib.dc_get_next_event(event_emitter)
if event == ffi.NULL:
break
if self._marked_for_shutdown:
break
evt = lib.dc_event_get_id(event)
data1 = lib.dc_event_get_data1_int(event)
# the following code relates to the deltachat/_build.py's helper

View File

@@ -16,7 +16,7 @@ class PerAccount:
""" per-Account-instance hook specifications.
All hooks are executed in a dedicated Event thread.
Hooks are generally not allowed to block/last long as this
Hooks are not allowed to block/last long as this
blocks overall event processing on the python side.
"""
@classmethod
@@ -31,6 +31,10 @@ class PerAccount:
ffi_event has "name", "data1", "data2" values as specified
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
DANGER: this hook is executed from the callback invoked by core.
Hook implementations need to be short running and can typically
not call back into core because this would easily cause recursion issues.
"""
@account_hookspec
@@ -51,37 +55,19 @@ class PerAccount:
@account_hookspec
def ac_message_delivered(self, message):
""" Called when an outgoing message has been delivered to SMTP.
:param message: Message that was just delivered.
"""
""" Called when an outgoing message has been delivered to SMTP. """
@account_hookspec
def ac_chat_modified(self, chat):
""" Chat was created or modified regarding membership, avatar, title.
:param chat: Chat which was modified.
"""
""" Chat was created or modified regarding membership, avatar, title. """
@account_hookspec
def ac_member_added(self, chat, contact, actor, message):
""" Called for each contact added to an accepted chat.
:param chat: Chat where contact was added.
:param contact: Contact that was added.
:param actor: Who added the contact (None if it was our self-addr)
:param message: The original system message that reports the addition.
"""
def ac_member_added(self, chat, contact, message):
""" Called for each contact added to an accepted chat. """
@account_hookspec
def ac_member_removed(self, chat, contact, actor, message):
""" Called for each contact removed from a chat.
:param chat: Chat where contact was removed.
:param contact: Contact that was removed.
:param actor: Who removed the contact (None if it was our self-addr)
:param message: The original system message that reports the removal.
"""
def ac_member_removed(self, chat, contact, message):
""" Called for each contact removed from a chat. """
class Global:

View File

@@ -1,7 +1,6 @@
""" The Message object. """
import os
import re
from . import props
from .cutil import from_dc_charpointer, as_dc_charpointer
from .capi import lib, ffi
@@ -21,8 +20,8 @@ class Message(object):
assert isinstance(dc_msg, ffi.CData)
assert dc_msg != ffi.NULL
self._dc_msg = dc_msg
msg_id = self.id
assert msg_id is not None and msg_id >= 0, repr(msg_id)
self.id = lib.dc_msg_get_id(dc_msg)
assert self.id is not None and self.id >= 0, repr(self.id)
def __eq__(self, other):
return self.account == other.account and self.id == other.id
@@ -46,13 +45,9 @@ class Message(object):
def new_empty(cls, account, view_type):
""" create a non-persistent message.
:param: view_type is the message type code or one of the strings:
"text", "audio", "video", "file", "sticker"
:param: view_type is "text", "audio", "video", "file"
"""
if isinstance(view_type, int):
view_type_code = view_type
else:
view_type_code = get_viewtype_code_from_name(view_type)
view_type_code = get_viewtype_code_from_name(view_type)
return Message(account, ffi.gc(
lib.dc_msg_new(account._dc_context, view_type_code),
lib.dc_msg_unref
@@ -72,11 +67,6 @@ class Message(object):
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
return Chat(self.account, chat_id)
@props.with_doc
def id(self):
"""id of this message. """
return lib.dc_msg_get_id(self._dc_msg)
@props.with_doc
def text(self):
"""unicode text of this messages (might be empty if not a text message). """
@@ -164,47 +154,6 @@ class Message(object):
if ts:
return datetime.utcfromtimestamp(ts)
@props.with_doc
def ephemeral_timer(self):
"""Ephemeral timer in seconds
:returns: timer in seconds or None if there is no timer
"""
timer = lib.dc_msg_get_ephemeral_timer(self._dc_msg)
if timer:
return timer
@props.with_doc
def ephemeral_timestamp(self):
"""UTC time when the message will be deleted.
:returns: naive datetime.datetime() object or None if the timer is not started.
"""
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
if ts:
return datetime.utcfromtimestamp(ts)
@property
def quoted_text(self):
"""Text inside the quote
:returns: Quoted text"""
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
@property
def quote(self):
"""Quote getter
:returns: Quoted message, if found in the database"""
msg = lib.dc_msg_get_quoted_msg(self._dc_msg)
if msg:
return Message(self.account, ffi.gc(msg, lib.dc_msg_unref))
@quote.setter
def quote(self, quoted_message):
"""Quote setter"""
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
def get_mime_headers(self):
""" return mime-header object for an incoming message.
@@ -221,11 +170,6 @@ class Message(object):
return email.message_from_bytes(s)
return email.message_from_string(s)
@property
def error(self):
"""Error message"""
return from_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
@property
def chat(self):
"""chat this message was posted in.
@@ -348,10 +292,6 @@ class Message(object):
""" return True if it's a gif message. """
return self._view_type == const.DC_MSG_GIF
def is_sticker(self):
""" return True if it's a sticker message. """
return self._view_type == const.DC_MSG_STICKER
def is_audio(self):
""" return True if it's an audio message. """
return self._view_type == const.DC_MSG_AUDIO
@@ -372,22 +312,21 @@ class Message(object):
# some code for handling DC_MSG_* view types
_view_type_mapping = {
'text': const.DC_MSG_TEXT,
'image': const.DC_MSG_IMAGE,
'gif': const.DC_MSG_GIF,
'audio': const.DC_MSG_AUDIO,
'video': const.DC_MSG_VIDEO,
'file': const.DC_MSG_FILE,
'sticker': const.DC_MSG_STICKER,
const.DC_MSG_TEXT: 'text',
const.DC_MSG_IMAGE: 'image',
const.DC_MSG_GIF: 'gif',
const.DC_MSG_AUDIO: 'audio',
const.DC_MSG_VIDEO: 'video',
const.DC_MSG_FILE: 'file'
}
def get_viewtype_code_from_name(view_type_name):
code = _view_type_mapping.get(view_type_name)
if code is not None:
return code
for code, value in _view_type_mapping.items():
if value == view_type_name:
return code
raise ValueError("message typecode not found for {!r}, "
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())))
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
#
@@ -397,43 +336,20 @@ def get_viewtype_code_from_name(view_type_name):
def map_system_message(msg):
if msg.is_system_message():
res = parse_system_add_remove(msg.text)
if not res:
return
action, affected, actor = res
affected = msg.account.get_contact_by_addr(affected)
if actor == "me":
actor = None
else:
actor = msg.account.get_contact_by_addr(actor)
d = dict(chat=msg.chat, contact=affected, actor=actor, message=msg)
return "ac_member_" + res[0], d
def extract_addr(text):
m = re.match(r'.*\((.+@.+)\)', text)
if m:
text = m.group(1)
text = text.rstrip(".")
return text.strip()
if res:
contact = msg.account.get_contact_by_addr(res[1])
if contact:
d = dict(chat=msg.chat, contact=contact, message=msg)
return "ac_member_" + res[0], d
def parse_system_add_remove(text):
""" return add/remove info from parsing the given system message text.
returns a (action, affected, actor) triple """
# Member Me (x@y) removed by a@b.
# Member x@y added by a@b
# Member With space (tmp1@x.org) removed by tmp2@x.org.
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
# Group left by some one (tmp1@x.org).
# Group left by tmp1@x.org.
# Member x@y removed by a@b
text = text.lower()
m = re.match(r'member (.+) (removed|added) by (.+)', text)
if m:
affected, action, actor = m.groups()
return action, extract_addr(affected), extract_addr(actor)
if text.startswith("group left by "):
addr = extract_addr(text[13:])
if addr:
return "removed", addr, addr
parts = text.split()
if parts[0] == "member":
if parts[2] in ("removed", "added"):
return parts[2], parts[1]
if parts[3] in ("removed", "added"):
return parts[3], parts[2].strip("()")

View File

@@ -32,10 +32,6 @@ def pytest_addoption(parser):
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
parser.addoption(
"--strict-tls", action="store_true",
help="Never accept invalid TLS certificates for test accounts",
)
def pytest_configure(config):
@@ -156,7 +152,7 @@ class SessionLiveConfigFromURL:
assert index == len(self.configlist), index
res = requests.post(self.url)
if res.status_code != 200:
pytest.skip("creating newtmpuser failed with code {}: '{}'".format(res.status_code, res.text))
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
@@ -235,13 +231,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac._evtracker.set_timeout(30)
ac.addr = ac.get_self_contact().addr
ac.set_config("displayname", logid)
if not quiet:
logger = FFIEventLogger(ac)
logger.init_time = self.init_time
ac.add_account_plugin(logger)
ac.add_account_plugin(FFIEventLogger(ac))
self._accounts.append(ac)
return ac
@@ -251,7 +244,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def get_unconfigured_account(self):
self.offline_count += 1
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
return self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac._evtracker.init_time = self.init_time
ac._evtracker.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.
@@ -286,15 +282,16 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
if pytestconfig.getoption("--strict-tls"):
# 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)
# 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), quiet=quiet)
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
@@ -312,36 +309,31 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
self.wait_configure_and_start_io([ac1])
self.wait_configure_and_start_io()
return ac1
def get_two_online_accounts(self, move=False, quiet=False):
ac1 = self.get_online_configuring_account(move=move, quiet=quiet)
ac2 = self.get_online_configuring_account(quiet=quiet)
self.wait_configure_and_start_io([ac1, ac2])
self.wait_configure_and_start_io()
return ac1, ac2
def get_many_online_accounts(self, num, move=True):
accounts = [self.get_online_configuring_account(move=move, quiet=True)
for i in range(num)]
self.wait_configure_and_start_io(accounts)
self.wait_configure_and_start_io()
for acc in accounts:
acc.add_account_plugin(FFIEventLogger(acc))
return accounts
def clone_online_account(self, account, pre_generated_key=True):
""" Clones addr, mail_pw, mvbox_watch, mvbox_move, sentbox_watch and the
direct_imap object of an online account. This simulates the user setting
up a new device without importing a backup.
`pre_generated_key` only means that a key from python/tests/data/key is
used in order to speed things up.
"""
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._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(30)
ac.update_config(dict(
addr=account.get_config("addr"),
mail_pw=account.get_config("mail_pw"),
@@ -349,35 +341,19 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
mvbox_move=account.get_config("mvbox_move"),
sentbox_watch=account.get_config("sentbox_watch"),
))
if hasattr(account, "direct_imap"):
# Attach the existing direct_imap. If we did not do this, a new one would be created and
# delete existing messages (see dc_account_extra_configure(configure))
ac.direct_imap = account.direct_imap
ac._configtracker = ac.configure()
return ac
def wait_configure_and_start_io(self, accounts=None):
if accounts is None:
accounts = self._accounts[:]
started_accounts = []
for acc in accounts:
if acc not in started_accounts:
self.wait_configure(acc)
acc.set_config("bcc_self", "0")
if acc.is_configured():
acc.start_io()
started_accounts.append(acc)
print("{}: {} account was started".format(
acc.get_config("displayname"), acc.get_config("addr")))
for acc in started_accounts:
acc._evtracker.wait_all_initial_fetches()
def wait_configure(self, acc):
if hasattr(acc, "_configtracker"):
acc._configtracker.wait_finish()
acc._evtracker.consume_events()
acc.get_device_chat().mark_noticed()
del acc._configtracker
def wait_configure_and_start_io(self):
for acc in self._accounts:
if hasattr(acc, "_configtracker"):
acc._configtracker.wait_finish()
del acc._configtracker
acc.set_config("bcc_self", "0")
if acc.is_configured() and not acc.is_started():
acc.start_io()
print("{}: {} account was successfully setup".format(
acc.get_config("displayname"), acc.get_config("addr")))
def run_bot_process(self, module, ffi=True):
fn = module.__file__
@@ -516,9 +492,6 @@ def lp():
def step(self, msg):
print("-" * 5, "step " + msg, "-" * 5)
def indent(self, msg):
print(" " + msg)
return Printer()

View File

@@ -20,16 +20,6 @@ class ImexTracker:
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
self._imex_events.put(ffi_event.data2)
def wait_progress(self, target_progress, progress_upper_limit=1000, progress_timeout=60):
while True:
ev = self._imex_events.get(timeout=progress_timeout)
if isinstance(ev, int) and ev >= target_progress:
assert ev <= progress_upper_limit, \
str(ev) + " exceeded upper progress limit " + str(progress_upper_limit)
return ev
if ev == 0:
return None
def wait_finish(self, progress_timeout=60):
""" Return list of written files, raise ValueError if ExportFailed. """
files_written = []

File diff suppressed because it is too large Load Diff

View File

@@ -7,12 +7,13 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
DCC_RS_DEV
DCC_RS_TARGET
DCC_PY_LIVECONFIG
DCC_NEW_TMP_EMAIL
CARGO_TARGET_DIR
RUSTC_WRAPPER
@@ -46,9 +47,7 @@ commands =
[testenv:doc]
changedir=doc
deps =
# With Python 3.7 and Sphinx 3.5.0, it throws an exception.
# Pin the version to the working one.
sphinx==3.4.3
sphinx
breathe
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html

View File

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

View File

@@ -1 +1 @@
1.50.0
1.43.1

View File

@@ -1,31 +1,22 @@
#!/usr/bin/env python3
#!/usr/bin/env python
import os
import sys
import re
import pathlib
import subprocess
from argparse import ArgumentParser
rex = re.compile(r'version = "(\S+)"')
def regex_matches(relpath, regex=rex):
def read_toml_version(relpath):
p = pathlib.Path(relpath)
assert p.exists()
for line in open(str(p)):
m = regex.match(line)
m = rex.match(line)
if m is not None:
return m
def read_toml_version(relpath):
res = regex_matches(relpath, rex)
if res is not None:
return res.group(1)
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()
@@ -34,28 +25,18 @@ def replace_toml_version(relpath, newversion):
for line in open(str(p)):
m = rex.match(line)
if m is not None:
print("{}: set version={}".format(relpath, newversion))
f.write('version = "{}"\n'.format(newversion))
else:
f.write(line)
os.rename(tmp_path, str(p))
if __name__ == "__main__":
def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml"]
try:
opts = parser.parse_args()
except SystemExit:
print()
for x in toml_list:
if len(sys.argv) < 2:
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
print("{}: {}".format(x, read_toml_version(x)))
print()
raise SystemExit("need argument: new version, example: 1.25.0")
newversion = opts.newversion
newversion = sys.argv[1]
if newversion.count(".") < 2:
raise SystemExit("need at least two dots in version")
@@ -63,22 +44,18 @@ def main():
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
assert core_toml == ffi_toml, (core_toml, ffi_toml)
if "alpha" not in newversion:
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))
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)
print("running cargo check")
subprocess.call(["cargo", "check"])
print("adding changes to git index")
subprocess.call(["git", "add", "-u"])
# subprocess.call(["cargo", "update", "-p", "deltachat"])
@@ -86,8 +63,3 @@ def main():
print("")
print(" git tag {}".format(newversion))
print("")
if __name__ == "__main__":
main()

View File

@@ -32,7 +32,7 @@ Messages SHOULD be encrypted by the
`prefer-encrypt=mutual` MAY be set by default.
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
by the [Protected Headers](https://tools.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
by the [Protected Headers](https://www.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
# Outgoing messages

View File

@@ -1,540 +0,0 @@
use std::collections::BTreeMap;
use async_std::fs;
use async_std::path::PathBuf;
use async_std::prelude::*;
use async_std::sync::{Arc, RwLock};
use uuid::Uuid;
use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use crate::context::Context;
use crate::events::Event;
/// Account manager, that can handle multiple accounts in a single place.
#[derive(Debug, Clone)]
pub struct Accounts {
dir: PathBuf,
config: Config,
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
}
impl Accounts {
/// Loads or creates an accounts folder at the given `dir`.
pub async fn new(os_name: String, dir: PathBuf) -> Result<Self> {
if !dir.exists().await {
Accounts::create(os_name, &dir).await?;
}
Accounts::open(dir).await
}
/// Creates a new default structure, including a default account.
pub async fn create(os_name: String, dir: &PathBuf) -> Result<()> {
fs::create_dir_all(dir)
.await
.context("failed to create folder")?;
// create default account
let config = Config::new(os_name.clone(), dir).await?;
let account_config = config.new_account(dir).await?;
Context::new(os_name, account_config.dbfile().into(), account_config.id)
.await
.context("failed to create default account")?;
Ok(())
}
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
/// no account exists and no config exists.
pub async fn open(dir: PathBuf) -> Result<Self> {
ensure!(dir.exists().await, "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists().await, "accounts.toml does not exist");
let config = Config::from_file(config_file).await?;
let accounts = config.load_accounts().await?;
Ok(Self {
dir,
config,
accounts: Arc::new(RwLock::new(accounts)),
})
}
/// Get an account by its `id`:
pub async fn get_account(&self, id: u32) -> Option<Context> {
self.accounts.read().await.get(&id).cloned()
}
/// Get the currently selected account.
pub async fn get_selected_account(&self) -> Context {
let id = self.config.get_selected_account().await;
self.accounts
.read()
.await
.get(&id)
.cloned()
.expect("inconsistent state")
}
/// Select the given account.
pub async fn select_account(&self, id: u32) -> Result<()> {
self.config.select_account(id).await?;
Ok(())
}
/// Add a new account.
pub async fn add_account(&self) -> Result<u32> {
let os_name = self.config.os_name().await;
let account_config = self.config.new_account(&self.dir).await?;
let ctx = Context::new(os_name, account_config.dbfile().into(), account_config.id).await?;
self.accounts.write().await.insert(account_config.id, ctx);
Ok(account_config.id)
}
/// Remove an account.
pub async fn remove_account(&self, id: u32) -> Result<()> {
let ctx = self.accounts.write().await.remove(&id);
ensure!(ctx.is_some(), "no account with this id: {}", id);
let ctx = ctx.unwrap();
ctx.stop_io().await;
drop(ctx);
if let Some(cfg) = self.config.get_account(id).await {
fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
.await
.context("failed to remove account data")?;
}
self.config.remove_account(id).await?;
Ok(())
}
/// Migrate an existing account into this structure.
pub async fn migrate_account(&self, dbfile: PathBuf) -> Result<u32> {
let blobdir = Context::derive_blobdir(&dbfile);
ensure!(
dbfile.exists().await,
"no database found: {}",
dbfile.display()
);
ensure!(
blobdir.exists().await,
"no blobdir found: {}",
blobdir.display()
);
let old_id = self.config.get_selected_account().await;
// create new account
let account_config = self
.config
.new_account(&self.dir)
.await
.context("failed to create new account")?;
let new_dbfile = account_config.dbfile().into();
let new_blobdir = Context::derive_blobdir(&new_dbfile);
let res = {
fs::create_dir_all(&account_config.dir)
.await
.context("failed to create dir")?;
fs::rename(&dbfile, &new_dbfile)
.await
.context("failed to rename dbfile")?;
fs::rename(&blobdir, &new_blobdir)
.await
.context("failed to rename blobdir")?;
Ok(())
};
match res {
Ok(_) => {
let ctx = Context::with_blobdir(
self.config.os_name().await,
new_dbfile,
new_blobdir,
account_config.id,
)
.await?;
self.accounts.write().await.insert(account_config.id, ctx);
Ok(account_config.id)
}
Err(err) => {
// remove temp account
fs::remove_dir_all(async_std::path::PathBuf::from(&account_config.dir))
.await
.context("failed to remove account data")?;
self.config.remove_account(account_config.id).await?;
// set selection back
self.select_account(old_id).await?;
Err(err)
}
}
}
/// Get a list of all account ids.
pub async fn get_all(&self) -> Vec<u32> {
self.accounts.read().await.keys().copied().collect()
}
/// Import a backup using a new account and selects it.
pub async fn import_account(&self, file: PathBuf) -> Result<u32> {
let old_id = self.config.get_selected_account().await;
let id = self.add_account().await?;
let ctx = self.get_account(id).await.expect("just added");
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, &file).await {
Ok(_) => Ok(id),
Err(err) => {
// remove temp account
self.remove_account(id).await?;
// set selection back
self.select_account(old_id).await?;
Err(err)
}
}
}
pub async fn start_io(&self) {
let accounts = &*self.accounts.read().await;
for account in accounts.values() {
account.start_io().await;
}
}
pub async fn stop_io(&self) {
let accounts = &*self.accounts.read().await;
for account in accounts.values() {
account.stop_io().await;
}
}
pub async fn maybe_network(&self) {
let accounts = &*self.accounts.read().await;
for account in accounts.values() {
account.maybe_network().await;
}
}
/// Unified event emitter.
pub async fn get_event_emitter(&self) -> EventEmitter {
let emitters: Vec<_> = self
.accounts
.read()
.await
.iter()
.map(|(_id, a)| a.get_event_emitter())
.collect();
EventEmitter(futures::stream::select_all(emitters))
}
}
#[derive(Debug)]
pub struct EventEmitter(futures::stream::SelectAll<crate::events::EventEmitter>);
impl EventEmitter {
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
pub fn recv_sync(&mut self) -> Option<Event> {
async_std::task::block_on(self.recv())
}
/// Async recv of an event. Return `None` if all `Sender`s have been droped.
pub async fn recv(&mut self) -> Option<Event> {
self.0.next().await
}
}
impl async_std::stream::Stream for EventEmitter {
type Item = Event;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.0).poll_next(cx)
}
}
pub const CONFIG_NAME: &str = "accounts.toml";
pub const DB_NAME: &str = "dc.db";
#[derive(Debug, Clone)]
pub struct Config {
file: PathBuf,
inner: Arc<RwLock<InnerConfig>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct InnerConfig {
pub os_name: String,
/// The currently selected account.
pub selected_account: u32,
pub next_id: u32,
pub accounts: Vec<AccountConfig>,
}
impl Config {
pub async fn new(os_name: String, dir: &PathBuf) -> Result<Self> {
let cfg = Config {
file: dir.join(CONFIG_NAME),
inner: Arc::new(RwLock::new(InnerConfig {
os_name,
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
})),
};
cfg.sync().await?;
Ok(cfg)
}
pub async fn os_name(&self) -> String {
self.inner.read().await.os_name.clone()
}
/// Sync the inmemory representation to disk.
async fn sync(&self) -> Result<()> {
fs::write(
&self.file,
toml::to_string_pretty(&*self.inner.read().await)?,
)
.await
.context("failed to write config")
}
/// Read a configuration from the given file into memory.
pub async fn from_file(file: PathBuf) -> Result<Self> {
let bytes = fs::read(&file).await.context("failed to read file")?;
let inner: InnerConfig = toml::from_slice(&bytes).context("failed to parse config")?;
Ok(Config {
file,
inner: Arc::new(RwLock::new(inner)),
})
}
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
let cfg = &*self.inner.read().await;
let mut accounts = BTreeMap::new();
for account_config in &cfg.accounts {
let ctx = Context::new(
cfg.os_name.clone(),
account_config.dbfile().into(),
account_config.id,
)
.await?;
accounts.insert(account_config.id, ctx);
}
Ok(accounts)
}
/// Create a new account in the given root directory.
pub async fn new_account(&self, dir: &PathBuf) -> Result<AccountConfig> {
let id = {
let inner = &mut self.inner.write().await;
let id = inner.next_id;
let uuid = Uuid::new_v4();
let target_dir = dir.join(uuid.to_simple_ref().to_string());
inner.accounts.push(AccountConfig {
id,
dir: target_dir.into(),
uuid,
});
inner.next_id += 1;
id
};
self.sync().await?;
self.select_account(id).await.expect("just added");
let cfg = self.get_account(id).await.expect("just added");
Ok(cfg)
}
/// Removes an existing acccount entirely.
pub async fn remove_account(&self, id: u32) -> Result<()> {
{
let inner = &mut *self.inner.write().await;
if let Some(idx) = inner.accounts.iter().position(|e| e.id == id) {
// remove account from the configs
inner.accounts.remove(idx);
}
if inner.selected_account == id {
// reset selected account
inner.selected_account = inner.accounts.get(0).map(|e| e.id).unwrap_or_default();
}
}
self.sync().await
}
pub async fn get_account(&self, id: u32) -> Option<AccountConfig> {
self.inner
.read()
.await
.accounts
.iter()
.find(|e| e.id == id)
.cloned()
}
pub async fn get_selected_account(&self) -> u32 {
self.inner.read().await.selected_account
}
pub async fn select_account(&self, id: u32) -> Result<()> {
{
let inner = &mut *self.inner.write().await;
ensure!(
inner.accounts.iter().any(|e| e.id == id),
"invalid account id: {}",
id
);
inner.selected_account = id;
}
self.sync().await?;
Ok(())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct AccountConfig {
/// Unique id.
pub id: u32,
/// Root directory for all data for this account.
pub dir: std::path::PathBuf,
pub uuid: Uuid,
}
impl AccountConfig {
/// Get the canoncial dbfile name for this configuration.
pub fn dbfile(&self) -> std::path::PathBuf {
self.dir.join(DB_NAME)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[async_std::test]
async fn test_account_new_open() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts1").into();
let accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
let accounts2 = Accounts::open(p).await.unwrap();
assert_eq!(accounts1.accounts.read().await.len(), 1);
assert_eq!(accounts1.config.get_selected_account().await, 1);
assert_eq!(accounts1.dir, accounts2.dir);
assert_eq!(
&*accounts1.config.inner.read().await,
&*accounts2.config.inner.read().await,
);
assert_eq!(
accounts1.accounts.read().await.len(),
accounts2.accounts.read().await.len()
);
}
#[async_std::test]
async fn test_account_new_add_remove() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
let id = accounts.add_account().await.unwrap();
assert_eq!(id, 2);
assert_eq!(accounts.config.get_selected_account().await, id);
assert_eq!(accounts.accounts.read().await.len(), 2);
accounts.select_account(1).await.unwrap();
assert_eq!(accounts.config.get_selected_account().await, 1);
accounts.remove_account(1).await.unwrap();
assert_eq!(accounts.config.get_selected_account().await, 2);
assert_eq!(accounts.accounts.read().await.len(), 1);
}
#[async_std::test]
async fn test_migrate_account() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
let extern_dbfile: PathBuf = dir.path().join("other").into();
let ctx = Context::new("my_os".into(), extern_dbfile.clone(), 0)
.await
.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
.await
.unwrap();
drop(ctx);
accounts
.migrate_account(extern_dbfile.clone())
.await
.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 2);
assert_eq!(accounts.config.get_selected_account().await, 2);
let ctx = accounts.get_selected_account().await;
assert_eq!(
"me@mail.com",
ctx.get_config(crate::config::Config::Addr)
.await
.unwrap()
.unwrap()
);
}
/// Tests that accounts are sorted by ID.
#[async_std::test]
async fn test_accounts_sorted() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
for expected_id in 2..10 {
let id = accounts.add_account().await.unwrap();
assert_eq!(id, expected_id);
}
let ids = accounts.get_all().await;
for (i, expected_id) in (1..10).enumerate() {
assert_eq!(ids.get(i), Some(&expected_id));
}
}
}

View File

@@ -6,13 +6,15 @@ use std::collections::BTreeMap;
use std::str::FromStr;
use std::{fmt, str};
use crate::contact::addr_cmp;
use deltachat_derive::*;
use crate::contact::*;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, SignedPublicKey};
/// Possible values for encryption preference
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive, Sqlx)]
#[repr(u8)]
pub enum EncryptPreference {
NoPreference = 0,

View File

@@ -7,18 +7,15 @@ use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::Error;
use image::GenericImageView;
use num_traits::FromPrimitive;
use thiserror::Error;
use crate::config::Config;
use crate::constants::{
MediaQuality, Viewtype, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE,
WORSE_IMAGE_SIZE,
};
use crate::constants::*;
use crate::context::Context;
use crate::events::EventType;
use crate::error::Error;
use crate::events::Event;
use crate::message;
/// Represents a file in the blob directory.
@@ -58,7 +55,7 @@ impl<'a> BlobObject<'a> {
) -> 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).await?;
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext).await?;
file.write_all(data)
.await
.map_err(|err| BlobError::WriteFailure {
@@ -66,17 +63,11 @@ impl<'a> BlobObject<'a> {
blobname: name.clone(),
cause: err.into(),
})?;
// workaround a bug in async-std
// (the executor does not handle blocking operation in Drop correctly,
// see https://github.com/async-rs/async-std/issues/900 )
let _ = file.flush().await;
let blob = BlobObject {
blobdir,
name: format!("$BLOBDIR/{}", name),
};
context.emit_event(EventType::NewBlobFile(blob.as_name().to_string()));
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
@@ -160,15 +151,11 @@ impl<'a> BlobObject<'a> {
cause: err,
});
}
// workaround, see create() for details
let _ = dst_file.flush().await;
let blob = BlobObject {
blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{}", name),
};
context.emit_event(EventType::NewBlobFile(blob.as_name().to_string()));
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
@@ -181,9 +168,6 @@ impl<'a> BlobObject<'a> {
/// subdirectory is used and [BlobObject::sanitise_name] does not
/// modify the filename.
///
/// Paths into the blob directory may be either defined by an absolute path
/// or by the relative prefix `$BLOBDIR`.
///
/// # Errors
///
/// This merely delegates to the [BlobObject::create_and_copy] and
@@ -195,11 +179,6 @@ impl<'a> BlobObject<'a> {
) -> std::result::Result<BlobObject<'_>, BlobError> {
if src.as_ref().starts_with(context.get_blobdir()) {
BlobObject::from_path(context, src)
} else if src.as_ref().starts_with("$BLOBDIR/") {
BlobObject::from_name(
context,
src.as_ref().to_str().unwrap_or_default().to_string(),
)
} else {
BlobObject::create_and_copy(context, src).await
}
@@ -380,18 +359,27 @@ impl<'a> BlobObject<'a> {
true
}
pub async fn recode_to_avatar_size(&self, context: &Context) -> Result<(), BlobError> {
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,
})?;
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
MediaQuality::Worse => WORSE_AVATAR_SIZE,
};
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
return Ok(());
}
self.recode_to_size(context, blob_abs, img_wh).await
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.into(),
})?;
Ok(())
}
pub async fn recode_to_image_size(&self, context: &Context) -> Result<(), BlobError> {
@@ -402,54 +390,39 @@ impl<'a> BlobObject<'a> {
return Ok(());
}
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
MediaQuality::Worse => WORSE_IMAGE_SIZE,
};
self.recode_to_size(context, blob_abs, img_wh).await
}
async fn recode_to_size(
&self,
context: &Context,
blob_abs: PathBuf,
img_wh: u32,
) -> Result<(), BlobError> {
let mut img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
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,
})?;
let orientation = self.get_exif_orientation(context);
let do_scale = img.width() > img_wh || img.height() > img_wh;
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
let img_wh = if MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
.unwrap_or_default()
== MediaQuality::Balanced
{
BALANCED_IMAGE_SIZE
} else {
WORSE_IMAGE_SIZE
};
if do_scale || do_rotate {
if do_scale {
img = img.thumbnail(img_wh, img_wh);
}
if do_rotate {
img = match orientation {
Ok(90) => img.rotate90(),
Ok(180) => img.rotate180(),
Ok(270) => img.rotate270(),
_ => img,
}
}
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.into(),
})?;
if img.width() <= img_wh && img.height() <= img_wh {
return Ok(());
}
let mut img = img.thumbnail(img_wh, img_wh);
match self.get_exif_orientation(context) {
Ok(90) => img = img.rotate90(),
Ok(180) => img = img.rotate180(),
Ok(270) => img = img.rotate270(),
_ => {}
}
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.into(),
})?;
Ok(())
}
@@ -514,67 +487,75 @@ pub enum BlobError {
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
#[error("Blob has a badname {}", .blobname.display())]
WrongName { blobname: PathBuf },
#[error("Sql: {0}")]
Sql(#[from] crate::sql::Error),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
use crate::test_utils::*;
#[async_std::test]
async fn test_create() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo", b"hello").await.unwrap();
let fname = t.get_blobdir().join("foo");
let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap();
let fname = t.ctx.get_blobdir().join("foo");
let data = fs::read(fname).await.unwrap();
assert_eq!(data, b"hello");
assert_eq!(blob.as_name(), "$BLOBDIR/foo");
assert_eq!(blob.to_abs_path(), t.get_blobdir().join("foo"));
assert_eq!(blob.to_abs_path(), t.ctx.get_blobdir().join("foo"));
}
#[async_std::test]
async fn test_lowercase_ext() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo.TXT", b"hello").await.unwrap();
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello")
.await
.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
}
#[async_std::test]
async fn test_as_file_name() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
assert_eq!(blob.as_file_name(), "foo.txt");
}
#[async_std::test]
async fn test_as_rel_path() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
}
#[async_std::test]
async fn test_suffix() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
assert_eq!(blob.suffix(), Some("txt"));
let blob = BlobObject::create(&t, "bar", b"world").await.unwrap();
let blob = BlobObject::create(&t.ctx, "bar", b"world").await.unwrap();
assert_eq!(blob.suffix(), None);
}
#[async_std::test]
async fn test_create_dup() {
let t = TestContext::new().await;
BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
let foo_path = t.get_blobdir().join("foo.txt");
BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
let foo_path = t.ctx.get_blobdir().join("foo.txt");
assert!(foo_path.exists().await);
BlobObject::create(&t, "foo.txt", b"world").await.unwrap();
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
BlobObject::create(&t.ctx, "foo.txt", b"world")
.await
.unwrap();
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() {
@@ -590,15 +571,15 @@ mod tests {
#[async_std::test]
async fn test_double_ext_preserved() {
let t = TestContext::new().await;
BlobObject::create(&t, "foo.tar.gz", b"hello")
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello")
.await
.unwrap();
let foo_path = t.get_blobdir().join("foo.tar.gz");
let foo_path = t.ctx.get_blobdir().join("foo.tar.gz");
assert!(foo_path.exists().await);
BlobObject::create(&t, "foo.tar.gz", b"world")
BlobObject::create(&t.ctx, "foo.tar.gz", b"world")
.await
.unwrap();
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() {
@@ -616,7 +597,7 @@ mod tests {
async fn test_create_long_names() {
let t = TestContext::new().await;
let s = "1".repeat(150);
let blob = BlobObject::create(&t, &s, b"data").await.unwrap();
let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap();
let blobname = blob.as_name().split('/').last().unwrap();
assert!(blobname.len() < 128);
}
@@ -626,14 +607,14 @@ mod tests {
let t = TestContext::new().await;
let src = t.dir.path().join("src");
fs::write(&src, b"boo").await.unwrap();
let blob = BlobObject::create_and_copy(&t, &src).await.unwrap();
let blob = BlobObject::create_and_copy(&t.ctx, &src).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/src");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
let whoops = t.dir.path().join("whoops");
assert!(BlobObject::create_and_copy(&t, &whoops).await.is_err());
let whoops = t.get_blobdir().join("whoops");
assert!(BlobObject::create_and_copy(&t.ctx, &whoops).await.is_err());
let whoops = t.ctx.get_blobdir().join("whoops");
assert!(!whoops.exists().await);
}
@@ -643,14 +624,14 @@ mod tests {
let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t, &src_ext).await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/external");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
let src_int = t.get_blobdir().join("internal");
let src_int = t.ctx.get_blobdir().join("internal");
fs::write(&src_int, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_int).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
@@ -660,7 +641,7 @@ mod tests {
let t = TestContext::new().await;
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t, &src_ext).await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
assert_eq!(
blob.as_name(),
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
@@ -696,7 +677,7 @@ mod tests {
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
assert!(stem.contains("foo"));
assert!(!stem.contains('?'));
assert!(!stem.contains("?"));
assert_eq!(ext, ".bar");
let (stem, ext) = BlobObject::sanitise_name("no-extension");
@@ -709,10 +690,10 @@ mod tests {
assert!(!stem.contains("ignored"));
assert!(stem.contains("this"));
assert!(stem.contains("forbidden"));
assert!(!stem.contains('/'));
assert!(!stem.contains('\\'));
assert!(!stem.contains(':'));
assert!(!stem.contains('*'));
assert!(!stem.contains('?'));
assert!(!stem.contains("/"));
assert!(!stem.contains("\\"));
assert!(!stem.contains(":"));
assert!(!stem.contains("*"));
assert!(!stem.contains("?"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,14 @@
//! # Chat list module
use anyhow::{bail, ensure, Result};
use async_std::prelude::*;
use sqlx::Row;
use crate::chat;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_DEADDROP,
DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT,
DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
};
use crate::contact::Contact;
use crate::context::Context;
use crate::ephemeral::delete_expired_messages;
use crate::chat::*;
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
use crate::error::{bail, ensure, Result};
use crate::lot::Lot;
use crate::message::{Message, MessageState, MsgId};
use crate::stock_str;
use crate::stock::StockMessage;
/// An object representing a single chatlist in memory.
///
@@ -65,8 +57,9 @@ impl Chatlist {
/// messages from addresses that have no relationship to the configured account.
/// The last of these messages is represented by DC_CHAT_ID_DEADDROP and you can retrieve details
/// about it with chatlist.get_msg_id(). Typically, the UI asks the user "Do you want to chat with NAME?"
/// and offers the options "Start chat", "Block" and "Not now";
/// The decision should be passed to dc_decide_on_contact_request().
/// and offers the options "Yes" (call dc_create_chat_by_msg_id()), "Never" (call dc_block_contact())
/// 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
/// "Show archived chats", if the user clicks this item, the UI should show a
@@ -83,7 +76,7 @@ impl Chatlist {
/// chats
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
/// and hides the device-chat,
/// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
/// - 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
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
@@ -106,7 +99,7 @@ impl Chatlist {
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
// messages get deleted to avoid reloading the same chatlist.
if let Err(err) = delete_expired_messages(context).await {
if let Err(err) = delete_device_expired_messages(context).await {
warn!(context, "Failed to hide expired messages: {}", err);
}
@@ -121,13 +114,6 @@ impl Chatlist {
ChatId::new(0)
};
let process_row = |row: sqlx::Result<sqlx::sqlite::SqliteRow>| {
let row = row?;
let chat_id: ChatId = row.try_get(0)?;
let msg_id: MsgId = row.try_get(1).unwrap_or_default();
Ok((chat_id, msg_id))
};
// select with left join and minimum:
//
// - the inner select must use `hidden` and _not_ `m.hidden`
@@ -143,10 +129,10 @@ impl Chatlist {
// 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: Vec<_> = if let Some(query_contact_id) = query_contact_id {
let mut ids = if let Some(query_contact_id) = query_contact_id {
// show chats shared with a given contact
context.sql.fetch(
sqlx::query("SELECT c.id, m.id
context.sql.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -154,15 +140,15 @@ impl Chatlist {
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1)
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;"
).bind(MessageState::OutDraft).bind(query_contact_id).bind(ChatVisibility::Pinned)
).await?.map(process_row).collect::<sqlx::Result<_>>().await?
ORDER BY c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsx![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
).await?
} else if flag_archived_only {
// show archived chats
// (this includes the archived device-chat; we could skip it,
@@ -170,9 +156,8 @@ impl Chatlist {
// and adapting the number requires larger refactorings and seems not to be worth the effort)
context
.sql
.fetch(
sqlx::query(
"SELECT c.id, m.id
.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -187,13 +172,9 @@ impl Chatlist {
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
)
.bind(MessageState::OutDraft),
paramsx![MessageState::OutDraft],
)
.await?
.map(process_row)
.collect::<sqlx::Result<_>>()
.await?
} else if let Some(query) = query {
let query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query");
@@ -207,9 +188,8 @@ impl Chatlist {
let str_like_cmd = format!("%{}%", query);
context
.sql
.fetch(
sqlx::query(
"SELECT c.id, m.id
.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -224,15 +204,9 @@ impl Chatlist {
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
)
.bind(MessageState::OutDraft)
.bind(skip_id)
.bind(str_like_cmd),
paramsx![MessageState::OutDraft, skip_id, str_like_cmd],
)
.await?
.map(process_row)
.collect::<sqlx::Result<_>>()
.await?
} else {
// show normal chatlist
let sort_id_up = if flag_for_forwarding {
@@ -243,8 +217,7 @@ impl Chatlist {
} else {
ChatId::new(0)
};
let mut ids: Vec<_> = context.sql.fetch(sqlx::query(
let mut ids = context.sql.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -259,21 +232,18 @@ impl Chatlist {
AND c.blocked=0
AND NOT c.archived=?3
GROUP BY c.id
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;"
)
.bind(MessageState::OutDraft)
.bind(skip_id)
.bind(ChatVisibility::Archived)
.bind(sort_id_up)
.bind(ChatVisibility::Pinned)
).await?.map(process_row).collect::<sqlx::Result<_>>().await?;
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsx![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
).await?;
if !flag_no_specials {
if let Some(last_deaddrop_fresh_msg_id) =
get_last_deaddrop_fresh_msg(context).await?
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
{
if !flag_for_forwarding {
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
}
}
add_archived_link_item = true;
@@ -281,11 +251,11 @@ impl Chatlist {
ids
};
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
if ids.is_empty() && flag_add_alldone_hint {
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
}
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
}
Ok(Chatlist { ids })
@@ -340,59 +310,50 @@ 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 (chat_id, lastmsg_id) = match self.ids.get(index) {
Some(ids) => ids,
None => {
let mut ret = Lot::new();
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return Lot::new();
return ret;
}
};
Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
}
pub async fn get_summary2(
context: &Context,
chat_id: ChatId,
lastmsg_id: MsgId,
chat: Option<&Chat>,
) -> Lot {
let mut ret = Lot::new();
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
} else if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
} else if let Ok(chat) = Chat::load_from_db(context, *chat_id).await {
chat_loaded = chat;
&chat_loaded
} else {
return ret;
};
let (lastmsg, lastcontact) =
if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
if lastmsg.from_id == DC_CONTACT_ID_SELF {
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
let lastcontact =
Contact::load_from_db(context, lastmsg.from_id).await.ok();
(Some(lastmsg), lastcontact)
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
}
}
} else {
(None, None)
};
let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id).await {
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).await.ok();
}
Some(lastmsg)
} else {
None
};
if chat.id.is_archived_link() {
ret.text2 = None;
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
{
ret.text2 = Some(stock_str::no_messages(context).await);
ret.text2 = Some(
context
.stock_str(StockMessage::NoMessages)
.await
.to_string(),
);
} else {
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context)
.await;
@@ -407,62 +368,61 @@ impl Chatlist {
}
/// Returns the number of archived chats
pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
let count = context
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
let v: i32 = context
.sql
.count(sqlx::query(
.query_value(
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
))
.await?;
Ok(count)
paramsx![],
)
.await
.unwrap_or_default();
v as u32
}
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Result<Option<MsgId>> {
async 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.
let id = context
context
.sql
.query_get_value(sqlx::query(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;"
)))
.await?;
Ok(id)
.query_value(
r#"
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;
"#,
paramsx![],
)
.await
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message;
use crate::message::ContactRequestDecision;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
use crate::test_utils::*;
#[async_std::test]
async fn test_try_load() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
.await
.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
assert_eq!(chats.get_chat_id(0), chat_id3);
assert_eq!(chats.get_chat_id(1), chat_id2);
@@ -471,24 +431,26 @@ mod tests {
// drafts are sorted to the top
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
chat_id2.set_draft(&t.ctx, Some(&mut msg)).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.get_chat_id(0), chat_id2);
// check chatlist query and archive functionality
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 0);
chat_id1
.set_visibility(&t, ChatVisibility::Archived)
.set_visibility(&t.ctx, ChatVisibility::Archived)
.await
.ok();
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
@@ -497,23 +459,23 @@ mod tests {
#[async_std::test]
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new().await;
t.update_device_chats().await.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
t.ctx.update_device_chats().await.unwrap();
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert!(chats.len() == 3);
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0))
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.await
.unwrap()
.is_self_talk());
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None)
.await
.unwrap();
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
assert!(Chat::load_from_db(&t, chats.get_chat_id(0))
assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.await
.unwrap()
.is_self_talk());
@@ -522,177 +484,49 @@ mod tests {
#[async_std::test]
async fn test_search_special_chat_names() {
let t = TestContext::new().await;
t.update_device_chats().await.unwrap();
t.ctx.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
.await
.unwrap();
assert_eq!(chats.len(), 0);
let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
.await
.unwrap();
assert_eq!(chats.len(), 0);
t.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
t.ctx
.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
t.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
t.ctx
.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
}
#[async_std::test]
async fn test_search_single_chat() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
// receive a one-to-one-message, accept contact request
dc_receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.com\n\
Subject: foo\n\
Message-ID: <msg1234@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
\n\
hello foo\n",
"INBOX",
1,
false,
)
.await?;
let chats = Chatlist::try_load(&t, 0, Some("Bob Authname"), None).await?;
assert_eq!(chats.len(), 0);
let msg = t.get_last_msg().await;
assert_eq!(msg.get_chat_id(), DC_CHAT_ID_DEADDROP);
let chat_id =
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
.await
.unwrap();
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = *contacts.first().unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Authname");
// check, the one-to-one-chat can be found using chatlist search query
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
// change the name of the contact; this also changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Nickname");
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
assert_eq!(chats.len(), 0);
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
assert_eq!(chats.len(), 1);
// revert contact to authname, this again changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Authname");
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
assert_eq!(chats.len(), 0);
Ok(())
}
#[async_std::test]
async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
// receive a one-to-one-message without authname set, accept contact request
dc_receive_imf(
&t,
b"From: bob@example.org\n\
To: alice@example.com\n\
Subject: foo\n\
Message-ID: <msg5678@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2021 22:38:57 +0000\n\
\n\
hello foo\n",
"INBOX",
1,
false,
)
.await?;
let msg = t.get_last_msg().await;
let chat_id =
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
.await
.unwrap();
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = *contacts.first().unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "bob@example.org");
// check, the one-to-one-chat can be found using chatlist search query
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
// change the name of the contact; this also changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Nickname");
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
assert_eq!(chats.len(), 0); // email-addresses are searchable in contacts, not in chats
let chats = Chatlist::try_load(&t, 0, Some("Bob Nickname"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
// revert name change, this again changes the name of the one-to-one-chat to the email-address
let test_id = Contact::create(&t, "", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "bob@example.org");
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
assert_eq!(chats.len(), 0);
// finally, also check that a simple substring-search is working with email-addresses
let chats = Chatlist::try_load(&t, 0, Some("b@exa"), None).await?;
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t, 0, Some("b@exac"), None).await?;
assert_eq!(chats.len(), 0);
Ok(())
}
#[async_std::test]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
chat_id1.set_draft(&t.ctx, Some(&mut msg)).await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t, 0, None).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t.ctx, 0, None).await;
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
}
}

View File

@@ -1,62 +0,0 @@
//! Implementation of Consistent Color Generation
//!
//! Consistent Color Generation is defined in XEP-0392.
//!
//! Color Vision Deficiency correction is not implemented as Delta Chat does not offer
//! corresponding settings.
use hsluv::hsluv_to_rgb;
use sha1::{Digest, Sha1};
/// Converts an identifier to Hue angle.
fn str_to_angle(s: impl AsRef<str>) -> f64 {
let bytes = s.as_ref().as_bytes();
let result = Sha1::digest(bytes);
let checksum: u16 = result.get(0).map_or(0, |&x| u16::from(x))
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
f64::from(checksum) / 65536.0 * 360.0
}
/// Converts RGB tuple to a 24-bit number.
///
/// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8
/// most significant bits corresponding to the red color.
fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
let r = ((r * 256.0) as u32).min(255);
let g = ((g * 256.0) as u32).min(255);
let b = ((b * 256.0) as u32).min(255);
65536 * r + 256 * g + b
}
/// Converts an identifier to RGB color.
///
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
/// half (50.0) to make colors suitable both for light and dark theme.
pub(crate) fn str_to_color(s: impl AsRef<str>) -> u32 {
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_str_to_angle() {
// Test against test vectors from
// https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd
assert!((str_to_angle("Romeo") - 327.255249).abs() < 1e-6);
assert!((str_to_angle("juliet@capulet.lit") - 209.410400).abs() < 1e-6);
assert!((str_to_angle("😺") - 331.199341).abs() < 1e-6);
assert!((str_to_angle("council") - 359.994507).abs() < 1e-6);
assert!((str_to_angle("Board") - 171.430664).abs() < 1e-6);
}
#[test]
fn test_rgb_to_u32() {
assert_eq!(rgb_to_u32((0.0, 0.0, 0.0)), 0);
assert_eq!(rgb_to_u32((1.0, 1.0, 1.0)), 0xffffff);
assert_eq!(rgb_to_u32((0.0, 0.0, 1.0)), 0x0000ff);
assert_eq!(rgb_to_u32((0.0, 1.0, 0.0)), 0x00ff00);
assert_eq!(rgb_to_u32((1.0, 0.0, 0.0)), 0xff0000);
assert_eq!(rgb_to_u32((1.0, 0.5, 0.0)), 0xff8000);
}
}

View File

@@ -1,6 +1,5 @@
//! # Key-value configuration management
use anyhow::Result;
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
@@ -8,13 +7,11 @@ use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input};
use crate::events::EventType;
use crate::job;
use crate::dc_tools::*;
use crate::events::Event;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
use crate::stock_str;
use crate::stock::StockMessage;
/// The available configuration keys.
#[derive(
@@ -27,16 +24,17 @@ pub enum Config {
MailUser,
MailPw,
MailPort,
MailSecurity,
ImapCertificateChecks,
SendServer,
SendUser,
SendPw,
SendPort,
SendSecurity,
SmtpCertificateChecks,
ServerFlags,
#[strum(props(default = "INBOX"))]
ImapFolder,
Displayname,
Selfstatus,
Selfavatar,
@@ -62,21 +60,12 @@ pub enum Config {
#[strum(props(default = "1"))]
MvboxMove,
#[strum(props(default = "0"))]
SentboxMove, // If `MvboxMove` is true, this config is ignored. Currently only used in tests.
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
/// If set to "1", on the first time `start_io()` is called after configuring,
/// the newest existing messages are fetched.
/// Existing recipients are added to the contact database regardless of this setting.
#[strum(props(default = "1"))]
FetchExistingMsgs,
#[strum(props(default = "0"))]
KeyGenType,
@@ -118,9 +107,6 @@ pub enum Config {
ConfiguredInboxFolder,
ConfiguredMvboxFolder,
ConfiguredSentboxFolder,
ConfiguredSpamFolder,
ConfiguredTimestamp,
ConfiguredProvider,
Configured,
#[strum(serialize = "sys.version")]
@@ -131,185 +117,116 @@ pub enum Config {
#[strum(serialize = "sys.config_keys")]
SysConfigKeys,
Bot,
/// Whether we send a warning if the password is wrong (set to false when we send a warning
/// because we do not want to send a second warning)
#[strum(props(default = "0"))]
NotifyAboutWrongPw,
/// address to webrtc instance to use for videochats
WebrtcInstance,
/// Timestamp of the last time housekeeping was run
LastHousekeeping,
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
#[strum(props(default = "60"))]
ScanAllFoldersDebounceSecs,
}
impl Context {
pub async fn config_exists(&self, key: Config) -> Result<bool> {
Ok(self.sql.get_raw_config(key).await?.is_some())
pub async fn config_exists(&self, key: Config) -> bool {
self.sql.get_raw_config(key).await.is_some()
}
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
pub async fn get_config(&self, key: Config) -> Option<String> {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(key).await?;
let rel_path = self.sql.get_raw_config(key).await;
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(key).await?,
_ => self.sql.get_raw_config(key).await,
};
if value.is_some() {
return Ok(value);
return value;
}
// Default values
match key {
Config::Selfstatus => Ok(Some(stock_str::status_line(self).await)),
Config::ConfiguredInboxFolder => Ok(Some("INBOX".to_owned())),
_ => Ok(key.get_str("default").map(|s| s.to_string())),
Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).await.into_owned()),
Config::ConfiguredInboxFolder => Some("INBOX".to_owned()),
_ => key.get_str("default").map(|s| s.to_string()),
}
}
pub async fn get_config_int(&self, key: Config) -> Result<i32> {
pub async fn get_config_int(&self, key: Config) -> i32 {
self.get_config(key)
.await
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
.and_then(|s| s.parse().ok())
.unwrap_or_default()
}
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
self.get_config(key)
.await
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_u64(&self, key: Config) -> Result<u64> {
self.get_config(key)
.await
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
Ok(self.get_config_int(key).await? != 0)
pub async fn get_config_bool(&self, key: Config) -> bool {
self.get_config_int(key).await != 0
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
/// at once, `Some(x)` means delete after `x` seconds.
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteServerAfter).await? {
0 => Ok(None),
1 => Ok(Some(0)),
x => Ok(Some(x as i64)),
pub async fn get_config_delete_server_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteServerAfter).await {
0 => None,
1 => Some(0),
x => Some(x as i64),
}
}
/// Gets the configured provider, as saved in the `configured_provider` value.
///
/// The provider is determined by `get_provider_info()` during configuration and then saved
/// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
return Ok(get_provider_by_id(&cfg));
}
Ok(None)
}
/// Gets configured "delete_device_after" value.
///
/// `None` means never delete the message, `Some(x)` means delete
/// after `x` seconds.
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteDeviceAfter).await? {
0 => Ok(None),
x => Ok(Some(x as i64)),
pub async fn get_config_delete_device_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteDeviceAfter).await {
0 => None,
x => Some(x as i64),
}
}
/// 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 async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
match key {
Config::Selfavatar => {
self.sql
.execute(sqlx::query("UPDATE contacts SET selfavatar_sent=0;"))
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsx![])
.await?;
self.sql
.set_raw_config_bool("attach_selfavatar", true)
.set_raw_config_bool(self, "attach_selfavatar", true)
.await?;
match value {
Some(value) => {
let blob = BlobObject::new_from_path(self, value).await?;
blob.recode_to_avatar_size(self).await?;
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
Ok(())
}
None => {
self.sql.set_raw_config(key, None).await?;
Ok(())
let blob = BlobObject::new_from_path(&self, value).await?;
blob.recode_to_avatar_size(self)?;
self.sql
.set_raw_config(self, key, Some(blob.as_name()))
.await
}
None => self.sql.set_raw_config(self, key, None).await,
}
}
Config::Selfstatus => {
let def = stock_str::status_line(self).await;
let def = self.stock_str(StockMessage::StatusLine).await;
let val = if value.is_none() || value.unwrap() == def {
None
} else {
value
};
self.sql.set_raw_config(key, val).await?;
Ok(())
self.sql.set_raw_config(self, key, val).await
}
Config::DeleteDeviceAfter => {
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
let ret = self.sql.set_raw_config(self, key, value).await;
// Force chatlist reload to delete old messages immediately.
self.emit_event(EventType::MsgsChanged {
self.emit_event(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
ret
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(key, value.as_deref()).await?;
Ok(())
}
Config::DeleteServerAfter => {
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
job::schedule_resync(self).await;
ret
}
_ => {
self.sql.set_raw_config(key, value).await?;
Ok(())
}
_ => self.sql.set_raw_config(self, key, value).await,
}
}
pub async fn set_config_bool(&self, key: Config, value: bool) -> crate::sql::Result<()> {
self.set_config(key, if value { Some("1") } else { None })
.await?;
Ok(())
}
}
/// Returns all available configuration keys concated together.
@@ -331,8 +248,8 @@ mod tests {
use std::string::ToString;
use crate::constants;
use crate::constants::BALANCED_AVATAR_SIZE;
use crate::test_utils::TestContext;
use crate::constants::AVATAR_SIZE;
use crate::test_utils::*;
use image::GenericImageView;
use num_traits::FromPrimitive;
use std::fs::File;
@@ -350,6 +267,11 @@ mod tests {
);
}
#[test]
fn test_default_prop() {
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
}
#[async_std::test]
async fn test_selfavatar_outside_blobdir() {
let t = TestContext::new().await;
@@ -359,14 +281,15 @@ mod tests {
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.jpg");
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists().await);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
@@ -374,14 +297,14 @@ mod tests {
assert_eq!(img.height(), 1000);
let img = image::open(avatar_blob).unwrap();
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
assert_eq!(img.width(), AVATAR_SIZE);
assert_eq!(img.height(), AVATAR_SIZE);
}
#[async_std::test]
async fn test_selfavatar_in_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.get_blobdir().join("avatar.png");
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()
@@ -392,15 +315,16 @@ mod tests {
assert_eq!(img.width(), 900);
assert_eq!(img.height(), 900);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.await
.unwrap();
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
assert_eq!(img.width(), AVATAR_SIZE);
assert_eq!(img.height(), AVATAR_SIZE);
}
#[async_std::test]
@@ -412,9 +336,10 @@ mod tests {
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.png");
let avatar_blob = t.ctx.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists().await);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
@@ -422,21 +347,24 @@ mod tests {
std::fs::metadata(&avatar_blob).unwrap().len(),
avatar_bytes.len() as u64
);
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}
#[async_std::test]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Balanced);
t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
t.ctx
.set_config(Config::MediaQuality, Some("1"))
.await
.unwrap();
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
assert_eq!(media_quality, 1);
assert_eq!(constants::MediaQuality::Worse as i32, 1);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();

View File

@@ -1,31 +1,32 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration
use quick_xml::events::{BytesStart, Event};
use std::io::BufRead;
use std::str::FromStr;
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::constants::*;
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::provider::{Protocol, Socket};
use super::read_url::read_url;
use super::{Error, ServerParams};
use super::Error;
#[derive(Debug)]
struct Server {
pub typ: String,
pub hostname: String,
pub port: u16,
pub sockettype: Socket,
pub username: String,
struct MozAutoconfigure<'a> {
pub in_emailaddr: &'a str,
pub in_emaildomain: &'a str,
pub in_emaillocalpart: &'a str,
pub out: LoginParam,
pub out_imap_set: bool,
pub out_smtp_set: bool,
pub tag_server: MozServer,
pub tag_config: MozConfigTag,
}
#[derive(Debug)]
struct MozAutoconfigure {
pub incoming_servers: Vec<Server>,
pub outgoing_servers: Vec<Server>,
#[derive(Debug, PartialEq)]
enum MozServer {
Undefined,
Imap,
Smtp,
}
#[derive(Debug)]
@@ -37,147 +38,10 @@ enum MozConfigTag {
Username,
}
impl Default for MozConfigTag {
fn default() -> Self {
Self::Undefined
}
}
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
let mut reader = quick_xml::Reader::from_str(xml_raw);
reader.trim_text(true);
impl FromStr for MozConfigTag {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_ref() {
"hostname" => Ok(MozConfigTag::Hostname),
"port" => Ok(MozConfigTag::Port),
"sockettype" => Ok(MozConfigTag::Sockettype),
"username" => Ok(MozConfigTag::Username),
_ => Err(()),
}
}
}
/// Parses a single IncomingServer or OutgoingServer section.
fn parse_server<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
server_event: &BytesStart,
) -> Result<Option<Server>, quick_xml::Error> {
let end_tag = String::from_utf8_lossy(server_event.name())
.trim()
.to_lowercase();
let typ = server_event
.attributes()
.find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type")
.unwrap_or_default()
})
.map(|typ| {
typ.unwrap()
.unescape_and_decode_value(reader)
.unwrap_or_default()
.to_lowercase()
})
.unwrap_or_default();
let mut hostname = None;
let mut port = None;
let mut sockettype = Socket::Automatic;
let mut username = None;
let mut tag_config = MozConfigTag::Undefined;
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf)? {
Event::Start(ref event) => {
tag_config = String::from_utf8_lossy(event.name())
.parse()
.unwrap_or_default();
}
Event::End(ref event) => {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == end_tag {
break;
}
}
Event::Text(ref event) => {
let val = event
.unescape_and_decode(reader)
.unwrap_or_default()
.trim()
.to_owned();
match tag_config {
MozConfigTag::Hostname => hostname = Some(val),
MozConfigTag::Port => port = Some(val.parse().unwrap_or_default()),
MozConfigTag::Username => username = Some(val),
MozConfigTag::Sockettype => {
sockettype = match val.to_lowercase().as_ref() {
"ssl" => Socket::Ssl,
"starttls" => Socket::Starttls,
"plain" => Socket::Plain,
_ => Socket::Automatic,
}
}
_ => {}
}
}
Event::Eof => break,
_ => (),
}
}
if let (Some(hostname), Some(port), Some(username)) = (hostname, port, username) {
Ok(Some(Server {
typ,
hostname,
port,
sockettype,
username,
}))
} else {
Ok(None)
}
}
fn parse_xml_reader<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<MozAutoconfigure, quick_xml::Error> {
let mut incoming_servers = Vec::new();
let mut outgoing_servers = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf)? {
Event::Start(ref event) => {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "incomingserver" {
if let Some(incoming_server) = parse_server(reader, event)? {
incoming_servers.push(incoming_server);
}
} else if tag == "outgoingserver" {
if let Some(outgoing_server) = parse_server(reader, event)? {
outgoing_servers.push(outgoing_server);
}
}
}
Event::Eof => break,
_ => (),
}
buf.clear();
}
Ok(MozAutoconfigure {
incoming_servers,
outgoing_servers,
})
}
/// Parses XML and fills in address and domain placeholders.
fn parse_xml_with_address(in_emailaddr: &str, xml_raw: &str) -> Result<MozAutoconfigure, Error> {
// Split address into local part and domain part.
let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
@@ -185,78 +49,59 @@ fn parse_xml_with_address(in_emailaddr: &str, xml_raw: &str) -> Result<MozAutoco
_ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
};
let mut reader = quick_xml::Reader::from_str(xml_raw);
reader.trim_text(true);
let moz_ac = parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
position: reader.buffer_position(),
error,
})?;
let fill_placeholders = |val: &str| -> String {
val.replace("%EMAILADDRESS%", in_emailaddr)
.replace("%EMAILLOCALPART%", in_emaillocalpart)
.replace("%EMAILDOMAIN%", in_emaildomain)
let mut moz_ac = MozAutoconfigure {
in_emailaddr,
in_emaildomain,
in_emaillocalpart,
out: LoginParam::new(),
out_imap_set: false,
out_smtp_set: false,
tag_server: MozServer::Undefined,
tag_config: MozConfigTag::Undefined,
};
let fill_server_placeholders = |server: Server| -> Server {
Server {
typ: server.typ,
hostname: fill_placeholders(&server.hostname),
port: server.port,
sockettype: server.sockettype,
username: fill_placeholders(&server.username),
let mut buf = Vec::new();
loop {
let event = reader
.read_event(&mut buf)
.map_err(|error| Error::InvalidXml {
position: reader.buffer_position(),
error,
})?;
match event {
quick_xml::events::Event::Start(ref e) => {
moz_autoconfigure_starttag_cb(e, &mut moz_ac, &reader)
}
quick_xml::events::Event::End(ref e) => moz_autoconfigure_endtag_cb(e, &mut moz_ac),
quick_xml::events::Event::Text(ref e) => {
moz_autoconfigure_text_cb(e, &mut moz_ac, &reader)
}
quick_xml::events::Event::Eof => break,
_ => (),
}
};
buf.clear();
}
Ok(MozAutoconfigure {
incoming_servers: moz_ac
.incoming_servers
.into_iter()
.map(fill_server_placeholders)
.collect(),
outgoing_servers: moz_ac
.outgoing_servers
.into_iter()
.map(fill_server_placeholders)
.collect(),
})
if moz_ac.out.mail_server.is_empty()
|| moz_ac.out.mail_port == 0
|| moz_ac.out.send_server.is_empty()
|| moz_ac.out.send_port == 0
{
Err(Error::IncompleteAutoconfig(moz_ac.out))
} else {
Ok(moz_ac.out)
}
}
/// Parses XML into `ServerParams` vector.
fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerParams>, Error> {
let moz_ac = parse_xml_with_address(in_emailaddr, xml_raw)?;
let res = moz_ac
.incoming_servers
.into_iter()
.chain(moz_ac.outgoing_servers.into_iter())
.filter_map(|server| {
let protocol = match server.typ.as_ref() {
"imap" => Some(Protocol::Imap),
"smtp" => Some(Protocol::Smtp),
_ => None,
};
Some(ServerParams {
protocol: protocol?,
socket: server.sockettype,
hostname: server.hostname,
port: server.port,
username: server.username,
})
})
.collect();
Ok(res)
}
pub(crate) async fn moz_autoconfigure(
pub async fn moz_autoconfigure(
context: &Context,
url: impl AsRef<str>,
url: &str,
param_in: &LoginParam,
) -> Result<Vec<ServerParams>, Error> {
let xml_raw = read_url(context, url.as_ref()).await?;
) -> Result<LoginParam, Error> {
let xml_raw = read_url(context, url).await?;
let res = parse_serverparams(&param_in.addr, &xml_raw);
let res = parse_xml(&param_in.addr, &xml_raw);
if let Err(err) = &res {
warn!(
context,
@@ -266,62 +111,212 @@ pub(crate) async fn moz_autoconfigure(
res
}
fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
event: &BytesText,
moz_ac: &mut MozAutoconfigure,
reader: &quick_xml::Reader<B>,
) {
let val = event.unescape_and_decode(reader).unwrap_or_default();
let addr = moz_ac.in_emailaddr;
let email_local = moz_ac.in_emaillocalpart;
let email_domain = moz_ac.in_emaildomain;
let val = val
.trim()
.replace("%EMAILADDRESS%", addr)
.replace("%EMAILLOCALPART%", email_local)
.replace("%EMAILDOMAIN%", email_domain);
match moz_ac.tag_server {
MozServer::Imap => match moz_ac.tag_config {
MozConfigTag::Hostname => moz_ac.out.mail_server = val,
MozConfigTag::Port => moz_ac.out.mail_port = val.parse().unwrap_or_default(),
MozConfigTag::Username => moz_ac.out.mail_user = val,
MozConfigTag::Sockettype => {
let val_lower = val.to_lowercase();
if val_lower == "ssl" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
}
if val_lower == "starttls" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32
}
if val_lower == "plain" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
}
}
_ => {}
},
MozServer::Smtp => match moz_ac.tag_config {
MozConfigTag::Hostname => moz_ac.out.send_server = val,
MozConfigTag::Port => moz_ac.out.send_port = val.parse().unwrap_or_default(),
MozConfigTag::Username => moz_ac.out.send_user = val,
MozConfigTag::Sockettype => {
let val_lower = val.to_lowercase();
if val_lower == "ssl" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
}
if val_lower == "starttls" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32
}
if val_lower == "plain" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
}
}
_ => {}
},
MozServer::Undefined => {}
}
}
fn moz_autoconfigure_endtag_cb(event: &BytesEnd, moz_ac: &mut MozAutoconfigure) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "incomingserver" {
if moz_ac.tag_server == MozServer::Imap {
moz_ac.out_imap_set = true;
}
moz_ac.tag_server = MozServer::Undefined;
moz_ac.tag_config = MozConfigTag::Undefined;
} else if tag == "outgoingserver" {
if moz_ac.tag_server == MozServer::Smtp {
moz_ac.out_smtp_set = true;
}
moz_ac.tag_server = MozServer::Undefined;
moz_ac.tag_config = MozConfigTag::Undefined;
} else {
moz_ac.tag_config = MozConfigTag::Undefined;
}
}
fn moz_autoconfigure_starttag_cb<B: std::io::BufRead>(
event: &BytesStart,
moz_ac: &mut MozAutoconfigure,
reader: &quick_xml::Reader<B>,
) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "incomingserver" {
moz_ac.tag_server = if let Some(typ) = event.attributes().find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type")
.unwrap_or_default()
}) {
let typ = typ
.unwrap()
.unescape_and_decode_value(reader)
.unwrap_or_default()
.to_lowercase();
if typ == "imap" && !moz_ac.out_imap_set {
MozServer::Imap
} else {
MozServer::Undefined
}
} else {
MozServer::Undefined
};
moz_ac.tag_config = MozConfigTag::Undefined;
} else if tag == "outgoingserver" {
moz_ac.tag_server = if !moz_ac.out_smtp_set {
MozServer::Smtp
} else {
MozServer::Undefined
};
moz_ac.tag_config = MozConfigTag::Undefined;
} else if tag == "hostname" {
moz_ac.tag_config = MozConfigTag::Hostname;
} else if tag == "port" {
moz_ac.tag_config = MozConfigTag::Port;
} else if tag == "sockettype" {
moz_ac.tag_config = MozConfigTag::Sockettype;
} else if tag == "username" {
moz_ac.tag_config = MozConfigTag::Username;
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]
fn test_parse_outlook_autoconfig() {
let xml_raw = include_str!("../../test-data/autoconfig/outlook.com.xml");
let res = parse_serverparams("example@outlook.com", xml_raw).expect("XML parsing failed");
assert_eq!(res[0].protocol, Protocol::Imap);
assert_eq!(res[0].hostname, "outlook.office365.com");
assert_eq!(res[0].port, 993);
assert_eq!(res[1].protocol, Protocol::Smtp);
assert_eq!(res[1].hostname, "smtp.office365.com");
assert_eq!(res[1].port, 587);
}
#[test]
fn test_parse_lakenet_autoconfig() {
let xml_raw = include_str!("../../test-data/autoconfig/lakenet.ch.xml");
let res =
parse_xml_with_address("example@lakenet.ch", xml_raw).expect("XML parsing failed");
assert_eq!(res.incoming_servers.len(), 4);
assert_eq!(res.incoming_servers[0].typ, "imap");
assert_eq!(res.incoming_servers[0].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[0].port, 993);
assert_eq!(res.incoming_servers[0].sockettype, Socket::Ssl);
assert_eq!(res.incoming_servers[0].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[1].typ, "imap");
assert_eq!(res.incoming_servers[1].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[1].port, 143);
assert_eq!(res.incoming_servers[1].sockettype, Socket::Starttls);
assert_eq!(res.incoming_servers[1].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[2].typ, "pop3");
assert_eq!(res.incoming_servers[2].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[2].port, 995);
assert_eq!(res.incoming_servers[2].sockettype, Socket::Ssl);
assert_eq!(res.incoming_servers[2].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[3].typ, "pop3");
assert_eq!(res.incoming_servers[3].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[3].port, 110);
assert_eq!(res.incoming_servers[3].sockettype, Socket::Starttls);
assert_eq!(res.incoming_servers[3].username, "example@lakenet.ch");
assert_eq!(res.outgoing_servers.len(), 1);
assert_eq!(res.outgoing_servers[0].typ, "smtp");
assert_eq!(res.outgoing_servers[0].hostname, "mail.lakenet.ch");
assert_eq!(res.outgoing_servers[0].port, 587);
assert_eq!(res.outgoing_servers[0].sockettype, Socket::Starttls);
assert_eq!(res.outgoing_servers[0].username, "example@lakenet.ch");
// Copied from https://autoconfig.thunderbird.net/v1.1/outlook.com on 2019-10-11
let xml_raw =
"<clientConfig version=\"1.1\">
<emailProvider id=\"outlook.com\">
<domain>hotmail.com</domain>
<domain>hotmail.co.uk</domain>
<domain>hotmail.co.jp</domain>
<domain>hotmail.com.br</domain>
<domain>hotmail.de</domain>
<domain>hotmail.fr</domain>
<domain>hotmail.it</domain>
<domain>hotmail.es</domain>
<domain>live.com</domain>
<domain>live.co.uk</domain>
<domain>live.co.jp</domain>
<domain>live.de</domain>
<domain>live.fr</domain>
<domain>live.it</domain>
<domain>live.jp</domain>
<domain>msn.com</domain>
<domain>outlook.com</domain>
<displayName>Outlook.com (Microsoft)</displayName>
<displayShortName>Outlook</displayShortName>
<incomingServer type=\"exchange\">
<hostname>outlook.office365.com</hostname>
<port>443</port>
<username>%EMAILADDRESS%</username>
<socketType>SSL</socketType>
<authentication>OAuth2</authentication>
<owaURL>https://outlook.office365.com/owa/</owaURL>
<ewsURL>https://outlook.office365.com/ews/exchange.asmx</ewsURL>
<useGlobalPreferredServer>true</useGlobalPreferredServer>
</incomingServer>
<incomingServer type=\"imap\">
<hostname>outlook.office365.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<incomingServer type=\"pop3\">
<hostname>outlook.office365.com</hostname>
<port>995</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
<pop3>
<leaveMessagesOnServer>true</leaveMessagesOnServer>
<!-- Outlook.com docs specifically mention that POP3 deletes have effect on the main inbox on webmail and IMAP -->
</pop3>
</incomingServer>
<outgoingServer type=\"smtp\">
<hostname>smtp.office365.com</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
<documentation url=\"http://windows.microsoft.com/en-US/windows/outlook/send-receive-from-app\">
<descr lang=\"en\">Set up an email app with Outlook.com</descr>
</documentation>
</emailProvider>
<webMail>
<loginPage url=\"https://www.outlook.com/\"/>
<loginPageInfo url=\"https://www.outlook.com/\">
<username>%EMAILADDRESS%</username>
<usernameField id=\"i0116\" name=\"login\"/>
<passwordField id=\"i0118\" name=\"passwd\"/>
<loginButton id=\"idSIButton9\" name=\"SI\"/>
</loginPageInfo>
</webMail>
</clientConfig>";
let res = parse_xml("example@outlook.com", xml_raw).expect("XML parsing failed");
assert_eq!(res.mail_server, "outlook.office365.com");
assert_eq!(res.mail_port, 993);
assert_eq!(res.send_server, "smtp.office365.com");
assert_eq!(res.send_port, 587);
}
}

View File

@@ -1,194 +1,123 @@
//! # Outlook's Autodiscover
//!
//! This module implements autoconfiguration via POX (Plain Old XML) interface to Autodiscover
//! Service. Newer SOAP interface, introduced in Exchange 2010, is not used.
//! Outlook's Autodiscover
use quick_xml::events::Event;
use std::io::BufRead;
use quick_xml::events::BytesEnd;
use crate::constants::*;
use crate::context::Context;
use crate::provider::{Protocol, Socket};
use crate::login_param::LoginParam;
use super::read_url::read_url;
use super::{Error, ServerParams};
use super::Error;
/// Result of parsing a single `Protocol` tag.
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox
#[derive(Debug)]
struct ProtocolTag {
/// Server type, such as "IMAP", "SMTP" or "POP3".
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox
pub typ: String,
/// Server identifier, hostname or IP address for IMAP and SMTP.
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox
pub server: String,
/// Network port.
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox
pub port: u16,
/// Whether connection should be secure, "on" or "off", default is "on".
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox
pub ssl: bool,
struct OutlookAutodiscover {
pub out: LoginParam,
pub out_imap_set: bool,
pub out_smtp_set: bool,
pub config_type: Option<String>,
pub config_server: String,
pub config_port: i32,
pub config_ssl: String,
pub config_redirecturl: Option<String>,
}
enum ParsingResult {
Protocols(Vec<ProtocolTag>),
/// XML redirect via `RedirectUrl` tag.
LoginParam(LoginParam),
RedirectUrl(String),
}
/// Parses a single Protocol section.
fn parse_protocol<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<Option<ProtocolTag>, quick_xml::Error> {
let mut protocol_type = None;
let mut protocol_server = None;
let mut protocol_port = None;
let mut protocol_ssl = true;
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
let mut outlk_ad = OutlookAutodiscover {
out: LoginParam::new(),
out_imap_set: false,
out_smtp_set: false,
config_type: None,
config_server: String::new(),
config_port: 0,
config_ssl: String::new(),
config_redirecturl: None,
};
let mut reader = quick_xml::Reader::from_str(&xml_raw);
reader.trim_text(true);
let mut buf = Vec::new();
let mut current_tag: Option<String> = None;
loop {
match reader.read_event(&mut buf)? {
Event::Start(ref event) => {
current_tag = Some(String::from_utf8_lossy(event.name()).trim().to_lowercase());
}
Event::End(ref event) => {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
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" {
break;
}
if Some(tag) == current_tag {
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);
}
}
Event::Text(ref e) => {
let val = e.unescape_and_decode(reader).unwrap_or_default();
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" => protocol_type = Some(val.trim().to_string()),
"server" => protocol_server = Some(val.trim().to_string()),
"port" => protocol_port = Some(val.trim().parse().unwrap_or_default()),
"ssl" => {
protocol_ssl = match val.trim() {
"on" => true,
"off" => false,
_ => true,
}
"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()),
_ => {}
};
}
}
Event::Eof => break,
_ => {}
}
}
if let (Some(protocol_type), Some(protocol_server), Some(protocol_port)) =
(protocol_type, protocol_server, protocol_port)
{
Ok(Some(ProtocolTag {
typ: protocol_type,
server: protocol_server,
port: protocol_port,
ssl: protocol_ssl,
}))
} else {
Ok(None)
}
}
/// Parses `RedirectUrl` tag.
fn parse_redirecturl<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<String, quick_xml::Error> {
let mut buf = Vec::new();
match reader.read_event(&mut buf)? {
Event::Text(ref e) => {
let val = e.unescape_and_decode(reader).unwrap_or_default();
Ok(val.trim().to_string())
}
_ => Ok("".to_string()),
}
}
fn parse_xml_reader<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<ParsingResult, quick_xml::Error> {
let mut protocols = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf)? {
Event::Start(ref e) => {
let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase();
if tag == "protocol" {
if let Some(protocol) = parse_protocol(reader)? {
protocols.push(protocol);
}
} else if tag == "redirecturl" {
let redirecturl = parse_redirecturl(reader)?;
return Ok(ParsingResult::RedirectUrl(redirecturl));
}
}
Event::Eof => break,
quick_xml::events::Event::Eof => break,
_ => (),
}
buf.clear();
}
Ok(ParsingResult::Protocols(protocols))
// XML redirect via redirecturl
let res = if outlk_ad.config_redirecturl.is_none()
|| outlk_ad.config_redirecturl.as_ref().unwrap().is_empty()
{
if outlk_ad.out.mail_server.is_empty()
|| outlk_ad.out.mail_port == 0
|| outlk_ad.out.send_server.is_empty()
|| outlk_ad.out.send_port == 0
{
return Err(Error::IncompleteAutoconfig(outlk_ad.out));
}
ParsingResult::LoginParam(outlk_ad.out)
} else {
ParsingResult::RedirectUrl(outlk_ad.config_redirecturl.unwrap())
};
Ok(res)
}
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
let mut reader = quick_xml::Reader::from_str(xml_raw);
reader.trim_text(true);
parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
position: reader.buffer_position(),
error,
})
}
fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
protocols
.into_iter()
.filter_map(|protocol| {
Some(ServerParams {
protocol: match protocol.typ.to_lowercase().as_ref() {
"imap" => Some(Protocol::Imap),
"smtp" => Some(Protocol::Smtp),
_ => None,
}?,
socket: match protocol.ssl {
true => Socket::Automatic,
false => Socket::Plain,
},
hostname: protocol.server,
port: protocol.port,
username: String::new(),
})
})
.collect()
}
pub(crate) async fn outlk_autodiscover(
pub async fn outlk_autodiscover(
context: &Context,
mut url: String,
) -> Result<Vec<ServerParams>, Error> {
url: &str,
_param_in: &LoginParam,
) -> Result<LoginParam, Error> {
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).await?;
@@ -198,18 +127,47 @@ pub(crate) async fn outlk_autodiscover(
}
match res? {
ParsingResult::RedirectUrl(redirect_url) => url = redirect_url,
ParsingResult::Protocols(protocols) => {
return Ok(protocols_to_serverparams(protocols));
}
ParsingResult::LoginParam(login_param) => return Ok(login_param),
}
}
Err(Error::RedirectionError)
}
fn outlk_autodiscover_endtag_cb(event: &BytesEnd, outlk_ad: &mut OutlookAutodiscover) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "protocol" {
if let Some(type_) = &outlk_ad.config_type {
let port = outlk_ad.config_port;
let ssl_on = outlk_ad.config_ssl == "on";
let ssl_off = outlk_ad.config_ssl == "off";
if type_ == "imap" && !outlk_ad.out_imap_set {
outlk_ad.out.mail_server =
std::mem::replace(&mut outlk_ad.config_server, String::new());
outlk_ad.out.mail_port = port;
if ssl_on {
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
} else if ssl_off {
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
}
outlk_ad.out_imap_set = true
} else if type_ == "smtp" && !outlk_ad.out_smtp_set {
outlk_ad.out.send_server =
std::mem::replace(&mut outlk_ad.config_server, String::new());
outlk_ad.out.send_port = outlk_ad.config_port;
if ssl_on {
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
} else if ssl_off {
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
}
outlk_ad.out_smtp_set = true
}
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]
@@ -226,13 +184,16 @@ mod tests {
</Response>
</Autodiscover>
").expect("XML is not parsed successfully");
if let ParsingResult::RedirectUrl(url) = res {
assert_eq!(
url,
"https://mail.example.com/autodiscover/autodiscover.xml"
);
} else {
panic!("redirecturl is not found");
match res {
ParsingResult::LoginParam(_lp) => {
panic!("redirecturl is not found");
}
ParsingResult::RedirectUrl(url) => {
assert_eq!(
url,
"https://mail.example.com/autodiscover/autodiscover.xml"
);
}
}
}
@@ -267,16 +228,11 @@ mod tests {
.expect("XML is not parsed successfully");
match res {
ParsingResult::Protocols(protocols) => {
assert_eq!(protocols[0].typ, "IMAP");
assert_eq!(protocols[0].server, "example.com");
assert_eq!(protocols[0].port, 993);
assert_eq!(protocols[0].ssl, true);
assert_eq!(protocols[1].typ, "SMTP");
assert_eq!(protocols[1].server, "smtp.example.com");
assert_eq!(protocols[1].port, 25);
assert_eq!(protocols[1].ssl, false);
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");

View File

@@ -1,60 +1,43 @@
//! Email accounts autoconfiguration process module
#![forbid(clippy::indexing_slicing)]
mod auto_mozilla;
mod auto_outlook;
mod read_url;
mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use anyhow::{bail, ensure, format_err, Context as _, Result};
use async_std::prelude::*;
use async_std::task;
use itertools::Itertools;
use job::Action;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::dc_tools::EmailAddress;
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::imap::Imap;
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::login_param::{CertificateChecks, LoginParam};
use crate::message::Message;
use crate::oauth2::dc_get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::oauth2::*;
use crate::smtp::Smtp;
use crate::stock_str;
use crate::{chat, e2ee, provider};
use crate::{config::Config, dc_tools::time};
use crate::{
constants::{Viewtype, DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2},
job,
};
use crate::{context::Context, param::Params};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use server_params::{expand_param_vector, ServerParams};
macro_rules! progress {
($context:tt, $progress:expr, $comment:expr) => {
($context:tt, $progress:expr) => {
assert!(
$progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
$context.emit_event($crate::events::EventType::ConfigureProgress {
progress: $progress,
comment: $comment,
});
};
($context:tt, $progress:expr) => {
progress!($context, $progress, None);
$context.emit_event($crate::events::Event::ConfigureProgress($progress));
};
}
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> Result<bool> {
self.sql
.get_raw_config_bool("configured")
.await
.map_err(Into::into)
pub async fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool("configured").await
}
/// Configures this account with the currently set parameters.
@@ -87,14 +70,13 @@ impl Context {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let mut param = LoginParam::from_database(self, "").await?;
let mut param = LoginParam::from_database(self, "").await;
let success = configure(self, &mut param).await;
self.set_config(Config::NotifyAboutWrongPw, None).await?;
if let Some(provider) = param.provider {
if let Some(provider) = provider::get_provider_info(&param.addr) {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if !self.config_exists(def.key).await? {
if !self.config_exists(def.key).await {
info!(self, "apply config_defaults {}={}", def.key, def.value);
self.set_config(def.key, Some(def.value)).await?;
} else {
@@ -120,25 +102,12 @@ impl Context {
match success {
Ok(_) => {
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
.await?;
progress!(self, 1000);
Ok(())
}
Err(err) => {
progress!(
self,
0,
Some(
stock_str::configuration_failed(
self,
// We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}:
format!("{:#}", err),
)
.await
)
);
error!(self, "Configure Failed: {}", err);
progress!(self, 0);
Err(err)
}
}
@@ -146,47 +115,27 @@ impl Context {
}
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let mut param_autoconfig: Option<LoginParam> = None;
let mut keep_flags = 0;
// Read login parameters from the database
progress!(ctx, 1);
// Check basic settings.
ensure!(!param.addr.is_empty(), "Please enter an email address.");
// Only check for IMAP password, SMTP password is an "advanced" setting.
ensure!(!param.imap.password.is_empty(), "Please enter a password.");
if param.smtp.password.is_empty() {
param.smtp.password = param.imap.password.clone()
}
// Normalize authentication flags.
let oauth2 = match param.server_flags & DC_LP_AUTH_FLAGS as i32 {
DC_LP_AUTH_OAUTH2 => true,
DC_LP_AUTH_NORMAL => false,
_ => false,
};
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
param.server_flags |= if oauth2 {
DC_LP_AUTH_OAUTH2 as i32
} else {
DC_LP_AUTH_NORMAL as i32
};
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
// Step 1: Load the parameters and check email-address and password
if oauth2 {
if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 {
// the used oauth2 addr may differ, check this.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.imap.password)
.await?
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.mail_pw)
.await
.and_then(|e| e.parse().ok())
{
info!(ctx, "Authorized address is {}", oauth2_addr);
param.addr = oauth2_addr;
ctx.sql
.set_raw_config("addr", Some(param.addr.as_str()))
.set_raw_config(ctx, "addr", Some(param.addr.as_str()))
.await?;
}
progress!(ctx, 20);
@@ -200,208 +149,134 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// Step 2: Autoconfig
progress!(ctx, 200);
let param_autoconfig;
if param.imap.server.is_empty()
&& param.imap.port == 0
&& param.imap.security == Socket::Automatic
&& param.imap.user.is_empty()
&& param.smtp.server.is_empty()
&& param.smtp.port == 0
&& param.smtp.security == Socket::Automatic
&& param.smtp.user.is_empty()
// param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then
// param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for
// autoconfig or not
if param.mail_server.is_empty()
&& param.mail_port == 0
&& param.send_server.is_empty()
&& param.send_port == 0
&& param.send_user.is_empty()
&& (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0
{
// no advanced parameters entered by the user: query provider-database or do Autoconfig
keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2;
if let Some(new_param) = get_offline_autoconfig(ctx, &param) {
// got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting
param_autoconfig = Some(new_param);
}
info!(
ctx,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&param_domain).await {
param.provider = Some(provider);
match provider.status {
provider::Status::Ok | provider::Status::Preparation => {
if provider.server.is_empty() {
info!(ctx, "offline autoconfig found, but no servers defined");
param_autoconfig = None;
} else {
info!(ctx, "offline autoconfig found");
let servers = provider
.server
.iter()
.map(|s| ServerParams {
protocol: s.protocol,
socket: s.socket,
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::Email => param.addr.to_string(),
UsernamePattern::Emaillocalpart => {
if let Some(at) = param.addr.find('@') {
param.addr.split_at(at).0.to_string()
} else {
param.addr.to_string()
}
}
},
})
.collect();
param_autoconfig = Some(servers)
}
}
provider::Status::Broken => {
info!(ctx, "offline autoconfig found, provider is broken");
param_autoconfig = None;
}
}
} else {
info!(ctx, "no offline autoconfig found");
if param_autoconfig.is_none() {
param_autoconfig =
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await;
}
} else {
param_autoconfig = None;
}
// C. Do we have any autoconfig result?
progress!(ctx, 500);
let mut servers = param_autoconfig.unwrap_or_default();
if !servers
.iter()
.any(|server| server.protocol == Protocol::Imap)
{
servers.push(ServerParams {
protocol: Protocol::Imap,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
})
}
if !servers
.iter()
.any(|server| server.protocol == Protocol::Smtp)
{
servers.push(ServerParams {
protocol: Protocol::Smtp,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
})
}
let servers = expand_param_vector(servers, &param.addr, &param_domain);
progress!(ctx, 550);
// Spawn SMTP configuration task
let mut smtp = Smtp::new();
let context_smtp = ctx.clone();
let mut smtp_param = param.smtp.clone();
let smtp_addr = param.addr.clone();
let smtp_servers: Vec<ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Smtp)
.cloned()
.collect();
let provider_strict_tls = param.provider.map_or(false, |provider| provider.strict_tls);
let smtp_config_task = task::spawn(async move {
let mut smtp_configured = false;
let mut errors = Vec::new();
for smtp_server in smtp_servers {
smtp_param.user = smtp_server.username.clone();
smtp_param.server = smtp_server.hostname.clone();
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
match try_smtp_one_param(
&context_smtp,
&smtp_param,
&smtp_addr,
oauth2,
provider_strict_tls,
&mut smtp,
)
.await
{
Ok(_) => {
smtp_configured = true;
break;
}
Err(e) => errors.push(e),
}
if let Some(ref cfg) = param_autoconfig {
info!(ctx, "Got autoconfig: {}", &cfg);
if !cfg.mail_user.is_empty() {
param.mail_user = cfg.mail_user.clone();
}
// all other values are always NULL when entering autoconfig
param.mail_server = cfg.mail_server.clone();
param.mail_port = cfg.mail_port;
param.send_server = cfg.send_server.clone();
param.send_port = cfg.send_port;
param.send_user = cfg.send_user.clone();
param.server_flags = cfg.server_flags;
// although param_autoconfig's data are no longer needed from,
// it is used to later to prevent trying variations of port/server/logins
}
param.server_flags |= keep_flags;
if smtp_configured {
Ok(smtp_param)
// Step 3: Fill missing fields with defaults
if param.mail_server.is_empty() {
param.mail_server = format!("imap.{}", param_domain,)
}
if param.mail_port == 0 {
param.mail_port = if 0 != param.server_flags & (0x100 | 0x400) {
143
} else {
Err(errors)
993
}
});
}
if param.mail_user.is_empty() {
param.mail_user = param.addr.clone();
}
if param.send_server.is_empty() && !param.mail_server.is_empty() {
param.send_server = param.mail_server.clone();
if param.send_server.starts_with("imap.") {
param.send_server = param.send_server.replacen("imap", "smtp", 1);
}
}
if param.send_port == 0 {
param.send_port = if 0 != param.server_flags & DC_LP_SMTP_SOCKET_STARTTLS as i32 {
587
} else if 0 != param.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 {
25
} else {
465
}
}
if param.send_user.is_empty() && !param.mail_user.is_empty() {
param.send_user = param.mail_user.clone();
}
if param.send_pw.is_empty() && !param.mail_pw.is_empty() {
param.send_pw = param.mail_pw.clone()
}
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_AUTH_FLAGS as i32) {
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
param.server_flags |= DC_LP_AUTH_NORMAL as i32
}
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_IMAP_SOCKET_FLAGS as i32) {
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
param.server_flags |= if param.send_port == 143 {
DC_LP_IMAP_SOCKET_STARTTLS as i32
} else {
DC_LP_IMAP_SOCKET_SSL as i32
}
}
if !dc_exactly_one_bit_set(param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32)) {
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= if param.send_port == 587 {
DC_LP_SMTP_SOCKET_STARTTLS as i32
} else if param.send_port == 25 {
DC_LP_SMTP_SOCKET_PLAIN as i32
} else {
DC_LP_SMTP_SOCKET_SSL as i32
}
}
// do we have a complete configuration?
ensure!(
!param.mail_server.is_empty()
&& param.mail_port != 0
&& !param.mail_user.is_empty()
&& !param.mail_pw.is_empty()
&& !param.send_server.is_empty()
&& param.send_port != 0
&& !param.send_user.is_empty()
&& !param.send_pw.is_empty()
&& param.server_flags != 0,
"Account settings incomplete."
);
progress!(ctx, 600);
// Configure IMAP
let (_s, r) = async_std::channel::bounded(1);
// try to connect to IMAP - if we did not got an autoconfig,
// do some further tries with different settings and username variations
let (_s, r) = async_std::sync::channel(1);
let mut imap = Imap::new(r);
let mut imap_configured = false;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Imap)
.collect();
let imap_servers_count = imap_servers.len();
let mut errors = Vec::new();
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
param.imap.user = imap_server.username.clone();
param.imap.server = imap_server.hostname.clone();
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
match try_imap_one_param(
ctx,
&param.imap,
&param.addr,
oauth2,
provider_strict_tls,
&mut imap,
)
.await
{
Ok(_) => {
imap_configured = true;
break;
}
Err(e) => errors.push(e),
}
progress!(
ctx,
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
if !imap_configured {
bail!(nicer_configuration_error(ctx, errors).await);
}
progress!(ctx, 850);
// Wait for SMTP configuration
match smtp_config_task.await {
Ok(smtp_param) => {
param.smtp = smtp_param;
}
Err(errors) => {
bail!(nicer_configuration_error(ctx, errors).await);
}
}
try_imap_connections(ctx, param, param_autoconfig.is_some(), &mut imap).await?;
progress!(ctx, 800);
try_smtp_connections(ctx, param, param_autoconfig.is_some()).await?;
progress!(ctx, 900);
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await?
|| ctx.get_config_bool(Config::MvboxMove).await?;
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|| ctx.get_config_bool(Config::MvboxMove).await;
imap.configure_folders(ctx, create_mvbox).await?;
@@ -416,27 +291,78 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// "configured_" prefix; also write the "configured"-flag */
// the trailing underscore is correct
param.save_to_database(ctx, "configured_").await?;
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
ctx.sql.set_raw_config_bool(ctx, "configured", true).await?;
progress!(ctx, 920);
e2ee::ensure_secret_key_exists(ctx).await?;
info!(ctx, "key generation completed");
job::add(
ctx,
job::Job::new(Action::FetchExistingMsgs, 0, Params::new(), 0),
)
.await;
progress!(ctx, 940);
update_device_chats_handle.await?;
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
enum AutoconfigProvider {
Mozilla,
Outlook,
}
#[derive(Debug, PartialEq, Eq)]
struct AutoconfigSource {
provider: AutoconfigProvider,
url: String,
}
impl AutoconfigSource {
fn all(domain: &str, addr: &str) -> [Self; 5] {
[
AutoconfigSource {
provider: AutoconfigProvider::Mozilla,
url: format!(
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
domain, addr,
),
},
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
AutoconfigSource {
provider: AutoconfigProvider::Mozilla,
url: format!(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
domain, addr
),
},
AutoconfigSource {
provider: AutoconfigProvider::Outlook,
url: format!("https://{}/autodiscover/autodiscover.xml", domain),
},
// Outlook uses always SSL but different domains (this comment describes the next two steps)
AutoconfigSource {
provider: AutoconfigProvider::Outlook,
url: format!(
"https://{}{}/autodiscover/autodiscover.xml",
"autodiscover.", domain
),
},
// always SSL for Thunderbird's database
AutoconfigSource {
provider: AutoconfigProvider::Mozilla,
url: format!("https://autoconfig.thunderbird.net/v1.1/{}", domain),
},
]
}
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<LoginParam> {
let params = match self.provider {
AutoconfigProvider::Mozilla => moz_autoconfigure(ctx, &self.url, &param).await?,
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url, &param).await?,
};
Ok(params)
}
}
/// Retrieve available autoconfigurations.
///
/// A Search configurations from the domain used in the email-address, prefer encrypted
@@ -446,161 +372,228 @@ async fn get_autoconfig(
param: &LoginParam,
param_domain: &str,
param_addr_urlencoded: &str,
) -> Option<Vec<ServerParams>> {
if let Ok(res) = moz_autoconfigure(
ctx,
format!(
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
param_domain, param_addr_urlencoded
),
param,
)
.await
{
return Some(res);
}
progress!(ctx, 300);
) -> Option<LoginParam> {
let sources = AutoconfigSource::all(param_domain, param_addr_urlencoded);
if let Ok(res) = moz_autoconfigure(
ctx,
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
format!(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
&param_domain, &param_addr_urlencoded
),
param,
)
.await
{
return Some(res);
}
progress!(ctx, 310);
// Outlook uses always SSL but different domains (this comment describes the next two steps)
if let Ok(res) = outlk_autodiscover(
ctx,
format!("https://{}/autodiscover/autodiscover.xml", &param_domain),
)
.await
{
return Some(res);
}
progress!(ctx, 320);
if let Ok(res) = outlk_autodiscover(
ctx,
format!(
"https://autodiscover.{}/autodiscover/autodiscover.xml",
&param_domain
),
)
.await
{
return Some(res);
}
progress!(ctx, 330);
// always SSL for Thunderbird's database
if let Ok(res) = moz_autoconfigure(
ctx,
format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain),
param,
)
.await
{
return Some(res);
let mut progress = 300;
for source in &sources {
let res = source.fetch(ctx, param).await;
progress!(ctx, progress);
progress += 10;
if let Ok(res) = res {
return Some(res);
}
}
None
}
async fn try_imap_one_param(
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&param.addr) {
match provider.status {
provider::Status::OK | provider::Status::PREPARATION => {
let imap = provider.get_imap_server();
let smtp = provider.get_smtp_server();
// clippy complains about these is_some()/unwrap() settings,
// however, rewriting the code to "if let" would make things less obvious,
// esp. if we allow more combinations of servers (pop, jmap).
// therefore, #[allow(clippy::unnecessary_unwrap)] is added above.
if let Some(imap) = imap {
if let Some(smtp) = smtp {
let mut p = LoginParam::new();
p.addr = param.addr.clone();
p.mail_server = imap.hostname.to_string();
p.mail_user = imap.apply_username_pattern(param.addr.clone());
p.mail_port = imap.port as i32;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match imap.socket {
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
};
p.send_server = smtp.hostname.to_string();
p.send_user = smtp.apply_username_pattern(param.addr.clone());
p.send_port = smtp.port as i32;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match smtp.socket {
provider::Socket::STARTTLS => DC_LP_SMTP_SOCKET_STARTTLS as i32,
provider::Socket::SSL => DC_LP_SMTP_SOCKET_SSL as i32,
};
info!(context, "offline autoconfig found: {}", p);
return Some(p);
}
}
info!(context, "offline autoconfig found, but no servers defined");
return None;
}
provider::Status::BROKEN => {
info!(context, "offline autoconfig found, provider is broken");
return None;
}
}
}
info!(context, "no offline autoconfig found");
None
}
async fn try_imap_connections(
context: &Context,
param: &ServerLoginParam,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
param: &mut LoginParam,
was_autoconfig: bool,
imap: &mut Imap,
) -> Result<(), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
) -> Result<()> {
// manually_set_param is used to check whether a particular setting was set manually by the user.
// If yes, we do not want to change it to avoid confusing error messages
// (you set port 443, but the app tells you it couldn't connect on port 993).
let manually_set_param = LoginParam::from_database(context, "").await;
if let Err(err) = imap
.connect(context, param, addr, oauth2, provider_strict_tls)
// progress 650 and 660
if try_imap_connection(context, param, &manually_set_param, was_autoconfig, 0, imap)
.await
.is_ok()
{
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
info!(context, "success: {}", inf);
Ok(())
return Ok(()); // we directly return here if it was autoconfig or the connection succeeded
}
progress!(context, 670);
// try_imap_connection() changed the flags and port. Change them back:
if manually_set_param.server_flags & DC_LP_IMAP_SOCKET_FLAGS == 0 {
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
param.server_flags |= DC_LP_IMAP_SOCKET_SSL;
}
if manually_set_param.mail_port == 0 {
param.mail_port = 993;
}
if let Some(at) = param.mail_user.find('@') {
param.mail_user = param.mail_user.split_at(at).0.to_string();
}
if let Some(at) = param.send_user.find('@') {
param.send_user = param.send_user.split_at(at).0.to_string();
}
// progress 680 and 690
try_imap_connection(context, param, &manually_set_param, was_autoconfig, 1, imap).await
}
async fn try_smtp_one_param(
async fn try_imap_connection(
context: &Context,
param: &ServerLoginParam,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
smtp: &mut Smtp,
) -> Result<(), ConfigurationError> {
param: &mut LoginParam,
manually_set_param: &LoginParam,
was_autoconfig: bool,
variation: usize,
imap: &mut Imap,
) -> Result<()> {
if try_imap_one_param(context, param, imap).await.is_ok() {
return Ok(());
}
if was_autoconfig {
return Ok(());
}
progress!(context, 650 + variation * 30);
if manually_set_param.server_flags & DC_LP_IMAP_SOCKET_FLAGS == 0 {
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS;
if try_imap_one_param(context, &param, imap).await.is_ok() {
return Ok(());
}
}
progress!(context, 660 + variation * 30);
if manually_set_param.mail_port == 0 {
param.mail_port = 143;
try_imap_one_param(context, param, imap).await
} else {
Err(format_err!("no more possible configs"))
}
}
async fn try_imap_one_param(context: &Context, param: &LoginParam, imap: &mut Imap) -> Result<()> {
let inf = format!(
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
"imap: {}@{}:{} flags=0x{:x} certificate_checks={}",
param.mail_user,
param.mail_server,
param.mail_port,
param.server_flags,
param.imap_certificate_checks
);
info!(context, "Trying: {}", inf);
if let Err(err) = smtp
.connect(context, param, addr, oauth2, provider_strict_tls)
.await
{
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
if imap.connect(context, &param).await {
info!(context, "success: {}", inf);
smtp.disconnect().await;
Ok(())
return Ok(());
}
bail!("Could not connect: {}", inf);
}
#[derive(Debug, thiserror::Error)]
#[error("Trying {config}…\nError: {msg}")]
pub struct ConfigurationError {
config: String,
msg: String,
async fn try_smtp_connections(
context: &Context,
param: &mut LoginParam,
was_autoconfig: bool,
) -> Result<()> {
// manually_set_param is used to check whether a particular setting was set manually by the user.
// If yes, we do not want to change it to avoid confusing error messages
// (you set port 443, but the app tells you it couldn't connect on port 993).
let manually_set_param = LoginParam::from_database(context, "").await;
let mut smtp = Smtp::new();
// try to connect to SMTP - if we did not got an autoconfig, the first try was SSL-465 and we do
// a second try with STARTTLS-587
if try_smtp_one_param(context, param, &mut smtp).await.is_ok() {
return Ok(());
}
if was_autoconfig {
return Ok(());
}
progress!(context, 850);
if manually_set_param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32) == 0 {
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
}
if manually_set_param.send_port == 0 {
param.send_port = 587;
}
if try_smtp_one_param(context, param, &mut smtp).await.is_ok() {
return Ok(());
}
progress!(context, 860);
if manually_set_param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32) == 0 {
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
}
if manually_set_param.send_port == 0 {
param.send_port = 25;
}
try_smtp_one_param(context, param, &mut smtp).await
}
async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationError>) -> String {
let first_err = if let Some(f) = errors.first() {
f
} else {
// This means configuration failed but no errors have been captured. This should never
// happen, but if it does, the user will see classic "Error: no error".
return "no error".to_string();
};
async fn try_smtp_one_param(context: &Context, param: &LoginParam, smtp: &mut Smtp) -> Result<()> {
let inf = format!(
"smtp: {}@{}:{} flags: 0x{:x}",
param.send_user, param.send_server, param.send_port, param.server_flags
);
info!(context, "Trying: {}", inf);
if errors
.iter()
.all(|e| e.msg.to_lowercase().contains("could not resolve"))
{
return stock_str::error_no_network(context).await;
if let Err(err) = smtp.connect(context, &param).await {
bail!("could not connect: {}", err);
}
if errors.iter().all(|e| e.msg == first_err.msg) {
return first_err.msg.to_string();
}
errors.iter().map(|e| e.to_string()).join("\n\n")
info!(context, "success: {}", inf);
smtp.disconnect().await;
Ok(())
}
#[derive(Debug, thiserror::Error)]
@@ -608,14 +601,17 @@ pub enum Error {
#[error("Invalid email address: {0:?}")]
InvalidEmailAddress(String),
#[error("XML error at position {position}: {error}")]
#[error("XML error at position {position}")]
InvalidXml {
position: usize,
#[source]
error: quick_xml::Error,
},
#[error("Failed to get URL: {0}")]
#[error("Bad or incomplete autoconfig")]
IncompleteAutoconfig(LoginParam),
#[error("Failed to get URL")]
ReadUrlError(#[from] self::read_url::Error),
#[error("Number of redirection is exceeded")]
@@ -624,18 +620,37 @@ pub enum Error {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use crate::config::Config;
use crate::test_utils::TestContext;
use super::*;
use crate::config::*;
use crate::test_utils::*;
#[async_std::test]
async fn test_no_panic_on_bad_credentials() {
let t = TestContext::new().await;
t.set_config(Config::Addr, Some("probably@unexistant.addr"))
t.ctx
.set_config(Config::Addr, Some("probably@unexistant.addr"))
.await
.unwrap();
t.set_config(Config::MailPw, Some("123456")).await.unwrap();
assert!(t.configure().await.is_err());
t.ctx
.set_config(Config::MailPw, Some("123456"))
.await
.unwrap();
assert!(t.ctx.configure().await.is_err());
}
#[async_std::test]
async fn test_get_offline_autoconfig() {
let context = TestContext::new().await.ctx;
let mut params = LoginParam::new();
params.addr = "someone123@example.org".to_string();
assert!(get_offline_autoconfig(&context, &params).is_none());
let mut params = LoginParam::new();
params.addr = "someone123@nauta.cu".to_string();
let found_params = get_offline_autoconfig(&context, &params).unwrap();
assert_eq!(found_params.mail_server, "imap.nauta.cu".to_string());
assert_eq!(found_params.send_server, "smtp.nauta.cu".to_string());
}
}

View File

@@ -12,7 +12,7 @@ pub async fn read_url(context: &Context, url: &str) -> Result<String, Error> {
match surf::get(url).recv_string().await {
Ok(res) => Ok(res),
Err(err) => {
info!(context, "Can\'t read URL {}: {}", url, err);
info!(context, "Can\'t read URL {}", url);
Err(Error::GetError(err))
}

View File

@@ -1,164 +0,0 @@
//! Variable server parameters lists
use crate::provider::{Protocol, Socket};
/// Set of variable parameters to try during configuration.
///
/// Can be loaded from offline provider database, online configuraiton
/// or derived from user entered parameters.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ServerParams {
/// Protocol, such as IMAP or SMTP.
pub protocol: Protocol,
/// Server hostname, empty if unknown.
pub hostname: String,
/// Server port, zero if unknown.
pub port: u16,
/// Socket security, such as TLS or STARTTLS, Socket::Automatic if unknown.
pub socket: Socket,
/// Username, empty if unknown.
pub username: String,
}
impl ServerParams {
pub(crate) fn expand_usernames(mut self, addr: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.username.is_empty() {
self.username = addr.to_string();
res.push(self.clone());
if let Some(at) = addr.find('@') {
self.username = addr.split_at(at).0.to_string();
res.push(self);
}
} else {
res.push(self)
}
res
}
pub(crate) fn expand_hostnames(mut self, param_domain: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.hostname.is_empty() {
self.hostname = param_domain.to_string();
res.push(self.clone());
self.hostname = match self.protocol {
Protocol::Imap => "imap.".to_string() + param_domain,
Protocol::Smtp => "smtp.".to_string() + param_domain,
};
res.push(self.clone());
self.hostname = "mail.".to_string() + param_domain;
res.push(self);
} else {
res.push(self);
}
res
}
pub(crate) fn expand_ports(mut self) -> Vec<ServerParams> {
// Try to infer port from socket security.
if self.port == 0 {
self.port = match self.socket {
Socket::Ssl => match self.protocol {
Protocol::Imap => 993,
Protocol::Smtp => 465,
},
Socket::Starttls | Socket::Plain => match self.protocol {
Protocol::Imap => 143,
Protocol::Smtp => 587,
},
Socket::Automatic => 0,
}
}
let mut res = Vec::new();
if self.port == 0 {
// Neither port nor security is set.
//
// Try common secure combinations.
// Try STARTTLS
self.socket = Socket::Starttls;
self.port = match self.protocol {
Protocol::Imap => 143,
Protocol::Smtp => 587,
};
res.push(self.clone());
// Try TLS
self.socket = Socket::Ssl;
self.port = match self.protocol {
Protocol::Imap => 993,
Protocol::Smtp => 465,
};
res.push(self);
} else if self.socket == Socket::Automatic {
// Try TLS over user-provided port.
self.socket = Socket::Ssl;
res.push(self.clone());
// Try STARTTLS over user-provided port.
self.socket = Socket::Starttls;
res.push(self);
} else {
res.push(self);
}
res
}
}
/// Expands vector of `ServerParams`, replacing placeholders with
/// variants to try.
pub(crate) fn expand_param_vector(
v: Vec<ServerParams>,
addr: &str,
domain: &str,
) -> Vec<ServerParams> {
v.into_iter()
// The order of expansion is important: ports are expanded the
// last, so they are changed the first. Username is only
// changed if default value (address with domain) didn't work
// for all available hosts and ports.
.flat_map(|params| params.expand_usernames(addr).into_iter())
.flat_map(|params| params.expand_hostnames(domain).into_iter())
.flat_map(|params| params.expand_ports().into_iter())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_param_vector() {
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 0,
socket: Socket::Ssl,
username: "foobar".to_string(),
}],
"foobar@example.net",
"example.net",
);
assert_eq!(
v,
vec![ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 993,
socket: Socket::Ssl,
username: "foobar".to_string(),
}],
);
}
}

View File

@@ -1,10 +1,19 @@
//! # Constants
use once_cell::sync::Lazy;
#![allow(dead_code)]
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
}
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
// some defaults
const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
const DC_INBOX_WATCH_DEFAULT: i32 = 1;
const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
#[derive(
Debug,
@@ -19,7 +28,7 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
Deserialize,
sqlx::Type,
)]
#[repr(i8)]
#[repr(i32)]
pub enum Blocked {
Not = 0,
Manually = 1,
@@ -32,8 +41,8 @@ impl Default for Blocked {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(i32)]
pub enum ShowEmails {
Off = 0,
AcceptedContacts = 1,
@@ -46,8 +55,8 @@ impl Default for ShowEmails {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(i32)]
pub enum MediaQuality {
Balanced = 0,
Worse = 1,
@@ -59,7 +68,7 @@ impl Default for MediaQuality {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(u8)]
pub enum KeyGenType {
Default = 0,
@@ -73,20 +82,6 @@ impl Default for KeyGenType {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i8)]
pub enum VideochatType {
Unknown = 0,
BasicWebrtc = 1,
Jitsi = 2,
}
impl Default for VideochatType {
fn default() -> Self {
VideochatType::Unknown
}
}
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;
@@ -99,30 +94,27 @@ pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
pub const DC_GCM_INFO_ONLY: u32 = 0x02;
pub const DC_GCL_VERIFIED_ONLY: u32 = 0x01;
pub const DC_GCL_ADD_SELF: u32 = 0x02;
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;
// warn about an outdated app after a given number of days.
// as we use the "provider-db generation date" as reference (that might not be updated very often)
// and as not all system get speedy updates,
// do not use too small value that will annoy users checking for nonexistant updates.
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub const DC_CHAT_ID_DEADDROP: ChatId = ChatId::new(1);
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
pub const DC_CHAT_ID_TRASH: u32 = 3;
/// a message is just in creation but not yet assigned to a chat (eg. we may need the message ID to set up blobs; this avoids unready message to be sent and shown)
const DC_CHAT_ID_MSGS_IN_CREATION: u32 = 4;
/// virtual chat showing all messages flagged with msgs.starred=2
pub const DC_CHAT_ID_STARRED: u32 = 5;
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ARCHIVED_LINK: ChatId = ChatId::new(6);
pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6;
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ALLDONE_HINT: ChatId = ChatId::new(7);
pub const DC_CHAT_ID_ALLDONE_HINT: u32 = 7;
/// larger chat IDs are "real" chats, their messages are "real" messages.
pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
#[derive(
Debug,
@@ -138,12 +130,12 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
Deserialize,
sqlx::Type,
)]
#[repr(u32)]
#[repr(i32)]
pub enum Chattype {
Undefined = 0,
Single = 100,
Group = 120,
Mailinglist = 140,
VerifiedGroup = 130,
}
impl Default for Chattype {
@@ -156,36 +148,10 @@ pub const DC_MSG_ID_MARKER1: u32 = 1;
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// string that indicates sth. is left out or truncated
pub const DC_ELLIPSE: &str = "[...]";
/// to keep bubbles and chat flow usable,
/// and to avoid problems with controls using very long texts,
/// we limit the text length to DC_DESIRED_TEXT_LEN.
/// if the text is longer, the full text can be retrieved usind has_html()/get_html().
///
/// we are using a bit less than DC_MAX_GET_TEXT_LEN to avoid cutting twice
/// (a bit less as truncation may not be exact and ellipses may be added).
///
/// note, that DC_DESIRED_TEXT_LEN and DC_MAX_GET_TEXT_LEN
/// define max. number of bytes, _not_ unicode graphemes.
/// in general, that seems to be okay for such an upper limit,
/// esp. as calculating the number of graphemes is not simple
/// (one graphemes may be a sequence of code points which is a sequence of bytes).
/// also even if we have the exact number of graphemes,
/// that would not always help on getting an idea about the screen space used
/// (to keep bubbles and chat flow usable).
///
/// therefore, the number of bytes is only a very rough estimation,
/// however, the ~30K seems to work okayish for a while,
/// if it turns out, it is too few for some alphabet, we can still increase.
pub const DC_DESIRED_TEXT_LEN: usize = 29_000;
/// approx. max. length (number of bytes) returned by dc_msg_get_text()
pub const DC_MAX_GET_TEXT_LEN: usize = 30_000;
/// 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()
pub const DC_MAX_GET_INFO_LEN: usize = 100_000;
const DC_MAX_GET_INFO_LEN: usize = 100_000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
@@ -216,15 +182,47 @@ pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
/// Connect to IMAP via STARTTLS.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_STARTTLS: i32 = 0x100;
/// Connect to IMAP via SSL.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_SSL: i32 = 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;
/// Connect to SMTP via STARTTLS.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_SMTP_SOCKET_STARTTLS: usize = 0x10000;
/// Connect to SMTP via SSL.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
/// Connect to SMTP unencrypted, this should not be used.
/// If this flag is set, automatic configuration is skipped.
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;
/// if none of these flags are set, the default is chosen
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
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 =
DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN;
/// How many existing messages shall be fetched after configuration.
pub const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
// QR code scanning (view from Bob, the joiner)
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
pub const DC_BOB_ERROR: i32 = 0;
pub const DC_BOB_SUCCESS: i32 = 1;
// max. width/height of an avatar
pub const BALANCED_AVATAR_SIZE: u32 = 256;
pub const WORSE_AVATAR_SIZE: u32 = 128;
pub const AVATAR_SIZE: u32 = 192;
// max. width/height of images
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
@@ -233,11 +231,6 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks.
// this does not affect MIME'e `To:` header.
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
#[derive(
Debug,
Display,
@@ -251,7 +244,7 @@ pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
Deserialize,
sqlx::Type,
)]
#[repr(u32)]
#[repr(i32)]
pub enum Viewtype {
Unknown = 0,
@@ -299,9 +292,6 @@ pub enum Viewtype {
/// The file is set via dc_msg_set_file()
/// and retrieved via dc_msg_get_file().
File = 60,
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
}
impl Default for Viewtype {
@@ -320,6 +310,64 @@ mod tests {
}
}
// These constants are used as events
// reported to the callback given to dc_context_new().
// If you do not want to handle an event, it is always safe to return 0,
// so there is no need to add a "case" for every event.
const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
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;
// TODO: Strings need some doumentation about used placeholders.
// These constants are used to set stock translation strings
const DC_STR_NOMESSAGES: usize = 1;
const DC_STR_SELF: usize = 2;
const DC_STR_DRAFT: usize = 3;
const DC_STR_VOICEMESSAGE: usize = 7;
const DC_STR_DEADDROP: usize = 8;
const DC_STR_IMAGE: usize = 9;
const DC_STR_VIDEO: usize = 10;
const DC_STR_AUDIO: usize = 11;
const DC_STR_FILE: usize = 12;
const DC_STR_STATUSLINE: usize = 13;
const DC_STR_NEWGROUPDRAFT: usize = 14;
const DC_STR_MSGGRPNAME: usize = 15;
const DC_STR_MSGGRPIMGCHANGED: usize = 16;
const DC_STR_MSGADDMEMBER: usize = 17;
const DC_STR_MSGDELMEMBER: usize = 18;
const DC_STR_MSGGROUPLEFT: usize = 19;
const DC_STR_GIF: usize = 23;
const DC_STR_ENCRYPTEDMSG: usize = 24;
const DC_STR_E2E_AVAILABLE: usize = 25;
const DC_STR_ENCR_TRANSP: usize = 27;
const DC_STR_ENCR_NONE: usize = 28;
const DC_STR_CANTDECRYPT_MSG_BODY: usize = 29;
const DC_STR_FINGERPRINTS: usize = 30;
const DC_STR_READRCPT: usize = 31;
const DC_STR_READRCPT_MAILBODY: usize = 32;
const DC_STR_MSGGRPIMGDELETED: usize = 33;
const DC_STR_E2E_PREFERRED: usize = 34;
const DC_STR_CONTACT_VERIFIED: usize = 35;
const DC_STR_CONTACT_NOT_VERIFIED: usize = 36;
const DC_STR_CONTACT_SETUP_CHANGED: usize = 37;
const DC_STR_ARCHIVEDCHATS: usize = 40;
const DC_STR_STARREDMSGS: usize = 41;
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
const DC_STR_CANNOT_LOGIN: usize = 60;
const DC_STR_SERVER_RESPONSE: usize = 61;
const DC_STR_MSGACTIONBYUSER: usize = 62;
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;
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]

File diff suppressed because it is too large Load Diff

View File

@@ -3,30 +3,26 @@
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::ops::Deref;
use std::time::{Instant, SystemTime};
use anyhow::{bail, ensure, Result};
use async_std::prelude::*;
use async_std::{
channel::{self, Receiver, Sender},
path::{Path, PathBuf},
sync::{Arc, Mutex, RwLock},
task,
};
use sqlx::Row;
use async_std::path::{Path, PathBuf};
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
use crate::chat::{get_chat_cnt, ChatId};
use crate::chat::*;
use crate::config::Config;
use crate::constants::DC_VERSION_STR;
use crate::contact::Contact;
use crate::dc_tools::{duration_to_str, time};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::constants::*;
use crate::contact::*;
use crate::dc_tools::duration_to_str;
use crate::error::*;
use crate::events::{Event, EventEmitter, Events};
use crate::job::{self, Action};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId};
use crate::param::Params;
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
use std::time::SystemTime;
#[derive(Clone, Debug)]
pub struct Context {
@@ -49,28 +45,17 @@ pub struct InnerContext {
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
pub(crate) os_name: Option<String>,
pub(crate) bob: Bob,
pub(crate) bob: RwLock<BobStatus>,
pub(crate) last_smeared_timestamp: RwLock<i64>,
pub(crate) running_state: RwLock<RunningState>,
/// Mutex to avoid generating the key for the user more than once.
pub(crate) generating_key_mutex: Mutex<()>,
/// Mutex to enforce only a single running oauth2 is running.
pub(crate) oauth2_mutex: Mutex<()>,
/// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messeges being sent.
pub(crate) wrong_pw_warning_mutex: Mutex<()>,
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
pub(crate) events: Events,
pub(crate) scheduler: RwLock<Scheduler>,
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
/// ID for this `Context` in the current process.
///
/// This allows for multiple `Context`s open in a single process where each context can
/// be identified by this ID.
pub(crate) id: u32,
creation_time: SystemTime,
}
@@ -91,18 +76,19 @@ pub struct RunningState {
pub fn get_info() -> BTreeMap<&'static str, String> {
let mut res = BTreeMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", crate::sql::version().to_string());
let version =
String::from_utf8(libsqlite3_sys::SQLITE_VERSION.to_vec()).expect("invalid version");
res.insert("sqlite_version", version);
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
res.insert("num_cpus", num_cpus::get().to_string());
res.insert("level", "awesome".into());
res
}
impl Context {
/// Creates new context.
pub async fn new(os_name: String, dbfile: PathBuf, id: u32) -> Result<Context> {
// pretty_env_logger::try_init_timed().ok();
pub async fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -110,14 +96,13 @@ impl Context {
if !blobdir.exists().await {
async_std::fs::create_dir_all(&blobdir).await?;
}
Context::with_blobdir(os_name, dbfile, blobdir, id).await
Context::with_blobdir(os_name, dbfile, blobdir).await
}
pub(crate) async fn with_blobdir(
pub async fn with_blobdir(
os_name: String,
dbfile: PathBuf,
blobdir: PathBuf,
id: u32,
) -> Result<Context> {
ensure!(
blobdir.is_dir().await,
@@ -126,28 +111,25 @@ impl Context {
);
let inner = InnerContext {
id,
blobdir,
dbfile,
os_name: Some(os_name),
running_state: RwLock::new(Default::default()),
sql: Sql::new(),
bob: Default::default(),
bob: RwLock::new(Default::default()),
last_smeared_timestamp: RwLock::new(0),
generating_key_mutex: Mutex::new(()),
oauth2_mutex: Mutex::new(()),
wrong_pw_warning_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
events: Events::default(),
scheduler: RwLock::new(Scheduler::Stopped),
ephemeral_task: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
};
let ctx = Context {
inner: Arc::new(inner),
};
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
Ok(ctx)
@@ -156,7 +138,7 @@ impl Context {
/// Starts the IO scheduler.
pub async fn start_io(&self) {
info!(self, "starting IO");
if self.inner.is_io_running().await {
if self.is_io_running().await {
info!(self, "IO is already running");
return;
}
@@ -167,9 +149,18 @@ impl Context {
}
}
/// Returns if the IO scheduler is running.
pub async fn is_io_running(&self) -> bool {
self.inner.is_io_running().await
}
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
info!(self, "stopping IO");
if !self.is_io_running().await {
info!(self, "IO is not running");
return;
}
self.inner.stop_io().await;
}
@@ -193,26 +184,15 @@ impl Context {
}
/// Emits a single event.
pub fn emit_event(&self, event: EventType) {
self.events.emit(Event {
id: self.id,
typ: event,
});
pub fn emit_event(&self, event: Event) {
self.events.emit(event);
}
/// Returns a receiver for emitted events.
///
/// Multiple emitters can be created, but note that in this case each emitted event will
/// only be received by one of the emitters, not by all of them.
/// Get the next queued event.
pub fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
}
/// Get the ID of this context.
pub fn get_id(&self) -> u32 {
self.id
}
// Ongoing process allocation/free/check
pub async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
@@ -225,7 +205,7 @@ impl Context {
s.ongoing_running = true;
s.shall_stop_ongoing = false;
let (sender, receiver) = channel::bounded(1);
let (sender, receiver) = channel(1);
s.cancel_sender = Some(sender);
Ok(receiver)
@@ -252,9 +232,7 @@ impl Context {
let s_a = &self.running_state;
let mut s = s_a.write().await;
if let Some(cancel) = s.cancel_sender.take() {
if let Err(err) = cancel.send(()).await {
warn!(self, "could not cancel ongoing: {:?}", err);
}
cancel.send(()).await;
}
if s.ongoing_running && !s.shall_stop_ongoing {
@@ -273,68 +251,66 @@ impl Context {
* UI chat/message related API
******************************************************************************/
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
let unset = "0";
let l = LoginParam::from_database(self, "").await?;
let l2 = LoginParam::from_database(self, "configured_").await?;
let displayname = self.get_config(Config::Displayname).await?;
let chats = get_chat_cnt(self).await? as usize;
let l = LoginParam::from_database(self, "").await;
let l2 = LoginParam::from_database(self, "configured_").await;
let displayname = self.get_config(Config::Displayname).await;
let chats = get_chat_cnt(self).await as usize;
let real_msgs = message::get_real_msg_cnt(self).await as usize;
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
let contacts = Contact::get_real_cnt(self).await? as usize;
let is_configured = self.get_config_int(Config::Configured).await?;
let contacts = Contact::get_real_cnt(self).await as usize;
let is_configured = self.get_config_int(Config::Configured).await;
let dbversion = self
.sql
.get_raw_config_int("dbversion")
.await?
.await
.unwrap_or_default();
let journal_mode = self
.sql
.query_get_value(sqlx::query("PRAGMA journal_mode;"))
.await?
.unwrap_or_else(|| "unknown".to_string());
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
.query_value("PRAGMA journal_mode;", paramsx![])
.await
.unwrap_or_else(|_| "unknown".to_string());
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await;
let bcc_self = self.get_config_int(Config::BccSelf).await;
let prv_key_cnt = self
let prv_key_cnt: Option<i32> = self
.sql
.count(sqlx::query("SELECT COUNT(*) FROM keypairs;"))
.await?;
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
.await
.ok();
let pub_key_cnt = self
let pub_key_cnt: Option<i32> = self
.sql
.count(sqlx::query("SELECT COUNT(*) FROM acpeerstates;"))
.await?;
.query_value("SELECT COUNT(*) FROM acpeerstates;", paramsx![])
.await
.ok();
let fingerprint_str = match SignedPublicKey::load_self(self).await {
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {}>", err),
};
let inbox_watch = self.get_config_int(Config::InboxWatch).await?;
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await?;
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
let inbox_watch = self.get_config_int(Config::InboxWatch).await;
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await;
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
let folders_configured = self
.sql
.get_raw_config_int("folders_configured")
.await?
.await
.unwrap_or_default();
let configured_sentbox_folder = self
.get_config(Config::ConfiguredSentboxFolder)
.await?
.await
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self
.get_config(Config::ConfiguredMvboxFolder)
.await?
.await
.unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info();
// insert values
res.insert("bot", self.get_config_int(Config::Bot).await?.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());
@@ -347,220 +323,147 @@ impl Context {
res.insert(
"selfavatar",
self.get_config(Config::Selfavatar)
.await?
.await
.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());
res.insert(
"fetch_existing_msgs",
self.get_config_int(Config::FetchExistingMsgs)
.await?
.to_string(),
);
res.insert(
"show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert("inbox_watch", inbox_watch.to_string());
res.insert("sentbox_watch", sentbox_watch.to_string());
res.insert("mvbox_watch", mvbox_watch.to_string());
res.insert("mvbox_move", mvbox_move.to_string());
res.insert("sentbox_move", sentbox_move.to_string());
res.insert("folders_configured", folders_configured.to_string());
res.insert("configured_sentbox_folder", configured_sentbox_folder);
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(
"key_gen_type",
self.get_config_int(Config::KeyGenType).await?.to_string(),
);
res.insert("bcc_self", bcc_self.to_string());
res.insert("private_key_count", prv_key_cnt.to_string());
res.insert("public_key_count", pub_key_cnt.to_string());
res.insert(
"private_key_count",
prv_key_cnt.unwrap_or_default().to_string(),
);
res.insert(
"public_key_count",
pub_key_cnt.unwrap_or_default().to_string(),
);
res.insert("fingerprint", fingerprint_str);
res.insert(
"webrtc_instance",
self.get_config(Config::WebrtcInstance)
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert(
"media_quality",
self.get_config_int(Config::MediaQuality).await?.to_string(),
);
res.insert(
"delete_device_after",
self.get_config_int(Config::DeleteDeviceAfter)
.await?
.to_string(),
);
res.insert(
"delete_server_after",
self.get_config_int(Config::DeleteServerAfter)
.await?
.to_string(),
);
res.insert(
"last_housekeeping",
self.get_config_int(Config::LastHousekeeping)
.await?
.to_string(),
);
res.insert(
"scan_all_folders_debounce_secs",
self.get_config_int(Config::ScanAllFoldersDebounceSecs)
.await?
.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
Ok(res)
res
}
/// Get a list of fresh, unmuted messages in any chat but deaddrop.
///
/// The list starts with the most recent message
/// and is typically used to show notifications.
/// Moreover, the number of returned messages
/// can be used for a badge counter on the app icon.
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
let list = self
.sql
.fetch(
sqlx::query(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>9",
" AND ct.blocked=0",
" AND c.blocked=0",
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
))
.bind(MessageState::InFresh)
.bind(time()),
pub async fn get_fresh_msgs(&self) -> Vec<MsgId> {
let show_deaddrop: i32 = 0;
self.sql
.query_values(
r#"
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;
"#,
paramsx![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
)
.await?
.map(|row| row?.try_get("id"))
.collect::<sqlx::Result<_>>()
.await?;
Ok(list)
.await
.unwrap_or_default()
}
/// Searches for messages containing the query string.
///
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
/// is `None` this searches messages from all chats.
pub async fn search_msgs(
&self,
chat_id: Option<ChatId>,
query: impl AsRef<str>,
) -> Result<Vec<MsgId>> {
#[allow(non_snake_case)]
pub async fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
let real_query = query.as_ref().trim();
if real_query.is_empty() {
return Ok(Vec::new());
return Vec::new();
}
let str_like_in_text = format!("%{}%", real_query);
let str_like_beg = format!("{}%", real_query);
let strLikeInText = format!("%{}%", real_query);
let strLikeBeg = format!("{}%", real_query);
let list = if let Some(chat_id) = chat_id {
self.sql
.fetch(
sqlx::query(
"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;",
)
.bind(chat_id)
.bind(str_like_in_text)
.bind(str_like_beg),
)
.await?
.map(|row| {
let row = row?;
let id = row.try_get::<MsgId, _>("id")?;
Ok(id)
})
.collect::<sqlx::Result<Vec<MsgId>>>()
.await?
let query = if !chat_id.is_unset() {
r#"
SELECT m.id
FROM msgs
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 {
self.sql
.fetch(
sqlx::query(
"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
AND ct.blocked=0
AND (m.txt LIKE ? OR ct.name LIKE ?)
ORDER BY m.timestamp DESC,m.id DESC;",
)
.bind(str_like_in_text)
.bind(str_like_beg),
)
.await?
.map(|row| {
let row = row?;
let id = row.try_get::<MsgId, _>("id")?;
Ok(id)
})
.collect::<sqlx::Result<Vec<MsgId>>>()
.await?
r#"
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.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;
"#
};
Ok(list)
self.sql
.query_values(query, paramsx![chat_id, strLikeInText, strLikeBeg])
.await
.unwrap_or_default()
}
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let inbox = self.get_config(Config::ConfiguredInboxFolder).await?;
Ok(inbox == Some(folder_name.as_ref().to_string()))
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredInboxFolder).await
== Some(folder_name.as_ref().to_string())
}
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
Ok(sentbox == Some(folder_name.as_ref().to_string()))
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredSentboxFolder).await
== Some(folder_name.as_ref().to_string())
}
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
Ok(mvbox == Some(folder_name.as_ref().to_string()))
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredMvboxFolder).await
== Some(folder_name.as_ref().to_string())
}
pub async fn is_spam_folder(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let is_spam = self.get_config(Config::ConfiguredSpamFolder).await?
== Some(folder_name.as_ref().to_string());
pub async fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
if !self.get_config_bool(Config::MvboxMove).await {
return;
}
Ok(is_spam)
}
if self.is_mvbox(folder).await {
return;
}
if let Ok(msg) = Message::load_from_db(self, msg_id).await {
if msg.is_setupmessage() {
// do not move setup messages;
// there may be a non-delta device that wants to handle it
return;
}
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
dbfile.with_file_name(blob_fname)
match msg.is_dc_message {
MessengerMessage::No => {}
MessengerMessage::Yes | MessengerMessage::Reply => {
job::add(
self,
job::Job::new(Action::MoveMsg, msg.id.to_u32(), Params::new(), 0),
)
.await;
}
}
}
}
}
@@ -570,19 +473,14 @@ impl InnerContext {
}
async fn stop_io(&self) {
if self.is_io_running().await {
let token = {
let lock = &*self.scheduler.read().await;
lock.pre_stop().await
};
{
let lock = &mut *self.scheduler.write().await;
lock.stop(token).await;
}
}
if let Some(ephemeral_task) = self.ephemeral_task.write().await.take() {
ephemeral_task.cancel().await;
assert!(self.is_io_running().await, "context is already stopped");
let token = {
let lock = &*self.scheduler.read().await;
lock.pre_stop().await
};
{
let lock = &mut *self.scheduler.write().await;
lock.stop(token).await;
}
}
}
@@ -597,6 +495,13 @@ impl Default for RunningState {
}
}
#[derive(Debug, Default)]
pub(crate) struct BobStatus {
pub expects: i32,
pub status: i32,
pub qr_scan: Option<Lot>,
}
pub fn get_version_str() -> &'static str {
&DC_VERSION_STR
}
@@ -605,170 +510,29 @@ pub fn get_version_str() -> &'static str {
mod tests {
use super::*;
use crate::chat::{get_chat_contacts, get_chat_msgs, set_muted, Chat, MuteDuration};
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
use crate::test_utils::TestContext;
use std::time::Duration;
use strum::IntoEnumIterator;
use crate::test_utils::*;
#[async_std::test]
async fn test_wrong_db() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new("FakeOs".into(), dbfile.into(), 1).await;
let res = Context::new("FakeOs".into(), dbfile.into()).await;
assert!(res.is_err());
}
#[async_std::test]
async fn test_get_fresh_msgs() {
let t = TestContext::new().await;
let fresh = t.get_fresh_msgs().await.unwrap();
let fresh = t.ctx.get_fresh_msgs().await;
assert!(fresh.is_empty())
}
async fn receive_msg(t: &TestContext, chat: &Chat) {
let members = get_chat_contacts(t, chat.id).await.unwrap();
let contact = Contact::load_from_db(t, *members.first().unwrap())
.await
.unwrap();
let msg = format!(
"From: {}\n\
To: alice@example.com\n\
Message-ID: <{}>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
contact.get_addr(),
dc_create_outgoing_rfc724_mid(None, contact.get_addr())
);
println!("{}", msg);
dc_receive_imf(t, msg.as_bytes(), "INBOX", 1, false)
.await
.unwrap();
}
#[async_std::test]
async fn test_get_fresh_msgs_and_muted_chats() {
// receive various mails in 3 chats
let t = TestContext::new_alice().await;
let bob = t.create_chat_with_contact("", "bob@g.it").await;
let claire = t.create_chat_with_contact("", "claire@g.it").await;
let dave = t.create_chat_with_contact("", "dave@g.it").await;
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
receive_msg(&t, &bob).await;
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
receive_msg(&t, &claire).await;
receive_msg(&t, &claire).await;
assert_eq!(
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
2
);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
receive_msg(&t, &dave).await;
receive_msg(&t, &dave).await;
receive_msg(&t, &dave).await;
assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.unwrap().len(), 3);
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
// mute one of the chats
set_muted(&t, claire.id, MuteDuration::Forever)
.await
.unwrap();
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
// receive more messages
receive_msg(&t, &bob).await;
receive_msg(&t, &claire).await;
receive_msg(&t, &dave).await;
assert_eq!(
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
3
);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
// unmute claire again
set_muted(&t, claire.id, MuteDuration::NotMuted)
.await
.unwrap();
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
}
#[async_std::test]
async fn test_get_fresh_msgs_and_muted_until() {
let t = TestContext::new_alice().await;
let bob = t.create_chat_with_contact("", "bob@g.it").await;
receive_msg(&t, &bob).await;
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
// chat is unmuted by default, here and in the following assert(),
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
// have the same view to the database.
assert!(!bob.is_muted());
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
// test get_fresh_msgs() with mute_until in the future
set_muted(
&t,
bob.id,
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
)
.await
.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
assert!(bob.is_muted());
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
// to test get_fresh_msgs() with mute_until in the past,
// we need to modify the database directly
t.sql
.execute(
sqlx::query("UPDATE chats SET muted_until=? WHERE id=?;")
.bind(time() - 3600)
.bind(bob.id),
)
.await
.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
assert!(!bob.is_muted());
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
// test get_fresh_msgs() with "forever" mute_until
set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
assert!(bob.is_muted());
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
// that results in "muted forever" by definition.
t.sql
.execute(sqlx::query("UPDATE chats SET muted_until=-2 WHERE id=?;").bind(bob.id))
.await
.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
assert!(!bob.is_muted());
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
}
#[async_std::test]
async fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new("FakeOS".into(), dbfile.into(), 1)
.await
.unwrap();
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
@@ -779,7 +543,7 @@ mod tests {
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new("FakeOS".into(), dbfile.into(), 1).await;
let res = Context::new("FakeOS".into(), dbfile.into()).await;
assert!(res.is_err());
}
@@ -789,9 +553,7 @@ mod tests {
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new("FakeOS".into(), dbfile.into(), 1)
.await
.unwrap();
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
@@ -801,7 +563,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir, 1).await;
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
assert!(res.is_err());
}
@@ -810,21 +572,21 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
assert!(res.is_err());
}
#[async_std::test]
async fn no_crashes_on_context_deref() {
let t = TestContext::new().await;
std::mem::drop(t);
std::mem::drop(t.ctx);
}
#[async_std::test]
async fn test_get_info() {
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();
let info = t.ctx.get_info().await;
assert!(info.get("database_dir").is_some());
}
@@ -835,48 +597,4 @@ mod tests {
assert!(info.get("database_dir").is_none());
assert_eq!(info.get("level").unwrap(), "awesome");
}
#[async_std::test]
async fn test_get_info_completeness() {
// For easier debugging,
// get_info() shall return all important information configurable by the Config-values.
//
// There are exceptions for Config-values considered to be unimportant,
// too sensitive or summarized in another item.
let skip_from_get_info = vec![
"addr",
"displayname",
"imap_certificate_checks",
"mail_server",
"mail_user",
"mail_pw",
"mail_port",
"mail_security",
"notify_about_wrong_pw",
"save_mime_headers",
"selfstatus",
"send_server",
"send_user",
"send_pw",
"send_port",
"send_security",
"server_flags",
"smtp_certificate_checks",
];
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();
for key in Config::iter() {
let key: String = key.to_string();
if !skip_from_get_info.contains(&&*key)
&& !key.starts_with("configured")
&& !key.starts_with("sys.")
{
assert!(
info.contains_key(&*key),
"'{}' missing in get_info() output",
key
);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,32 +4,29 @@
use core::cmp::{max, min};
use std::borrow::Cow;
use std::fmt;
use std::io::Cursor;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::{bail, Error};
use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng};
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{Viewtype, DC_ELLIPSE, DC_OUTDATED_WARNING_DAYS};
use crate::context::Context;
use crate::events::EventType;
use crate::message::Message;
use crate::provider::get_provider_update_timestamp;
use crate::stock_str;
use crate::error::{bail, Error};
use crate::events::Event;
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
0 != v && 0 == v & (v - 1)
}
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
#[allow(clippy::indexing_slicing)]
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let ellipse = "[...]";
let count = buf.chars().count();
if approx_chars > 0 && count > approx_chars + DC_ELLIPSE.len() {
if approx_chars > 0 && count > approx_chars + ellipse.len() {
let end_pos = buf
.char_indices()
.nth(approx_chars)
@@ -37,15 +34,39 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
.unwrap_or_default();
if let Some(index) = buf[..end_pos].rfind(|c| c == ' ' || c == '\n') {
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSE))
Cow::Owned(format!("{}{}", &buf[..=index], ellipse))
} else {
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSE))
Cow::Owned(format!("{}{}", &buf[..end_pos], ellipse))
}
} else {
Cow::Borrowed(buf)
}
}
/// the colors must fulfill some criterions as:
/// - contrast to black and to white
/// - work as a text-color
/// - being noticeable on a typical map
/// - harmonize together while being different enough
/// (therefore, we cannot just use random rgb colors :)
const COLORS: [u32; 16] = [
0xe5_65_55, 0xf2_8c_48, 0x8e_85_ee, 0x76_c8_4d, 0x5b_b6_cc, 0x54_9c_dd, 0xd2_5c_99, 0xb3_78_00,
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
];
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
let str_lower = s.as_ref().to_lowercase();
let mut checksum = 0;
let bytes = str_lower.as_bytes();
for (i, byte) in bytes.iter().enumerate() {
checksum += (i + 1) * *byte as usize;
checksum %= 0x00ff_ffff;
}
let color_index = checksum % COLORS.len();
COLORS[color_index]
}
/* ******************************************************************************
* date/time tools
******************************************************************************/
@@ -129,67 +150,6 @@ pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize
start
}
// if the system time is not plausible, once a day, add a device message.
// for testing we're using time() as that is also used for message timestamps.
// moreover, add a warning if the app is outdated.
pub(crate) async fn maybe_add_time_based_warnings(context: &Context) {
if !maybe_warn_on_bad_time(context, time(), get_provider_update_timestamp()).await {
maybe_warn_on_outdated(context, time(), get_provider_update_timestamp()).await;
}
}
async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestamp: i64) -> bool {
if now < known_past_timestamp {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(
stock_str::bad_time_msg_body(
context,
Local
.timestamp(now, 0)
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
)
.await,
);
add_device_msg_with_importance(
context,
Some(
format!(
"bad-time-warning-{}",
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m-%d") // repeat every day
)
.as_str(),
),
Some(&mut msg),
true,
)
.await
.ok();
return true;
}
false
}
async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) {
if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::update_reminder_msg_body(context).await);
add_device_msg(
context,
Some(
format!(
"outdated-warning-{}",
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m") // repeat every month
)
.as_str(),
),
Some(&mut msg),
)
.await
.ok();
}
}
/* Message-ID tools */
pub(crate) fn dc_create_id() -> String {
/* generate an id. the generated ID should be as short and as unique as possible:
@@ -238,7 +198,7 @@ fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
pub(crate) fn dc_create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
let hostname = from_addr
.find('@')
.and_then(|k| from_addr.get(k..))
.map(|k| &from_addr[k..])
.unwrap_or("@nohost");
match grpid {
Some(grpid) => format!("Gr.{}.{}{}", grpid, dc_create_id(), hostname),
@@ -280,9 +240,9 @@ pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
/// Returns the `(width, height)` of the given image buffer.
pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
let image = image::io::Reader::new(Cursor::new(buf)).with_guessed_format()?;
let dimensions = image.into_dimensions()?;
Ok(dimensions)
let meta = image_meta::load_from_buf(buf)?;
Ok((meta.dimensions.width, meta.dimensions.height))
}
/// Expand paths relative to $BLOBDIR into absolute paths.
@@ -323,7 +283,7 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
let dpath = format!("{}", path.as_ref().to_string_lossy());
match fs::remove_file(path_abs).await {
Ok(_) => {
context.emit_event(EventType::DeletedBlobFile(dpath));
context.emit_event(Event::DeletedBlobFile(dpath));
true
}
Err(err) => {
@@ -333,23 +293,6 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
}
}
pub async fn dc_delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
match async_std::fs::read_dir(path).await {
Ok(mut read_dir) => {
while let Some(entry) = read_dir.next().await {
match entry {
Ok(file) => {
dc_delete_file(context, file.file_name()).await;
}
Err(e) => warn!(context, "Could not read file to delete: {}", e),
}
}
}
Err(e) => warn!(context, "Could not read dir to delete: {}", e),
}
}
pub(crate) async fn dc_copy_file(
context: &Context,
src_path: impl AsRef<Path>,
@@ -504,28 +447,21 @@ pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
}
}
/// Returns Ok((temp_path, dest_path)) on success. The backup can then be written to temp_path. If the backup succeeded,
/// it can be renamed to dest_path. This guarantees that the backup is complete.
pub(crate) async fn get_next_backup_path(
pub(crate) async fn dc_get_next_backup_path(
folder: impl AsRef<Path>,
backup_time: i64,
) -> Result<(PathBuf, PathBuf), Error> {
) -> Result<PathBuf, Error> {
let folder = PathBuf::from(folder.as_ref());
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
// Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer:
.format("delta-chat-backup-%Y-%m-%d")
.format("delta-chat-%Y-%m-%d")
.to_string();
// 64 backup files per day should be enough for everyone
for i in 0..64 {
let mut tempfile = folder.clone();
tempfile.push(format!("{}-{:02}.tar.part", stem, i));
let mut destfile = folder.clone();
destfile.push(format!("{}-{:02}.tar", stem, i));
if !tempfile.exists().await && !destfile.exists().await {
return Ok((tempfile, destfile));
let mut path = folder.clone();
path.push(format!("{}-{}.bak", stem, i));
if !path.exists().await {
return Ok(path);
}
}
bail!("could not create backup file, disk full?");
@@ -632,55 +568,20 @@ impl FromStr for EmailAddress {
}
}
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
input
.as_ref()
.replace("\n", " ")
.replace("\r", " ")
.trim()
.to_string()
}
pub(crate) trait IsNoneOrEmpty<T> {
fn is_none_or_empty(&self) -> bool;
}
impl<T> IsNoneOrEmpty<T> for Option<T>
where
T: AsRef<str>,
{
fn is_none_or_empty(&self) -> bool {
!matches!(self, Some(s) if !s.as_ref().is_empty())
}
}
pub fn remove_subject_prefix(last_subject: &str) -> String {
let subject_start = if last_subject.starts_with("Chat:") {
0
} else {
// "Antw:" is the longest abbreviation in
// https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages,
// so look at the first _5_ characters:
match last_subject.chars().take(5).position(|c| c == ':') {
Some(prefix_end) => prefix_end + 1,
None => 0,
}
};
last_subject
.chars()
.skip(subject_start)
.collect::<String>()
.trim()
.to_string()
/// Utility to check if a in the binary represantion of listflags
/// the bit at position bitindex is 1.
pub(crate) fn listflags_has(listflags: u32, bitindex: usize) -> bool {
let listflags = listflags as usize;
(listflags & bitindex) == bitindex
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use std::convert::TryInto;
use crate::test_utils::TestContext;
use crate::constants::*;
use crate::test_utils::*;
#[test]
fn test_rust_ftoa() {
@@ -835,9 +736,6 @@ mod tests {
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
}
use crate::chat;
use crate::chatlist::Chatlist;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use proptest::prelude::*;
proptest! {
@@ -869,7 +767,7 @@ mod tests {
#[async_std::test]
async fn test_file_handling() {
let t = TestContext::new().await;
let context = &t;
let context = &t.ctx;
macro_rules! dc_file_exist {
($ctx:expr, $fname:expr) => {
$ctx.get_blobdir()
@@ -931,15 +829,29 @@ mod tests {
assert!(!dc_file_exist!(context, &fn0).await);
}
#[test]
fn test_listflags_has() {
let listflags: u32 = 0x1101;
assert!(listflags_has(listflags, 0x1));
assert!(!listflags_has(listflags, 0x10));
assert!(listflags_has(listflags, 0x100));
assert!(listflags_has(listflags, 0x1000));
let listflags: u32 = (DC_GCL_ADD_SELF | DC_GCL_VERIFIED_ONLY).try_into().unwrap();
assert!(listflags_has(listflags, DC_GCL_VERIFIED_ONLY));
assert!(listflags_has(listflags, DC_GCL_ADD_SELF));
let listflags: u32 = DC_GCL_VERIFIED_ONLY.try_into().unwrap();
assert!(!listflags_has(listflags, DC_GCL_ADD_SELF));
}
#[async_std::test]
async fn test_create_smeared_timestamp() {
let t = TestContext::new().await;
assert_ne!(
dc_create_smeared_timestamp(&t).await,
dc_create_smeared_timestamp(&t).await
dc_create_smeared_timestamp(&t.ctx).await,
dc_create_smeared_timestamp(&t.ctx).await
);
assert!(
dc_create_smeared_timestamp(&t).await
dc_create_smeared_timestamp(&t.ctx).await
>= SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
@@ -951,13 +863,13 @@ mod tests {
async fn test_create_smeared_timestamps() {
let t = TestContext::new().await;
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
let start = dc_create_smeared_timestamps(&t, count as usize).await;
let next = dc_smeared_time(&t).await;
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
let next = dc_smeared_time(&t.ctx).await;
assert!((start + count - 1) < next);
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
let start = dc_create_smeared_timestamps(&t, count as usize).await;
let next = dc_smeared_time(&t).await;
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
let next = dc_smeared_time(&t.ctx).await;
assert!((start + count - 1) < next);
}
@@ -993,170 +905,4 @@ mod tests {
"3h 1m 0s"
);
}
#[test]
fn test_get_filemeta() {
let data = include_bytes!("../test-data/image/avatar900x900.png");
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 900);
assert_eq!(h, 900);
let data = include_bytes!("../test-data/image/avatar1000x1000.jpg");
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 1000);
assert_eq!(h, 1000);
let data = include_bytes!("../test-data/image/image100x50.gif");
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 100);
assert_eq!(h, 50);
}
#[test]
fn test_improve_single_line_input() {
assert_eq!(improve_single_line_input("Hi\naiae "), "Hi aiae");
assert_eq!(improve_single_line_input("\r\nahte\n\r"), "ahte");
}
#[async_std::test]
async fn test_maybe_warn_on_bad_time() {
let t = TestContext::new().await;
let timestamp_now = time();
let timestamp_future = timestamp_now + 60 * 60 * 24 * 7;
let timestamp_past = NaiveDateTime::new(
NaiveDate::from_ymd(2020, 9, 1),
NaiveTime::from_hms(0, 0, 0),
)
.timestamp_millis()
/ 1_000;
// a correct time must not add a device message
maybe_warn_on_bad_time(&t, timestamp_now, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// we cannot find out if a date in the future is wrong - a device message is not added
maybe_warn_on_bad_time(&t, timestamp_future, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// a date in the past must add a device message
maybe_warn_on_bad_time(&t, timestamp_past, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// the message should be added only once a day - test that an hour later and nearly a day later
maybe_warn_on_bad_time(
&t,
timestamp_past + 60 * 60,
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
maybe_warn_on_bad_time(
&t,
timestamp_past + 60 * 60 * 24 - 1,
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// next day, there should be another device message
maybe_warn_on_bad_time(
&t,
timestamp_past + 60 * 60 * 24,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
assert_eq!(device_chat_id, chats.get_chat_id(0));
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 2);
}
#[async_std::test]
async fn test_maybe_warn_on_outdated() {
let t = TestContext::new().await;
let timestamp_now: i64 = time();
// in about 6 months, the app should not be outdated
// (if this fails, provider-db is not updated since 6 months)
maybe_warn_on_outdated(
&t,
timestamp_now + 180 * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// in 1 year, the app should be considered as outdated
maybe_warn_on_outdated(
&t,
timestamp_now + 365 * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// do not repeat the warning every day ...
// (we test that for the 2 subsequent days, this may be the next month, so the result should be 1 or 2 device message)
maybe_warn_on_outdated(
&t,
timestamp_now + (365 + 1) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
maybe_warn_on_outdated(
&t,
timestamp_now + (365 + 2) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
let test_len = msgs.len();
assert!(test_len == 1 || test_len == 2);
// ... but every month
// (forward generous 33 days to avoid being in the same month as in the previous check)
maybe_warn_on_outdated(
&t,
timestamp_now + (365 + 33) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), test_len + 1);
}
}

View File

@@ -2,54 +2,20 @@
//!
//! A module to remove HTML tags from the email text
use std::io::BufRead;
use lazy_static::lazy_static;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use once_cell::sync::Lazy;
use quick_xml::{
events::{BytesEnd, BytesStart, BytesText},
Reader,
};
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
lazy_static! {
static ref LINE_RE: regex::Regex = regex::Regex::new(r"(\r?\n)+").unwrap();
}
struct Dehtml {
strbuilder: String,
add_text: AddText,
last_href: Option<String>,
/// GMX wraps a quote in `<div name="quote">`. After a `<div name="quote">`, this count is
/// increased at each `<div>` and decreased at each `</div>`. This way we know when the quote ends.
/// If this is > `0`, then we are inside a `<div name="quote">`
divs_since_quote_div: u32,
/// Everything between <div name="quote"> and <div name="quoted-content"> is usually metadata
/// If this is > `0`, then we are inside a `<div name="quoted-content">`.
divs_since_quoted_content_div: u32,
/// All-Inkl just puts the quote into `<blockquote> </blockquote>`. This count is
/// increased at each `<blockquote>` and decreased at each `</blockquote>`.
blockquotes_since_blockquote: u32,
}
impl Dehtml {
fn line_prefix(&self) -> &str {
if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 {
"> "
} else {
""
}
}
fn append_prefix(&self, line_end: impl AsRef<str>) -> String {
// line_end is e.g. "\n\n". We add "> " if necessary.
line_end.as_ref().to_owned() + self.line_prefix()
}
fn get_add_text(&self) -> AddText {
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
AddText::No // Everything between <div name="quoted"> and <div name="quoted_content"> is metadata which we don't want
} else {
self.add_text
}
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
#[derive(Debug, PartialEq)]
enum AddText {
No,
YesRemoveLineEnds,
@@ -58,28 +24,13 @@ enum AddText {
// 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) -> Option<String> {
let s = dehtml_quick_xml(buf);
if !s.trim().is_empty() {
return Some(s);
}
let s = dehtml_manually(buf);
if !s.trim().is_empty() {
return Some(s);
}
None
}
fn dehtml_quick_xml(buf: &str) -> String {
let buf = buf.trim().trim_start_matches("<!doctype html>");
pub fn dehtml(buf: &str) -> String {
let buf = buf.trim();
let mut dehtml = Dehtml {
strbuilder: String::with_capacity(buf.len()),
add_text: AddText::YesRemoveLineEnds,
last_href: None,
divs_since_quote_div: 0,
divs_since_quoted_content_div: 0,
blockquotes_since_blockquote: 0,
};
let mut reader = quick_xml::Reader::from_str(buf);
@@ -95,12 +46,6 @@ fn dehtml_quick_xml(buf: &str) -> String {
Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::CData(ref e)) => dehtml_cdata_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::Empty(ref e)) => {
// Handle empty tags as a start tag immediately followed by end tag.
// For example, `<p/>` is treated as `<p></p>`.
dehtml_starttag_cb(e, &mut dehtml, &reader);
dehtml_endtag_cb(&BytesEnd::borrowed(e.name()), &mut dehtml);
}
Err(e) => {
eprintln!(
"Parse html error: Error at position {}: {:?}",
@@ -118,16 +63,13 @@ fn dehtml_quick_xml(buf: &str) -> String {
}
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
if dehtml.add_text == AddText::YesPreserveLineEnds
|| dehtml.add_text == AddText::YesRemoveLineEnds
{
let last_added = escaper::decode_html_buf_sloppy(event.escaped()).unwrap_or_default();
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
if dehtml.add_text == AddText::YesRemoveLineEnds {
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
} else if !dehtml.line_prefix().is_empty() {
let l = dehtml.append_prefix("\n");
dehtml.strbuilder += LINE_RE.replace_all(&last_added, l.as_str()).as_ref();
} else {
dehtml.strbuilder += &last_added;
}
@@ -135,16 +77,13 @@ fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
}
fn dehtml_cdata_cb(event: &BytesText, dehtml: &mut Dehtml) {
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
if dehtml.add_text == AddText::YesPreserveLineEnds
|| dehtml.add_text == AddText::YesRemoveLineEnds
{
let last_added = escaper::decode_html_buf_sloppy(event.escaped()).unwrap_or_default();
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
if dehtml.add_text == AddText::YesRemoveLineEnds {
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
} else if !dehtml.line_prefix().is_empty() {
let l = dehtml.append_prefix("\n");
dehtml.strbuilder += LINE_RE.replace_all(&last_added, l.as_str()).as_ref();
} else {
dehtml.strbuilder += &last_added;
}
@@ -155,15 +94,8 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
match tag.as_str() {
"p" | "table" | "td" | "style" | "script" | "title" | "pre" => {
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"div" => {
pop_tag(&mut dehtml.divs_since_quote_div);
pop_tag(&mut dehtml.divs_since_quoted_content_div);
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
"p" | "div" | "table" | "td" | "style" | "script" | "title" | "pre" => {
dehtml.strbuilder += "\n\n";
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"a" => {
@@ -174,16 +106,11 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
}
}
"b" | "strong" => {
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "*";
}
dehtml.strbuilder += "*";
}
"i" | "em" => {
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "_";
}
dehtml.strbuilder += "_";
}
"blockquote" => pop_tag(&mut dehtml.blockquotes_since_blockquote),
_ => {}
}
}
@@ -196,27 +123,19 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
match tag.as_str() {
"p" | "table" | "td" => {
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
#[rustfmt::skip]
"div" => {
maybe_push_tag(event, reader, "quote", &mut dehtml.divs_since_quote_div);
maybe_push_tag(event, reader, "quoted-content", &mut dehtml.divs_since_quoted_content_div);
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
"p" | "div" | "table" | "td" => {
dehtml.strbuilder += "\n\n";
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"br" => {
dehtml.strbuilder += &dehtml.append_prefix("\n");
dehtml.strbuilder += "\n";
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"style" | "script" | "title" => {
dehtml.add_text = AddText::No;
}
"pre" => {
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.strbuilder += "\n\n";
dehtml.add_text = AddText::YesPreserveLineEnds;
}
"a" => {
@@ -237,74 +156,18 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
}
}
"b" | "strong" => {
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "*";
}
dehtml.strbuilder += "*";
}
"i" | "em" => {
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "_";
}
dehtml.strbuilder += "_";
}
"blockquote" => dehtml.blockquotes_since_blockquote += 1,
_ => {}
}
}
/// In order to know when a specific tag is closed, we need to count the opening and closing tags.
/// The `counts`s are stored in the `Dehtml` struct.
fn pop_tag(count: &mut u32) {
if *count > 0 {
*count -= 1;
}
}
/// In order to know when a specific tag is closed, we need to count the opening and closing tags.
/// The `counts`s are stored in the `Dehtml` struct.
fn maybe_push_tag(
event: &BytesStart,
reader: &Reader<impl BufRead>,
tag_name: &str,
count: &mut u32,
) {
if *count > 0 || tag_contains_attr(event, reader, tag_name) {
*count += 1;
}
}
fn tag_contains_attr(event: &BytesStart, reader: &Reader<impl BufRead>, name: &str) -> bool {
event.attributes().any(|r| {
r.map(|a| {
a.unescape_and_decode_value(reader)
.map(|v| v == name)
.unwrap_or(false)
})
.unwrap_or(false)
})
}
pub fn dehtml_manually(buf: &str) -> String {
// Just strip out everything between "<" and ">"
let mut strbuilder = String::new();
let mut show_next_chars = true;
for c in buf.chars() {
match c {
'<' => show_next_chars = false,
'>' => show_next_chars = true,
_ => {
if show_next_chars {
strbuilder.push(c)
}
}
}
}
strbuilder
}
#[cfg(test)]
mod tests {
use super::*;
use crate::simplify::simplify;
#[test]
fn test_dehtml() {
@@ -313,40 +176,31 @@ mod tests {
"<a href='https://example.com'> Foo </a>",
"[ Foo ](https://example.com)",
),
("<img href='/foo.png'>", ""),
("<b> bar </b>", "* bar *"),
("<b> bar <i> foo", "* bar _ foo"),
("&amp; bar", "& bar"),
// Despite missing ', this should be shown:
("<a href='/foo.png>Hi</a> ", "Hi "),
(
"<a href='https://get.delta.chat/'/>",
"[](https://get.delta.chat/)",
),
("<!doctype html>\n<b>fat text</b>", "*fat text*"),
// Invalid html (at least DC should show the text if the html is invalid):
("<!some invalid html code>\n<b>some text</b>", "some text"),
// Note missing '
("<a href='/foo.png>Hi</a> ", ""),
("", ""),
];
for (input, output) in cases {
assert_eq!(simplify(dehtml(input).unwrap(), true).0, output);
}
let none_cases = vec!["<html> </html>", ""];
for input in none_cases {
assert_eq!(dehtml(input), None);
assert_eq!(dehtml(input), output);
}
}
#[test]
fn test_dehtml_parse_br() {
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
let plain = dehtml(html).unwrap();
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\nline3");
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).unwrap();
let plain = dehtml(html);
assert_eq!(plain, "[text](url)");
}
@@ -354,7 +208,7 @@ mod tests {
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = dehtml(html).unwrap();
let plain = dehtml(html);
assert_eq!(plain, "text *bold*<>");
}
@@ -364,7 +218,7 @@ mod tests {
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).unwrap();
let plain = dehtml(html);
assert_eq!(
plain,
@@ -387,20 +241,7 @@ mod tests {
</body>
</html>
"##;
let txt = dehtml(input).unwrap();
let txt = dehtml(input);
assert_eq!(txt.trim(), "lots of text");
}
#[async_std::test]
async fn test_quote_div() {
let input = include_str!("../test-data/message/gmx-quote-body.eml");
let dehtml = dehtml(input).unwrap();
println!("{}", dehtml);
let (msg, forwarded, cut, top_quote, footer) = simplify(dehtml, false);
assert_eq!(msg, "Test");
assert_eq!(forwarded, false);
assert_eq!(cut, false);
assert_eq!(top_quote.as_deref(), Some("test"));
assert_eq!(footer, None);
}
}

View File

@@ -2,19 +2,20 @@
use std::collections::HashSet;
use anyhow::{bail, ensure, format_err, Result};
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
use crate::aheader::*;
use crate::config::Config;
use crate::context::Context;
use crate::error::*;
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::keyring::*;
use crate::peerstate::*;
use crate::pgp;
use crate::securejoin::handle_degrade_event;
#[derive(Debug)]
pub struct EncryptHelper {
@@ -26,9 +27,9 @@ pub struct EncryptHelper {
impl EncryptHelper {
pub async fn new(context: &Context) -> Result<EncryptHelper> {
let prefer_encrypt =
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await)
.unwrap_or_default();
let addr = match context.get_config(Config::ConfiguredAddr).await? {
let addr = match context.get_config(Config::ConfiguredAddr).await {
None => {
bail!("addr not configured!");
}
@@ -51,42 +52,23 @@ impl EncryptHelper {
}
/// Determines if we can and should encrypt.
///
/// For encryption to be enabled, `e2ee_guaranteed` should be true, or strictly more than a half
/// of peerstates should prefer encryption. Own preference is counted equally to peer
/// preferences, even if message copy is not sent to self.
///
/// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
/// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
///
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
pub fn should_encrypt(
&self,
context: &Context,
e2ee_guaranteed: bool,
peerstates: &[(Option<Peerstate>, &str)],
) -> Result<bool> {
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
1
} else {
0
};
if !(self.prefer_encrypt == EncryptPreference::Mutual || e2ee_guaranteed) {
return Ok(false);
}
for (peerstate, addr) in peerstates {
match peerstate {
Some(peerstate) => {
info!(
context,
"peerstate for {:?} is {}", addr, peerstate.prefer_encrypt
);
match peerstate.prefer_encrypt {
EncryptPreference::NoPreference => {}
EncryptPreference::Mutual => prefer_encrypt_count += 1,
EncryptPreference::Reset => {
if !e2ee_guaranteed {
return Ok(false);
}
}
};
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !e2ee_guaranteed {
info!(context, "peerstate for {:?} is no-encrypt", addr);
return Ok(false);
}
}
None => {
let msg = format!("peerstate for {:?} missing, cannot encrypt", addr);
@@ -100,11 +82,7 @@ impl EncryptHelper {
}
}
// Count number of recipients, including self.
// This does not depend on whether we send a copy to self or not.
let recipients_count = peerstates.len() + 1;
Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
Ok(true)
}
/// Tries to encrypt the passed in `mail`.
@@ -137,14 +115,6 @@ impl EncryptHelper {
}
}
/// Tries to decrypt a message, but only if it is structured as an
/// Autocrypt message.
///
/// Returns decrypted body and a set of valid signature fingerprints
/// if successful.
///
/// If the message is wrongly signed, this will still return the decrypted
/// message but the HashSet will be empty.
pub async fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
@@ -153,36 +123,49 @@ pub async fn try_decrypt(
let from = mail
.headers
.get_header(HeaderDef::From_)
.and_then(|from_addr| mailparse::addrparse_header(from_addr).ok())
.and_then(|from_addr| mailparse::addrparse_header(&from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
.unwrap_or_default();
let mut peerstate = Peerstate::from_addr(context, &from).await?;
let mut peerstate = None;
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
if message_time > 0 {
peerstate = Peerstate::from_addr(context, &from).await.ok();
// Apply Autocrypt header
if let Some(ref header) = Aheader::from_headers(context, &from, &mail.headers) {
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
if let Some(ref header) = autocryptheader {
peerstate.apply_header(&header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) {
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
} else if let Some(ref header) = autocryptheader {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
}
// Possibly perform decryption
/* possibly perform decryption */
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
let mut signatures = HashSet::default();
if let Some(ref mut peerstate) = peerstate {
peerstate.handle_fingerprint_change(context).await?;
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
peerstate = Peerstate::from_addr(&context, &from).await.ok();
}
if let Some(peerstate) = peerstate {
if peerstate.degrade_event.is_some() {
handle_degrade_event(context, &peerstate).await?;
}
if let Some(key) = peerstate.gossip_key {
public_keyring_for_validate.add(key);
}
if let Some(key) = peerstate.public_key {
public_keyring_for_validate.add(key);
}
}
@@ -194,89 +177,56 @@ pub async fn try_decrypt(
&mut signatures,
)
.await?;
if let Some(mut peerstate) = peerstate {
// If message is not encrypted and it is not a read receipt, degrade encryption.
if out_mail.is_none()
&& message_time > peerstate.last_seen_autocrypt
&& !contains_report(mail)
{
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
}
Ok((out_mail, signatures))
}
/// Returns a reference to the encrypted payload of a valid PGP/MIME message.
///
/// Returns `None` if the message is not a valid PGP/MIME message.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/encrypted" {
return None;
}
if let [first_part, second_part] = &mail.subparts[..] {
if first_part.ctype.mimetype == "application/pgp-encrypted"
&& second_part.ctype.mimetype == "application/octet-stream"
{
Some(second_part)
} else {
None
}
} else {
None
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
ensure!(
mail.ctype.mimetype == "multipart/encrypted",
"Not a multipart/encrypted message: {}",
mail.ctype.mimetype
);
ensure!(
mail.subparts.len() == 2,
"Invalid Autocrypt Level 1 Mime Parts"
);
ensure!(
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
mail.subparts[0].ctype,
);
ensure!(
mail.subparts[1].ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
mail.subparts[1].ctype
);
Ok(&mail.subparts[1])
}
/// Returns a reference to the encrypted payload of a ["Mixed
/// Up"][pgpmime-message-mangling] message.
///
/// According to [RFC 3156] encrypted messages should have
/// `multipart/encrypted` MIME type and two parts, but Microsoft
/// Exchange and ProtonMail IMAP/SMTP Bridge are known to mangle this
/// structure by changing the type to `multipart/mixed` and prepending
/// an empty part at the start.
///
/// ProtonMail IMAP/SMTP Bridge prepends a part literally saying
/// "Empty Message", so we don't check its contents at all, checking
/// only for `text/plain` type.
///
/// Returns `None` if the message is not a "Mixed Up" message.
///
/// [RFC 3156]: https://www.rfc-editor.org/info/rfc3156
/// [pgpmime-message-mangling]: https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html
fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/mixed" {
return None;
}
if let [first_part, second_part, third_part] = &mail.subparts[..] {
if first_part.ctype.mimetype == "text/plain"
&& second_part.ctype.mimetype == "application/pgp-encrypted"
&& third_part.ctype.mimetype == "application/octet-stream"
{
Some(third_part)
} else {
None
}
} else {
None
}
}
async fn decrypt_if_autocrypt_message(
async fn decrypt_if_autocrypt_message<'a>(
context: &Context,
mail: &ParsedMail<'_>,
mail: &ParsedMail<'a>,
private_keyring: Keyring<SignedSecretKey>,
public_keyring_for_validate: Keyring<SignedPublicKey>,
ret_valid_signatures: &mut HashSet<Fingerprint>,
) -> Result<Option<Vec<u8>>> {
let encrypted_data_part = match get_autocrypt_mime(mail).or_else(|| get_mixed_up_mime(mail)) {
None => {
// The returned bool is true if we detected an Autocrypt-encrypted
// message and successfully decrypted it. Decryption then modifies the
// passed in mime structure in place. The returned bool is false
// if it was not an Autocrypt message.
//
// Errors are returned for failures related to decryption of AC-messages.
let encrypted_data_part = match get_autocrypt_mime(mail) {
Err(_) => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
}
Some(res) => res,
Ok(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
@@ -310,16 +260,13 @@ async fn decrypt_part(
)
.await?;
// If the message was wrongly or not signed, still return the plain text.
// The caller has to check the signatures then.
ensure!(!ret_valid_signatures.is_empty(), "no valid signatures");
return Ok(Some(plain));
}
Ok(None)
}
#[allow(clippy::indexing_slicing)]
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
if let Some(index) = input.iter().position(|b| *b > b' ') {
if input.len() - index > 26 {
@@ -358,7 +305,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.await
.ok_or_else(|| {
format_err!(concat!(
"Failed to get self address, ",
@@ -373,12 +320,7 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
mod tests {
use super::*;
use crate::chat;
use crate::constants::Viewtype;
use crate::message::Message;
use crate::param::Param;
use crate::peerstate::ToSave;
use crate::test_utils::{bob_keypair, TestContext};
use crate::test_utils::*;
mod ensure_secret_key_exists {
use super::*;
@@ -387,13 +329,13 @@ mod tests {
async fn test_prexisting() {
let t = TestContext::new().await;
let test_addr = t.configure_alice().await;
assert_eq!(ensure_secret_key_exists(&t).await.unwrap(), test_addr);
assert_eq!(ensure_secret_key_exists(&t.ctx).await.unwrap(), test_addr);
}
#[async_std::test]
async fn test_not_configured() {
let t = TestContext::new().await;
assert!(ensure_secret_key_exists(&t).await.is_err());
assert!(ensure_secret_key_exists(&t.ctx).await.is_err());
}
}
@@ -438,162 +380,4 @@ Sent with my Delta Chat Messenger: https://delta.chat";
let data = b"blas";
assert_eq!(has_decrypted_pgp_armor(data), false);
}
#[async_std::test]
async fn test_encrypted_no_autocrypt() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(&bob).await.id;
let chat_bob = bob.create_chat(&alice).await.id;
// Alice sends unencrypted message to Bob
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
// Bob receives unencrypted message from Alice
let msg = bob.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
// Parsing a message is enough to update peerstate
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
// Bob sends encrypted message to Alice
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&bob.ctx, chat_bob, &mut msg).await?;
chat::send_msg(&bob.ctx, chat_bob, &mut msg).await?;
let sent = bob.pop_sent_msg().await;
// Alice receives encrypted message from Bob
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
let peerstate_bob = Peerstate::from_addr(&alice.ctx, "bob@example.net")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_bob.prefer_encrypt, EncryptPreference::Mutual);
// Now Alice and Bob have established keys.
// Alice sends encrypted message without Autocrypt header.
let mut msg = Message::new(Viewtype::Text);
msg.param.set_int(Param::SkipAutocrypt, 1);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
// Alice sends plaintext message with Autocrypt header.
let mut msg = Message::new(Viewtype::Text);
msg.param.set_int(Param::ForcePlaintext, 1);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
// Alice sends plaintext message without Autocrypt header.
let mut msg = Message::new(Viewtype::Text);
msg.param.set_int(Param::ForcePlaintext, 1);
msg.param.set_int(Param::SkipAutocrypt, 1);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset);
Ok(())
}
fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option<Peerstate>, &'static str)> {
let addr = "bob@foo.bar";
let pub_key = bob_keypair().public;
let peerstate = Peerstate {
addr: addr.into(),
last_seen: 13,
last_seen_autocrypt: 14,
prefer_encrypt,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: Some(pub_key.clone()),
gossip_timestamp: 15,
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
vec![(Some(peerstate), addr)]
}
#[async_std::test]
async fn test_should_encrypt() {
let t = TestContext::new_alice().await;
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
// test with EncryptPreference::NoPreference:
// if e2ee_eguaranteed is unset, there is no encryption as not more than half of peers want encryption
let ps = new_peerstates(EncryptPreference::NoPreference);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with EncryptPreference::Reset
let ps = new_peerstates(EncryptPreference::Reset);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with EncryptPreference::Mutual (self is also Mutual)
let ps = new_peerstates(EncryptPreference::Mutual);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with missing peerstate
let ps = vec![(None, "bob@foo.bar")];
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
}
#[test]
fn test_mixed_up_mime() -> Result<()> {
// "Mixed Up" mail as received when sending an encrypted
// message using Delta Chat Desktop via ProtonMail IMAP/SMTP
// Bridge.
let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml");
let mail = mailparse::parse_mail(mixed_up_mime)?;
assert!(get_autocrypt_mime(&mail).is_none());
assert!(get_mixed_up_mime(&mail).is_some());
// Same "Mixed Up" mail repaired by Thunderbird 78.9.0.
//
// It added `X-Enigmail-Info: Fixed broken PGP/MIME message`
// header although the repairing is done by the built-in
// OpenPGP support, not Enigmail.
let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml");
let mail = mailparse::parse_mail(repaired_mime)?;
assert!(get_autocrypt_mime(&mail).is_some());
assert!(get_mixed_up_mime(&mail).is_none());
Ok(())
}
}

View File

@@ -1,787 +0,0 @@
//! # Ephemeral messages
//!
//! Ephemeral messages are messages that have an Ephemeral-Timer
//! header attached to them, which specifies time in seconds after
//! which the message should be deleted both from the device and from
//! the server. The timer is started when the message is marked as
//! seen, which usually happens when its contents is displayed on
//! device screen.
//!
//! Each chat, including 1:1, group chats and "saved messages" chat,
//! has its own ephemeral timer setting, which is applied to all
//! messages sent to the chat. The setting is synchronized to all the
//! devices participating in the chat by applying the timer value from
//! all received messages, including BCC-self ones, to the chat. This
//! way the setting is eventually synchronized among all participants.
//!
//! When user changes ephemeral timer setting for the chat, a system
//! message is automatically sent to update the setting for all
//! participants. This allows changing the setting for a chat like any
//! group chat setting, e.g. name and avatar, without the need to
//! write an actual message.
//!
//! ## Device settings
//!
//! In addition to per-chat ephemeral message setting, each device has
//! two global user-configured settings that complement per-chat
//! settings: `delete_device_after` and `delete_server_after`. These
//! settings are not synchronized among devices and apply to all
//! messages known to the device, including messages sent or received
//! before configuring the setting.
//!
//! `delete_device_after` configures the maximum time device is
//! storing the messages locally. `delete_server_after` configures the
//! time after which device will delete the messages it knows about
//! from the server.
//!
//! ## How messages are deleted
//!
//! When the message is deleted locally, its contents is removed and
//! it is moved to the trash chat. This database entry is then used to
//! track the Message-ID and corresponding IMAP folder and UID until
//! the message is deleted from the server. Vice versa, when device
//! deletes the message from the server, it removes IMAP folder and
//! UID information, but keeps the message contents. When database
//! entry is both moved to trash chat and does not contain UID
//! information, it is deleted from the database, leaving no trace of
//! the message.
//!
//! ## When messages are deleted
//!
//! Local deletion happens when the chatlist or chat is loaded. A
//! `MsgsChanged` event is emitted when a message deletion is due, to
//! make UI reload displayed messages and cause actual deletion.
//!
//! Server deletion happens by generating IMAP deletion jobs based on
//! the database entries which are expired either according to their
//! ephemeral message timers or global `delete_server_after` setting.
use std::convert::{TryFrom, TryInto};
use std::num::ParseIntError;
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{ensure, Context as _, Error};
use async_std::task;
use serde::{Deserialize, Serialize};
use sqlx::Row;
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
use crate::constants::{
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
};
use crate::context::Context;
use crate::dc_tools::time;
use crate::events::EventType;
use crate::message::{Message, MessageState, MsgId};
use crate::mimeparser::SystemMessage;
use crate::sql;
use crate::stock_str;
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
pub enum Timer {
Disabled,
Enabled { duration: u32 },
}
impl Timer {
pub fn to_u32(self) -> u32 {
match self {
Self::Disabled => 0,
Self::Enabled { duration } => duration,
}
}
pub fn from_u32(duration: u32) -> Self {
if duration == 0 {
Self::Disabled
} else {
Self::Enabled { duration }
}
}
}
impl Default for Timer {
fn default() -> Self {
Self::Disabled
}
}
impl ToString for Timer {
fn to_string(&self) -> String {
self.to_u32().to_string()
}
}
impl FromStr for Timer {
type Err = ParseIntError;
fn from_str(input: &str) -> Result<Timer, ParseIntError> {
input.parse::<u32>().map(Self::from_u32)
}
}
impl sqlx::Type<sqlx::Sqlite> for Timer {
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
<i64 as sqlx::Type<_>>::type_info()
}
fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool {
<i64 as sqlx::Type<_>>::compatible(ty)
}
}
impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for Timer {
fn encode_by_ref(
&self,
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
) -> sqlx::encode::IsNull {
args.push(sqlx::sqlite::SqliteArgumentValue::Int64(
self.to_u32() as i64
));
sqlx::encode::IsNull::No
}
}
impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for Timer {
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
let value: i64 = sqlx::Decode::decode(value)?;
if value == 0 {
Ok(Self::Disabled)
} else if let Ok(duration) = u32::try_from(value) {
Ok(Self::Enabled { duration })
} else {
Err(Box::new(sqlx::Error::Decode(Box::new(
crate::error::OutOfRangeError,
))))
}
}
}
impl ChatId {
/// Get ephemeral message timer value in seconds.
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
let timer = context
.sql
.query_get_value(
sqlx::query("SELECT ephemeral_timer FROM chats WHERE id=?;").bind(self),
)
.await?;
Ok(timer.unwrap_or_default())
}
/// Set ephemeral timer value without sending a message.
///
/// Used when a message arrives indicating that someone else has
/// changed the timer value for a chat.
pub(crate) async fn inner_set_ephemeral_timer(
self,
context: &Context,
timer: Timer,
) -> Result<(), Error> {
ensure!(!self.is_special(), "Invalid chat ID");
context
.sql
.execute(
sqlx::query(
"UPDATE chats
SET ephemeral_timer=?
WHERE id=?;",
)
.bind(timer)
.bind(self),
)
.await?;
context.emit_event(EventType::ChatEphemeralTimerModified {
chat_id: self,
timer,
});
Ok(())
}
/// Set ephemeral message timer value in seconds.
///
/// If timer value is 0, disable ephemeral message timer.
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<(), Error> {
if timer == self.get_ephemeral_timer(context).await? {
return Ok(());
}
self.inner_set_ephemeral_timer(context, timer).await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_ephemeral_timer_changed(context, timer, DC_CONTACT_ID_SELF).await);
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
if let Err(err) = send_msg(context, self, &mut msg).await {
error!(
context,
"Failed to send a message about ephemeral message timer change: {:?}", err
);
}
Ok(())
}
}
/// Returns a stock message saying that ephemeral timer is changed to `timer` by `from_id`.
pub(crate) async fn stock_ephemeral_timer_changed(
context: &Context,
timer: Timer,
from_id: u32,
) -> String {
match timer {
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id as u32).await,
Timer::Enabled { duration } => match duration {
0..=59 => {
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id as u32)
.await
}
60 => stock_str::msg_ephemeral_timer_minute(context, from_id as u32).await,
61..=3599 => {
stock_str::msg_ephemeral_timer_minutes(
context,
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
from_id as u32,
)
.await
}
3600 => stock_str::msg_ephemeral_timer_hour(context, from_id as u32).await,
3601..=86399 => {
stock_str::msg_ephemeral_timer_hours(
context,
format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
from_id as u32,
)
.await
}
86400 => stock_str::msg_ephemeral_timer_day(context, from_id as u32).await,
86401..=604_799 => {
stock_str::msg_ephemeral_timer_days(
context,
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
from_id as u32,
)
.await
}
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id as u32).await,
_ => {
stock_str::msg_ephemeral_timer_weeks(
context,
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
from_id as u32,
)
.await
}
},
}
}
impl MsgId {
/// Returns ephemeral message timer value for the message.
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
let res = match context
.sql
.query_get_value::<_, i64>(
sqlx::query("SELECT ephemeral_timer FROM msgs WHERE id=?").bind(self),
)
.await?
{
None | Some(0) => Timer::Disabled,
Some(duration) => Timer::Enabled {
duration: u32::try_from(duration)?,
},
};
Ok(res)
}
/// Starts ephemeral message timer for the message if it is not started yet.
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
let ephemeral_timestamp = time() + i64::from(duration);
context
.sql
.execute(
sqlx::query(
"UPDATE msgs SET ephemeral_timestamp = ? \
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
AND id = ?",
)
.bind(ephemeral_timestamp)
.bind(ephemeral_timestamp)
.bind(self),
)
.await?;
schedule_ephemeral_task(context).await;
}
Ok(())
}
}
/// Deletes messages which are expired according to
/// `delete_device_after` setting or `ephemeral_timestamp` column.
///
/// Returns true if any message is deleted, so caller can emit
/// MsgsChanged event. If nothing has been deleted, returns
/// false. This function does not emit the MsgsChanged event itself,
/// because it is also called when chatlist is reloaded, and emitting
/// MsgsChanged there will cause infinite reload loop.
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, Error> {
let mut updated = context
.sql
.execute(
sqlx::query(
// If you change which information is removed here, also change MsgId::trash() and
// which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH
r#"
UPDATE msgs
SET
chat_id=?, txt='', subject='', txt_raw='',
mime_headers='', from_id=0, to_id=0, param=''
WHERE
ephemeral_timestamp != 0
AND ephemeral_timestamp <= ?
AND chat_id != ?
"#,
)
.bind(DC_CHAT_ID_TRASH)
.bind(time())
.bind(DC_CHAT_ID_TRASH),
)
.await
.context("update failed")?
> 0;
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.await
.unwrap_or_default()
.0;
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.await
.unwrap_or_default()
.0;
let threshold_timestamp = time() - delete_device_after;
// Delete expired messages
//
// Only update the rows that have to be updated, to avoid emitting
// unnecessary "chat modified" events.
let rows_modified = context
.sql
.execute(
sqlx::query(
"UPDATE msgs \
SET txt = 'DELETED', chat_id = ? \
WHERE timestamp < ? \
AND chat_id > ? \
AND chat_id != ? \
AND chat_id != ?",
)
.bind(DC_CHAT_ID_TRASH)
.bind(threshold_timestamp)
.bind(DC_CHAT_ID_LAST_SPECIAL)
.bind(self_chat_id)
.bind(device_chat_id),
)
.await
.context("deleted update failed")?;
updated |= rows_modified > 0;
}
schedule_ephemeral_task(context).await;
Ok(updated)
}
/// Schedule a task to emit MsgsChanged event when the next local
/// deletion happens. Existing task is cancelled to make sure at most
/// one such task is scheduled at a time.
///
/// UI is expected to reload the chatlist or the chat in response to
/// MsgsChanged event, this will trigger actual deletion.
///
/// This takes into account only per-chat timeouts, because global device
/// timeouts are at least one hour long and deletion is triggered often enough
/// by user actions.
pub async fn schedule_ephemeral_task(context: &Context) {
let ephemeral_timestamp: Option<i64> = match context
.sql
.query_get_value(
sqlx::query(
r#"
SELECT ephemeral_timestamp
FROM msgs
WHERE ephemeral_timestamp != 0
AND chat_id != ?
ORDER BY ephemeral_timestamp ASC
LIMIT 1;
"#,
)
.bind(DC_CHAT_ID_TRASH), // Trash contains already deleted messages, skip them
)
.await
{
Err(err) => {
warn!(context, "Can't calculate next ephemeral timeout: {}", err);
return;
}
Ok(ephemeral_timestamp) => ephemeral_timestamp,
};
// Cancel existing task, if any
if let Some(ephemeral_task) = context.ephemeral_task.write().await.take() {
ephemeral_task.cancel().await;
}
if let Some(ephemeral_timestamp) = ephemeral_timestamp {
let now = SystemTime::now();
let until = UNIX_EPOCH
+ Duration::from_secs(ephemeral_timestamp.try_into().unwrap_or(u64::MAX))
+ Duration::from_secs(1);
if let Ok(duration) = until.duration_since(now) {
// Schedule a task, ephemeral_timestamp is in the future
let context1 = context.clone();
let ephemeral_task = task::spawn(async move {
async_std::task::sleep(duration).await;
emit_event!(
context1,
EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0)
}
);
});
*context.ephemeral_task.write().await = Some(ephemeral_task);
} else {
// Emit event immediately
emit_event!(
context,
EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0)
}
);
}
}
}
/// Returns ID of any expired message that should be deleted from the server.
///
/// It looks up the trash chat too, to find messages that are already
/// deleted locally, but not deleted on the server.
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
let now = time();
let threshold_timestamp = match context.get_config_delete_server_after().await? {
None => 0,
Some(delete_server_after) => now - delete_server_after,
};
let row = context
.sql
.fetch_optional(
sqlx::query(
"SELECT id FROM msgs \
WHERE ( \
timestamp < ? \
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
) \
AND server_uid != 0 \
LIMIT 1",
)
.bind(threshold_timestamp)
.bind(now),
)
.await?;
if let Some(row) = row {
let msg_id = row.try_get(0)?;
Ok(Some(msg_id))
} else {
Ok(None)
}
}
/// Start ephemeral timers for seen messages if they are not started
/// yet.
///
/// It is possible that timers are not started due to a missing or
/// failed `MsgId.start_ephemeral_timer()` call, either in the current
/// or previous version of Delta Chat.
///
/// This function is supposed to be called in the background,
/// e.g. from housekeeping task.
pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()> {
context
.sql
.execute(
sqlx::query(
"UPDATE msgs \
SET ephemeral_timestamp = ? + ephemeral_timer \
WHERE ephemeral_timer > 0 \
AND ephemeral_timestamp = 0 \
AND state NOT IN (?, ?, ?)",
)
.bind(time())
.bind(MessageState::InFresh)
.bind(MessageState::InNoticed)
.bind(MessageState::OutDraft),
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use async_std::task::sleep;
use super::*;
use crate::test_utils::TestContext;
use crate::{
chat::{self, Chat, ChatItem},
dc_tools::IsNoneOrEmpty,
};
#[async_std::test]
async fn test_stock_ephemeral_messages() {
let context = TestContext::new().await;
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Disabled, DC_CONTACT_ID_SELF).await,
"Message deletion timer is disabled by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 1 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 s by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 30 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 30 s by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 60 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 minute by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 90 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1.5 minutes by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 30 * 60 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 30 minutes by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 60 * 60 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 hour by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 5400 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1.5 hours by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled {
duration: 2 * 60 * 60
},
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 2 hours by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled {
duration: 24 * 60 * 60
},
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 day by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled {
duration: 2 * 24 * 60 * 60
},
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 2 days by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled {
duration: 7 * 24 * 60 * 60
},
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 week by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled {
duration: 4 * 7 * 24 * 60 * 60
},
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 4 weeks by me."
);
}
#[async_std::test]
async fn test_ephemeral_timer() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(&bob).await.id;
let chat_bob = bob.create_chat(&alice).await.id;
// Alice sends message to Bob
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
// Alice sends second message to Bob, with no timer
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Disabled
);
// Bob sets ephemeral timer and sends a message about timer change
chat_bob
.set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
.await?;
let sent_timer_change = bob.pop_sent_msg().await;
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Enabled { duration: 60 }
);
// Bob receives message from Alice.
// Alice message has no timer. However, Bob should not disable timer,
// because Alice replies to old message.
bob.recv_msg(&sent).await;
assert_eq!(
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
Timer::Disabled
);
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Enabled { duration: 60 }
);
// Alice receives message from Bob
alice.recv_msg(&sent_timer_change).await;
assert_eq!(
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
Timer::Enabled { duration: 60 }
);
Ok(())
}
#[async_std::test]
async fn test_ephemeral_delete_msgs() {
let t = TestContext::new_alice().await;
let chat = t.get_self_chat().await;
t.send_text(chat.id, "Saved message, which we delete manually")
.await;
let msg = t.get_last_msg_in(chat.id).await;
msg.id.delete_from_db(&t).await.unwrap();
check_msg_was_deleted(&t, &chat, msg.id).await;
chat.id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 1 })
.await
.unwrap();
let msg = t
.send_text(chat.id, "Saved message, disappearing after 1s")
.await;
sleep(Duration::from_millis(1100)).await;
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
}
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await.unwrap();
// Check that the chat is empty except for possibly info messages:
for item in &chat_items {
if let ChatItem::Message { msg_id } = item {
let msg = Message::load_from_db(t, *msg_id).await.unwrap();
assert!(msg.is_info())
}
}
// Check that if there is a message left, the text and metadata are gone
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
assert_eq!(msg.from_id, 0);
assert_eq!(msg.to_id, 0);
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
let rawtxt: Option<String> = t
.sql
.query_get_value(sqlx::query("SELECT txt_raw FROM msgs WHERE id=?;").bind(msg_id))
.await
.unwrap();
assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt);
}
}
}

View File

@@ -1,8 +1,6 @@
//! # Error handling
#[derive(Debug, thiserror::Error)]
#[error("Out of Range")]
pub struct OutOfRangeError;
pub use anyhow::{bail, ensure, format_err, Error, Result};
#[macro_export]
macro_rules! ensure_eq {

View File

@@ -1,13 +1,10 @@
//! # Events specification
use std::ops::Deref;
use async_std::channel::{self, Receiver, Sender, TrySendError};
use async_std::path::PathBuf;
use async_std::sync::{channel, Receiver, Sender, TrySendError};
use strum::EnumProperty;
use crate::chat::ChatId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
#[derive(Debug)]
@@ -18,7 +15,7 @@ pub struct Events {
impl Default for Events {
fn default() -> Self {
let (sender, receiver) = channel::bounded(1_000);
let (sender, receiver) = channel(1_000);
Self { receiver, sender }
}
@@ -35,7 +32,7 @@ impl Events {
// try again
self.emit(event);
}
Err(TrySendError::Closed(_)) => {
Err(TrySendError::Disconnected(_)) => {
unreachable!("unable to emit event, channel disconnected");
}
}
@@ -47,17 +44,6 @@ impl Events {
}
}
/// A receiver of events from a [`Context`].
///
/// See [`Context::get_event_emitter`] to create an instance. If multiple instances are
/// created events emitted by the [`Context`] will only be delivered to one of the
/// `EventEmitter`s.
///
/// The `EventEmitter` is also a [`Stream`], so a typical usage is in a `while let` loop.
///
/// [`Context`]: crate::context::Context
/// [`Context::get_event_emitter`]: crate::context::Context::get_event_emitter
/// [`Stream`]: async_std::stream::Stream
#[derive(Debug, Clone)]
pub struct EventEmitter(Receiver<Event>);
@@ -67,59 +53,15 @@ impl EventEmitter {
async_std::task::block_on(self.recv())
}
/// Async recv of an event. Return `None` if the `Sender` has been droped.
/// Blocking async recv of an event. Return `None` if the `Sender` has been droped.
pub async fn recv(&self) -> Option<Event> {
// TODO: change once we can use async channels internally.
self.0.recv().await.ok()
}
}
impl async_std::stream::Stream for EventEmitter {
type Item = Event;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.0).poll_next(cx)
}
}
/// The event emitted by a [`Context`] from an [`EventEmitter`].
///
/// Events are documented on the C/FFI API in `deltachat.h` as `DC_EVENT_*` contants. The
/// context emits them in relation to various operations happening, a lot of these are again
/// documented in `deltachat.h`.
///
/// This struct [`Deref`]s to the [`EventType`].
///
/// [`Context`]: crate::context::Context
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
/// The ID of the [`Context`] which emitted this event.
///
/// This allows using multiple [`Context`]s in a single process as they are identified
/// by this ID.
///
/// [`Context`]: crate::context::Context
pub id: u32,
/// The event payload.
///
/// These are documented in `deltachat.h` as the `DC_EVENT_*` constants.
pub typ: EventType,
}
impl Deref for Event {
type Target = EventType;
fn deref(&self) -> &EventType {
&self.typ
}
}
impl EventType {
/// Returns the corresponding Event ID.
///
/// These are the IDs used in the `DC_EVENT_*` constants in `deltachat.h`.
impl Event {
/// Returns the corresponding Event id.
pub fn as_id(&self) -> i32 {
self.get_str("id")
.expect("missing id")
@@ -129,11 +71,10 @@ impl EventType {
}
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty)]
pub enum EventType {
pub enum Event {
/// The library-user may write an informational string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
/// 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.
#[strum(props(id = "100"))]
Info(String),
@@ -157,22 +98,27 @@ pub enum EventType {
#[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 file in the $BLOBDIR was deleted
/// 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.
/// This event should not be reported to the end-user using a popup or something like that.
#[strum(props(id = "300"))]
Warning(String),
/// The library-user should report an error to the end-user.
/// Passed to the callback given to dc_context_new().
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
@@ -193,6 +139,7 @@ pub enum EventType {
/// Network errors should be reported to users in a non-disturbing way,
/// however, as network errors may come in a sequence,
/// it is not useful to raise each an every error to the user.
/// For this purpose, data1 is set to 1 if the error is probably worth reporting.
///
/// Moreover, if the UI detects that the device is offline,
/// it is probably more useful to report this to the user
@@ -223,11 +170,6 @@ pub enum EventType {
#[strum(props(id = "2005"))]
IncomingMsg { chat_id: ChatId, msg_id: MsgId },
/// Messages were seen or noticed.
/// chat id is always set.
#[strum(props(id = "2008"))]
MsgsNoticed(ChatId),
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
#[strum(props(id = "2010"))]
@@ -247,19 +189,9 @@ pub enum EventType {
/// 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().
///
/// This event does not include ephemeral timer modification, which
/// is a separate event.
#[strum(props(id = "2020"))]
ChatModified(ChatId),
/// Chat ephemeral timer changed.
#[strum(props(id = "2021"))]
ChatEphemeralTimerModified {
chat_id: ChatId,
timer: EphemeralTimer,
},
/// 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.
@@ -275,16 +207,10 @@ pub enum EventType {
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
#[strum(props(id = "2041"))]
ConfigureProgress {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
},
ConfigureProgress(usize),
/// Inform about the import/export progress started by imex().
///

View File

@@ -1,177 +0,0 @@
///! # format=flowed support
///!
///! Format=flowed is defined in
///! [RFC 3676](https://tools.ietf.org/html/rfc3676).
///!
///! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
///! during formatting, i.e., DelSp parameter introduced in RFC 3676
///! is assumed to be set to "no".
///!
///! For received messages, DelSp parameter is honoured.
/// Wraps line to 72 characters using format=flowed soft breaks.
///
/// 72 characters is the limit recommended by RFC 3676.
///
/// The function breaks line only after SP and before non-whitespace
/// characters. It also does not insert breaks before ">" to avoid the
/// need to do space stuffing (see RFC 3676) for quotes.
///
/// If there are long words, line may still exceed the limits on line
/// length. However, this should be rare and should not result in
/// immediate mail rejection: SMTP (RFC 2821) limit is 998 characters,
/// and Spam Assassin limit is 78 characters.
fn format_line_flowed(line: &str, prefix: &str) -> String {
let mut result = String::new();
let mut buffer = prefix.to_string();
let mut after_space = false;
for c in line.chars() {
if c == ' ' {
buffer.push(c);
after_space = true;
} else if c == '>' {
if buffer.is_empty() {
// Space stuffing, see RFC 3676
buffer.push(' ');
}
buffer.push(c);
after_space = false;
} else {
if after_space && buffer.len() >= 72 && !c.is_whitespace() {
// Flush the buffer and insert soft break (SP CRLF).
result += &buffer;
result += "\r\n";
buffer = prefix.to_string();
}
buffer.push(c);
after_space = false;
}
}
result + &buffer
}
fn format_flowed_prefix(text: &str, prefix: &str) -> String {
let mut result = String::new();
for line in text.split('\n') {
if !result.is_empty() {
result += "\r\n";
}
let line = line.trim_end();
if prefix.len() + line.len() > 78 {
result += &format_line_flowed(line, prefix);
} else {
result += prefix;
if prefix.is_empty() && line.starts_with('>') {
// Space stuffing, see RFC 3676
result.push(' ');
}
result += line;
}
}
result
}
/// Returns text formatted according to RFC 3767 (format=flowed).
///
/// This function accepts text separated by LF, but returns text
/// separated by CRLF.
///
/// RFC 2646 technique is used to insert soft line breaks, so DelSp
/// SHOULD be set to "no" when sending.
pub fn format_flowed(text: &str) -> String {
format_flowed_prefix(text, "")
}
/// Same as format_flowed(), but adds "> " prefix to each line.
pub fn format_flowed_quote(text: &str) -> String {
format_flowed_prefix(text, "> ")
}
/// Joins lines in format=flowed text.
///
/// Lines must be separated by single LF.
///
/// Quote processing is not supported, it is assumed that they are
/// deleted during simplification.
///
/// Signature separator line is not processed here, it is assumed to
/// be stripped beforehand.
pub fn unformat_flowed(text: &str, delsp: bool) -> String {
let mut result = String::new();
let mut skip_newline = true;
for line in text.split('\n') {
// Revert space-stuffing
let line = line.strip_prefix(" ").unwrap_or(line);
if !skip_newline {
result.push('\n');
}
if let Some(line) = line.strip_suffix(" ") {
// Flowed line
result += line;
if !delsp {
result.push(' ');
}
skip_newline = true;
} else {
// Fixed line
result += line;
skip_newline = false;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_flowed() {
let text = "Foo bar baz";
assert_eq!(format_flowed(text), "Foo bar baz");
let text = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\
\n\
To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
let expected = "This is the Autocrypt Setup Message used to transfer your key between clients.\r\n\
\r\n\
To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
client and enter the setup code presented on the generating device.";
assert_eq!(format_flowed(text), expected);
let text = "> Not a quote";
assert_eq!(format_flowed(text), " > Not a quote");
}
#[test]
fn test_unformat_flowed() {
let text = "this is a very long message that should be wrapped using format=flowed and \n\
unwrapped on the receiver";
let expected =
"this is a very long message that should be wrapped using format=flowed and \
unwrapped on the receiver";
assert_eq!(unformat_flowed(text, false), expected);
}
#[test]
fn test_format_flowed_quote() {
let quote = "this is a quoted line";
let expected = "> this is a quoted line";
assert_eq!(format_flowed_quote(quote), expected);
let quote = "> foo bar baz";
let expected = "> > foo bar baz";
assert_eq!(format_flowed_quote(quote), expected);
let quote = "this is a very long quote that should be wrapped using format=flowed and unwrapped on the receiver";
let expected =
"> this is a very long quote that should be wrapped using format=flowed and \r\n\
> unwrapped on the receiver";
assert_eq!(format_flowed_quote(quote), expected);
}
}

View File

@@ -3,6 +3,7 @@ use mailparse::{MailHeader, MailHeaderMap};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[strum(serialize_all = "kebab_case")]
#[allow(dead_code)]
pub enum HeaderDef {
MessageId,
Subject,
@@ -11,26 +12,16 @@ pub enum HeaderDef {
To,
Cc,
Disposition,
/// Used in the "Body Part Header" of MDNs as of RFC 8098.
/// Indicates the Message-ID of the message for which the MDN is being issued.
OriginalMessageId,
/// Delta Chat extension for message IDs in combined MDNs
AdditionalMessageIds,
/// Outlook-SMTP-server replace the `Message-ID:`-header
/// and write the original ID to `X-Microsoft-Original-Message-ID`.
/// To sort things correctly and to not show outgoing messages twice,
/// we need to check that header as well.
XMicrosoftOriginalMessageId,
ListId,
References,
InReplyTo,
Precedence,
ContentType,
ContentId,
ChatVersion,
ChatGroupId,
ChatGroupName,
@@ -44,7 +35,6 @@ pub enum HeaderDef {
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
ChatWebrtcRoom,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
@@ -52,9 +42,6 @@ pub enum HeaderDef {
SecureJoinFingerprint,
SecureJoinInvitenumber,
SecureJoinAuth,
Sender,
EphemeralTimer,
Received,
_TestHeader,
}

View File

@@ -1,553 +0,0 @@
///! # Get message as HTML.
///!
///! Use `Message.has_html()` to check if the UI shall render a
///! corresponding button and `MsgId.get_html()` to get the full message.
///!
///! Even when the original mime-message is not HTML,
///! `MsgId.get_html()` will return HTML -
///! this allows nice quoting, handling linebreaks properly etc.
use futures::future::FutureExt;
use std::future::Future;
use std::pin::Pin;
use anyhow::Result;
use lettre_email::mime::{self, Mime};
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{Message, MsgId};
use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
use crate::plaintext::PlainText;
use crate::{context::Context, message};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
impl Message {
/// Check if the message can be retrieved as HTML.
/// Typically, this is the case, when the mime structure of a Message is modified,
/// meaning that some text is cut or the original message
/// is in HTML and `simplify()` may hide some maybe important information.
/// The corresponding ffi-function is `dc_msg_has_html()`.
/// To get the HTML-code of the message, use `MsgId.get_html()`.
pub fn has_html(&self) -> bool {
self.mime_modified
}
/// Set HTML-part part of a message that is about to be sent.
/// The HTML-part is written to the database before sending and
/// used as the `text/html` part in the MIME-structure.
///
/// Received HTML parts are handled differently,
/// they are saved together with the whole MIME-structure
/// in `mime_headers` and the HTML-part is extracted using `MsgId::get_html()`.
/// (To underline this asynchronicity, we are using the wording "SendHtml")
pub fn set_html(&mut self, html: Option<String>) {
if let Some(html) = html {
self.param.set(SendHtml, html);
self.mime_modified = true;
} else {
self.param.remove(SendHtml);
self.mime_modified = false;
}
}
}
/// Type defining a rough mime-type.
/// This is mainly useful on iterating
/// to decide whether a mime-part has subtypes.
enum MimeMultipartType {
Multiple,
Single,
Message,
}
/// Function takes a content type from a ParsedMail structure
/// and checks and returns the rough mime-type.
async fn get_mime_multipart_type(ctype: &ParsedContentType) -> MimeMultipartType {
let mimetype = ctype.mimetype.to_lowercase();
if mimetype.starts_with("multipart") && ctype.params.get("boundary").is_some() {
MimeMultipartType::Multiple
} else if mimetype == "message/rfc822" {
MimeMultipartType::Message
} else {
MimeMultipartType::Single
}
}
/// HtmlMsgParser converts a mime-message to HTML.
#[derive(Debug)]
struct HtmlMsgParser {
pub html: String,
pub plain: Option<PlainText>,
}
impl HtmlMsgParser {
/// Function takes a raw mime-message string,
/// searches for the main-text part
/// and returns that as parser.html
pub async fn from_bytes(context: &Context, rawmime: &[u8]) -> Result<Self> {
let mut parser = HtmlMsgParser {
html: "".to_string(),
plain: None,
};
let parsedmail = mailparse::parse_mail(rawmime)?;
parser.collect_texts_recursive(context, &parsedmail).await?;
if parser.html.is_empty() {
if let Some(plain) = &parser.plain {
parser.html = plain.to_html().await;
}
} else {
parser.cid_to_data_recursive(context, &parsedmail).await?;
}
Ok(parser)
}
/// Function iterates over all mime-parts
/// and searches for text/plain and text/html parts and saves the
/// first one found.
/// in the corresponding structure fields.
///
/// Usually, there is at most one plain-text and one HTML-text part,
/// multiple plain-text parts might be used for mailinglist-footers,
/// therefore we use the first one.
fn collect_texts_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
// Boxed future to deal with recursion
async move {
match get_mime_multipart_type(&mail.ctype).await {
MimeMultipartType::Multiple => {
for cur_data in mail.subparts.iter() {
self.collect_texts_recursive(context, cur_data).await?
}
Ok(())
}
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).unwrap();
self.collect_texts_recursive(context, &mail).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype == mime::TEXT_HTML {
if self.html.is_empty() {
if let Ok(decoded_data) = mail.get_body() {
self.html = decoded_data;
}
}
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
if let Ok(decoded_data) = mail.get_body() {
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
},
});
}
}
Ok(())
}
}
}
.boxed()
}
/// Replace cid:-protocol by the data:-protocol where appropriate.
/// This allows the final html-file to be self-contained.
fn cid_to_data_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
// Boxed future to deal with recursion
async move {
match get_mime_multipart_type(&mail.ctype).await {
MimeMultipartType::Multiple => {
for cur_data in mail.subparts.iter() {
self.cid_to_data_recursive(context, cur_data).await?;
}
Ok(())
}
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).unwrap();
self.cid_to_data_recursive(context, &mail).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype.type_() == mime::IMAGE {
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
if let Ok(cid) = parse_message_id(&cid) {
if let Ok(replacement) = mimepart_to_data_url(mail).await {
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
);
match regex::Regex::new(&re_string) {
Ok(re) => {
self.html = re
.replace_all(
&*self.html,
format!("${{1}}{}${{3}}", replacement).as_str(),
)
.as_ref()
.to_string()
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}",
re_string,
e
),
}
}
}
}
}
Ok(())
}
}
}
.boxed()
}
}
/// Convert a mime part to a data: url as defined in [RFC 2397](https://tools.ietf.org/html/rfc2397).
async fn mimepart_to_data_url(mail: &mailparse::ParsedMail<'_>) -> Result<String> {
let data = mail.get_body_raw()?;
let data = base64::encode(&data);
Ok(format!("data:{};base64,{}", mail.ctype.mimetype, data))
}
impl MsgId {
/// Get HTML from a message-id.
/// This requires `mime_headers` field to be set for the message;
/// this is the case at least when `Message.has_html()` returns true
/// (we do not save raw mime unconditionally in the database to save space).
/// The corresponding ffi-function is `dc_get_msg_html()`.
pub async fn get_html(self, context: &Context) -> Result<Option<String>> {
let rawmime = message::get_mime_headers(context, self).await?;
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
Err(err) => {
warn!(context, "get_html: parser error: {}", err);
Ok(None)
}
Ok(parser) => Ok(Some(parser.html)),
}
} else {
warn!(context, "get_html: no mime for {}", self);
Ok(None)
}
}
}
/// Wraps HTML text into a new text/html mimepart structure.
///
/// Used on forwarding messages to avoid leaking the original mime structure
/// and also to avoid sending too much, maybe large data.
pub async fn new_html_mimepart(html: String) -> PartBuilder {
PartBuilder::new()
.content_type(&"text/html; charset=utf-8".parse::<mime::Mime>().unwrap())
.body(html)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::chat::forward_msgs;
use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::MessengerMessage;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_htmlparse_plain_unspecified() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
This message does not have Content-Type nor Subject.<br/>
<br/>
</body></html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_plain_iso88591() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_iso88591.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
<br/>
</body></html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_plain_flowed() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_flowed.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert!(parser.plain.unwrap().flowed);
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
This line ends with a space and will be merged with the next one due to format=flowed.<br/>
<br/>
This line does not end with a space<br/>
and will be wrapped as usual.<br/>
<br/>
</body></html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_alt_plain() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
mime-modified should not be set set as there is no html and no special stuff;<br/>
although not being a delta-message.<br/>
test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x27; :)<br/>
<br/>
<br/>
</body></html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_html.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
// on windows, `\r\n` linends are returned from mimeparser,
// however, rust multiline-strings use just `\n`;
// therefore, we just remove `\r` before comparison.
assert_eq!(
parser.html.replace("\r", ""),
r##"
<html>
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
</html>"##
);
}
#[async_std::test]
async fn test_htmlparse_alt_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace("\r", ""), // see comment in test_htmlparse_html()
r##"<html>
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
</html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_alt_plain_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace("\r", ""), // see comment in test_htmlparse_html()
r##"<html>
<p>
this is <b>html</b>
</p>
</html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_apple_cid_jpg() {
// load raw mime html-data with related image-part (cid:)
// and make sure, Content-Id has angle-brackets that are removed correctly.
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/apple_cid_jpg.eml");
let test = String::from_utf8_lossy(raw);
assert!(test.contains("Content-Id: <8AE052EF-BC90-486F-BB78-58D3590308EC@fritz.box>"));
assert!(test.contains("cid:8AE052EF-BC90-486F-BB78-58D3590308EC@fritz.box"));
assert!(test.find("data:").is_none());
// parsing converts cid: to data:
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert!(parser.html.contains("<html>"));
assert!(!parser.html.contains("Content-Id:"));
assert!(parser.html.contains("data:image/jpeg;base64,/9j/4AAQ"));
assert!(!parser.html.contains("cid:"));
}
#[async_std::test]
async fn test_get_html_empty() {
let t = TestContext::new().await;
let msg_id = MsgId::new_unset();
assert!(msg_id.get_html(&t).await.unwrap().is_none())
}
#[async_std::test]
async fn test_html_forwarding() {
// alice receives a non-delta html-message
let alice = TestContext::new_alice().await;
alice.set_config(Config::ShowEmails, Some("2")).await.ok();
let chat = alice
.create_chat_with_contact("", "sender@testrun.org")
.await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
dc_receive_imf(&alice, raw, "INBOX", 1, false)
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::No);
assert!(!msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
// alice: create chat with bob and forward received html-message there
let chat = alice.create_chat_with_contact("", "bob@example.net").await;
forward_msgs(&alice, &[msg.get_id()], chat.get_id())
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_eq!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
// bob: check that bob also got the html-part of the forwarded message
let bob = TestContext::new_bob().await;
let chat = bob.create_chat_with_contact("", "alice@example.com").await;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let msg = bob.get_last_msg_in(chat.get_id()).await;
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
#[async_std::test]
async fn test_html_forwarding_encrypted() {
// Alice receives a non-delta html-message
// (`ShowEmails=1` lets Alice actually receive non-delta messages for known contacts,
// the contact is marked as known by creating a chat using `chat_with_contact()`)
let alice = TestContext::new_alice().await;
alice.set_config(Config::ShowEmails, Some("1")).await.ok();
let chat = alice
.create_chat_with_contact("", "sender@testrun.org")
.await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
dc_receive_imf(&alice, raw, "INBOX", 1, false)
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
// forward the message to saved-messages,
// this will encrypt the message as new_alice() has set up keys
let chat = alice.get_self_chat().await;
forward_msgs(&alice, &[msg.get_id()], chat.get_id())
.await
.unwrap();
let msg = alice.pop_sent_msg().await;
// receive the message on another device
let alice = TestContext::new_alice().await;
assert_eq!(alice.get_config_int(Config::ShowEmails).await.unwrap(), 0); // set to "1" above, make sure it is another db
alice.recv_msg(&msg).await;
let chat = alice.get_self_chat().await;
let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_eq!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.get_showpadlock());
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
#[async_std::test]
async fn test_set_html() {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// alice sends a message with html-part to bob
let chat_id = alice.create_chat(&bob).await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("plain text".to_string()));
msg.set_html(Some("<b>html</b> text".to_string()));
assert!(msg.mime_modified);
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();
// check the message is written correctly to alice's db
let msg = alice.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
// let bob receive the message
let chat_id = bob.create_chat(&alice).await.id;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let msg = bob.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
}
}

View File

@@ -56,7 +56,7 @@ impl Client {
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
self,
auth_type: S,
authenticator: A,
authenticator: &A,
) -> std::result::Result<Session, (ImapError, Self)> {
let Client { inner, is_secure } = self;
let session =
@@ -85,6 +85,9 @@ impl Client {
let tls_stream: Box<dyn SessionStream> =
Box::new(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()
@@ -101,6 +104,9 @@ impl Client {
let stream: Box<dyn SessionStream> = Box::new(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

View File

@@ -1,15 +1,34 @@
use super::Imap;
use anyhow::{bail, format_err, Result};
use async_imap::extensions::idle::IdleResponse;
use async_imap::types::UnsolicitedResponse;
use async_std::prelude::*;
use std::time::{Duration, SystemTime};
use crate::{context::Context, scheduler::InterruptInfo};
use super::select_folder;
use super::session::Session;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IMAP IDLE protocol failed to init/complete")]
IdleProtocolFailed(#[from] async_imap::error::Error),
#[error("IMAP IDLE protocol timed out")]
IdleTimeout(#[from] async_std::future::TimeoutError),
#[error("IMAP server does not have IDLE capability")]
IdleAbilityMissing,
#[error("IMAP select folder error")]
SelectFolderError(#[from] select_folder::Error),
#[error("Setup handle error")]
SetupHandleError(#[from] super::Error),
}
impl Imap {
pub fn can_idle(&self) -> bool {
self.config.can_idle
@@ -23,42 +42,20 @@ impl Imap {
use futures::future::FutureExt;
if !self.can_idle() {
bail!("IMAP server does not have IDLE capability");
return Err(Error::IdleAbilityMissing);
}
self.setup_handle(context).await?;
self.setup_handle_if_needed(context).await?;
self.select_folder(context, watch_folder.clone()).await?;
let session = self.session.take();
let timeout = Duration::from_secs(23 * 60);
let mut info = Default::default();
if let Some(session) = self.session.take() {
// if we have unsolicited responses we directly return
let mut unsolicited_exists = false;
while let Ok(response) = session.unsolicited_responses.try_recv() {
match response {
UnsolicitedResponse::Exists(_) => {
warn!(context, "skip idle, got unsolicited EXISTS {:?}", response);
unsolicited_exists = true;
}
_ => info!(context, "ignoring unsolicited response {:?}", response),
}
}
if unsolicited_exists {
self.session = Some(session);
return Ok(info);
}
if let Ok(info) = self.idle_interrupt.try_recv() {
info!(context, "skip idle, got interrupt {:?}", info);
self.session = Some(session);
return Ok(info);
}
if let Some(session) = session {
let mut handle = session.idle();
if let Err(err) = handle.init().await {
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
return Err(Error::IdleProtocolFailed(err));
}
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
@@ -68,47 +65,68 @@ impl Imap {
Interrupt(InterruptInfo),
}
info!(
context,
"{}: Idle entering wait-on-remote state",
watch_folder.as_deref().unwrap_or("None")
);
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let info = self.idle_interrupt.recv().await;
// cancel imap idle connection properly
if self.skip_next_idle_wait {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait = false;
drop(idle_wait);
drop(interrupt);
Ok(Event::Interrupt(info.unwrap_or_default()))
});
info!(context, "Idle wait was skipped");
} else {
info!(context, "Idle entering wait-on-remote state");
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(
self.idle_interrupt.recv().map(|probe_network| {
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
}),
);
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "Idle has NewData {:?}", x);
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(context, "Idle wait was interrupted");
}
Ok(Event::Interrupt(i)) => {
info = i;
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
match fut.await {
Ok(Event::IdleResponse(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(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(context, "Idle wait was interrupted");
}
Ok(Event::Interrupt(i)) => {
info = i;
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
let session = handle
// if we can't properly terminate the idle
// protocol let's break the connection.
let res = handle
.done()
.timeout(Duration::from_secs(15))
.await
.map_err(|err| format_err!("IMAP IDLE protocol timed out: {}", err))??;
self.session = Some(Session { inner: session });
} else {
warn!(context, "Attempted to idle without a session");
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
self.session = Some(Session { inner: 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(info)
@@ -123,74 +141,80 @@ impl Imap {
// in this case, we're waiting for a configure job (and an interrupt).
let fake_idle_start_time = SystemTime::now();
info!(context, "IMAP-fake-IDLEing...");
// Do not poll, just wait for an interrupt when no folder is passed in.
let watch_folder = if let Some(watch_folder) = watch_folder {
watch_folder
} else {
info!(context, "IMAP-fake-IDLE: no folder, waiting for interrupt");
if watch_folder.is_none() {
return self.idle_interrupt.recv().await.unwrap_or_default();
};
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let mut interval = async_std::stream::interval(Duration::from_secs(60));
enum Event {
Tick,
Interrupt(InterruptInfo),
}
// loop until we are interrupted or if we fetched something
let info = loop {
use futures::future::FutureExt;
match interval
.next()
.map(|_| Event::Tick)
.race(
self.idle_interrupt
.recv()
.map(|probe_network| Event::Interrupt(probe_network.unwrap_or_default())),
)
.await
{
Event::Tick => {
// 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).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if self.config.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false, None);
}
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.
match self.fetch_new_messages(context, &watch_folder, false).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {
let mut info: InterruptInfo = Default::default();
if self.skip_next_idle_wait {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait = false;
info!(context, "fake-idle wait was skipped");
} else {
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let mut interval = async_std::stream::interval(Duration::from_secs(60));
enum Event {
Tick,
Interrupt(InterruptInfo),
}
// loop until we are interrupted or if we fetched something
info =
loop {
use futures::future::FutureExt;
match interval
.next()
.map(|_| Event::Tick)
.race(self.idle_interrupt.recv().map(|probe_network| {
Event::Interrupt(probe_network.unwrap_or_default())
}))
.await
{
Event::Tick => {
// 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).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if self.config.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false, None);
}
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 InterruptInfo::new(false, None);
}
}
Err(err) => {
error!(context, "could not fetch from folder: {}", err);
self.trigger_reconnect()
}
}
}
}
Err(err) => {
error!(context, "could not fetch from folder: {:#}", err);
self.trigger_reconnect()
Event::Interrupt(info) => {
// Interrupt
break info;
}
}
}
Event::Interrupt(info) => {
// Interrupt
break info;
}
}
};
};
}
info!(
context,

File diff suppressed because it is too large Load Diff

View File

@@ -1,105 +0,0 @@
use std::time::Instant;
use anyhow::{Context as _, Result};
use crate::config::Config;
use crate::context::Context;
use crate::imap::Imap;
use async_std::prelude::*;
use super::{get_folder_meaning, get_folder_meaning_by_name, FolderMeaning};
impl Imap {
pub async fn scan_folders(&mut self, context: &Context) -> Result<()> {
// First of all, debounce to once per minute:
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
let elapsed_secs = last_scan.elapsed().as_secs();
let debounce_secs = context
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
.await?;
if elapsed_secs < debounce_secs {
return Ok(());
}
}
info!(context, "Starting full folder scan");
self.setup_handle(context).await?;
let session = self.session.as_mut();
let session = session.context("scan_folders(): IMAP No Connection established")?;
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
let watched_folders = get_watched_folders(context).await;
let mut sentbox_folder = None;
let mut spam_folder = None;
for folder in folders {
let folder = match folder {
Ok(f) => f,
Err(e) => {
warn!(context, "Can't get folder: {}", e);
continue;
}
};
let foldername = folder.name();
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(foldername);
if folder_meaning == FolderMeaning::SentObjects {
// Always takes precedent
sentbox_folder = Some(folder.name().to_string());
} else if folder_meaning == FolderMeaning::Spam {
spam_folder = Some(folder.name().to_string());
} else if folder_name_meaning == FolderMeaning::SentObjects {
// only set iff none has been already set
if sentbox_folder.is_none() {
sentbox_folder = Some(folder.name().to_string());
}
} else if folder_name_meaning == FolderMeaning::Spam && spam_folder.is_none() {
spam_folder = Some(folder.name().to_string());
}
if watched_folders.contains(&foldername.to_string()) {
info!(
context,
"Not scanning folder {} as it is watched anyway", foldername
);
} else {
info!(context, "Scanning folder: {}", foldername);
if let Err(e) = self.fetch_new_messages(context, foldername, false).await {
warn!(context, "Can't fetch new msgs in scanned folder: {:#}", e);
}
}
}
context
.set_config(Config::ConfiguredSentboxFolder, sentbox_folder.as_deref())
.await?;
context
.set_config(Config::ConfiguredSpamFolder, spam_folder.as_deref())
.await?;
last_scan.replace(Instant::now());
Ok(())
}
}
async fn get_watched_folders(context: &Context) -> Vec<String> {
let mut res = Vec::new();
let folder_watched_configured = &[
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),
(Config::MvboxWatch, Config::ConfiguredMvboxFolder),
(Config::InboxWatch, Config::ConfiguredInboxFolder),
];
for (watched, configured) in folder_watched_configured {
if context.get_config_bool(*watched).await.unwrap_or_default() {
if let Ok(Some(folder)) = context.get_config(*configured).await {
res.push(folder);
}
}
}
res
}

View File

@@ -51,22 +51,13 @@ impl Imap {
Ok(())
}
/// Issues a CLOSE command if selected folder needs expunge.
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> Result<()> {
if self.config.selected_folder_needs_expunge {
self.close_folder(context).await?;
}
Ok(())
}
/// select a folder, possibly update uid_validity and, if needed,
/// expunge the folder to remove delete-marked messages.
/// Returns whether a new folder was selected.
pub(super) async fn select_folder<S: AsRef<str>>(
&mut self,
context: &Context,
folder: Option<S>,
) -> Result<NewlySelected> {
) -> Result<()> {
if self.session.is_none() {
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
@@ -79,13 +70,16 @@ impl Imap {
if let Some(ref folder) = folder {
if let Some(ref selected_folder) = self.config.selected_folder {
if folder.as_ref() == selected_folder {
return Ok(NewlySelected::No);
return Ok(());
}
}
}
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
self.maybe_close_folder(context).await?;
let needs_expunge = { self.config.selected_folder_needs_expunge };
if needs_expunge {
self.close_folder(context).await?;
}
// select new folder
if let Some(ref folder) = folder {
@@ -100,7 +94,7 @@ impl Imap {
Ok(mailbox) => {
self.config.selected_folder = Some(folder.as_ref().to_string());
self.config.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
Ok(())
}
Err(async_imap::error::Error::ConnectionLost) => {
self.trigger_reconnect();
@@ -120,15 +114,7 @@ impl Imap {
Err(Error::NoSession)
}
} else {
Ok(NewlySelected::No)
Ok(())
}
}
}
#[derive(PartialEq, Debug, Copy, Clone, Eq)]
pub(super) enum NewlySelected {
/// The folder was newly selected during this call to select_folder().
Yes,
/// No SELECT command was run because the folder already was selected
/// and self.config.selected_mailbox was not updated (so, e.g. it may contain an outdated uid_next)
No,
}

View File

@@ -1,45 +1,32 @@
//! # Import/export module
use std::any::Any;
use std::ffi::OsStr;
use std::cmp::{max, min};
use anyhow::{bail, ensure, format_err, Context as _, Result};
use async_std::path::{Path, PathBuf};
use async_std::{
fs::{self, File},
prelude::*,
};
use async_std::prelude::*;
use rand::{thread_rng, Rng};
use sqlx::Row;
use crate::blob::BlobObject;
use crate::chat;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::{
dc_copy_file, dc_create_folder, dc_delete_file, dc_delete_files_in_dir, dc_get_filesuffix_lc,
dc_open_file_std, dc_read_file, dc_write_file, get_next_backup_path, time, EmailAddress,
};
use crate::dc_tools::*;
use crate::e2ee;
use crate::events::EventType;
use crate::error::*;
use crate::events::Event;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::param::*;
use crate::pgp;
use crate::sql::{self, Sql};
use crate::stock_str;
use crate::{blob::BlobObject, log::LogExt};
use ::pgp::types::KeyTrait;
use async_tar::Archive;
// Name of the database file in the backup.
const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
const BLOBS_BACKUP_NAME: &str = "blobs_backup";
use crate::stock::StockMessage;
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u32)]
#[repr(i32)]
pub enum ImexMode {
/// Export all private keys and all public keys of the user to the
/// directory given as `param1`. The default key is written to the files `public-key-default.asc`
@@ -55,8 +42,8 @@ pub enum ImexMode {
/// Export a backup to the directory given as `param1`.
/// The backup contains all contacts, chats, images and other data and device independent settings.
/// The backup does not contain device dependent settings as ringtones or LED notification settings.
/// The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
/// the format is `delta-chat-<day>-<number>.tar`
/// The name of the backup is typically `delta-chat.<day>.bak`, if more than one backup is create on a day,
/// the format is `delta-chat.<day>-<number>.bak`
ExportBackup = 11,
/// `param1` is the file (not: directory) to import. The file is normally
@@ -79,87 +66,28 @@ pub enum ImexMode {
///
/// Only one import-/export-progress can run at the same time.
/// To cancel an import-/export-progress, drop the future returned by this function.
pub async fn imex(context: &Context, what: ImexMode, param1: impl AsRef<Path>) -> Result<()> {
let cancel = context.alloc_ongoing().await?;
pub async fn imex(
context: &Context,
what: ImexMode,
param1: Option<impl AsRef<Path>>,
) -> Result<()> {
use futures::future::FutureExt;
let res = async {
let success = imex_inner(context, what, param1).await;
match success {
Ok(()) => {
info!(context, "IMEX successfully completed");
context.emit_event(EventType::ImexProgress(1000));
Ok(())
}
Err(err) => {
cleanup_aborted_imex(context, what).await;
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
error!(context, "{:#}", err);
context.emit_event(EventType::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err);
}
}
}
.race(async {
cancel.recv().await.ok();
cleanup_aborted_imex(context, what).await;
Err(format_err!("canceled"))
})
.await;
let cancel = context.alloc_ongoing().await?;
let res = imex_inner(context, what, param1)
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
.await;
context.free_ongoing().await;
res
}
async fn cleanup_aborted_imex(context: &Context, what: ImexMode) {
if what == ImexMode::ImportBackup {
dc_delete_file(context, context.get_dbfile()).await;
dc_delete_files_in_dir(context, context.get_blobdir()).await;
}
if what == ImexMode::ExportBackup || what == ImexMode::ImportBackup {
if let Err(e) = context.sql.open(context, context.get_dbfile(), false).await {
warn!(context, "Re-opening db after imex failed: {}", e);
}
}
}
/// Returns the filename of the backup found (otherwise an error)
pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
let dir_name = dir_name.as_ref();
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
let mut newest_backup_name = "".to_string();
let mut newest_backup_path: Option<PathBuf> = None;
while let Some(dirent) = dir_iter.next().await {
if let Ok(dirent) = dirent {
let path = dirent.path();
let name = dirent.file_name();
let name: String = name.to_string_lossy().into();
if name.starts_with("delta-chat")
&& name.ends_with(".tar")
&& (newest_backup_name.is_empty() || name > newest_backup_name)
{
// We just use string comparison to determine which backup is newer.
// This works fine because the filenames have the form ...delta-chat-backup-2020-07-24-00.tar
newest_backup_path = Some(path);
newest_backup_name = name;
}
}
}
match newest_backup_path {
Some(path) => Ok(path.to_string_lossy().into_owned()),
None => has_backup_old(context, dir_name).await,
// When we decide to remove support for .bak backups, we can replace this with `None => bail!("no backup found in {}", dir_name.display()),`.
}
}
/// Returns the filename of the backup found (otherwise an error)
pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
let dir_name = dir_name.as_ref();
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
let mut newest_backup_time = 0;
let mut newest_backup_name = "".to_string();
let mut newest_backup_path: Option<PathBuf> = None;
while let Some(dirent) = dir_iter.next().await {
if let Ok(dirent) = dirent {
@@ -168,39 +96,21 @@ pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Re
let name = name.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
let sql = Sql::new();
match sql.open(context, &path, true).await {
Ok(_) => {
let curr_backup_time = sql
.get_raw_config_int("backup_time")
.await?
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
newest_backup_time = curr_backup_time;
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close().await;
}
Err(e) => {
warn!(
context,
"Found backup file {} which could not be opened: {}", name, e
);
// On some Android devices we can't open sql files that are not in our private directory
// (see https://github.com/deltachat/deltachat-android/issues/1768). So, compare names
// to still find the newest backup.
let name: String = name.into();
if newest_backup_time == 0
&& (newest_backup_name.is_empty() || name > newest_backup_name)
{
newest_backup_path = Some(path);
newest_backup_name = name;
}
}
sql.open(context, &path, true).await?;
let curr_backup_time = sql
.get_raw_config_int("backup_time")
.await
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
newest_backup_time = curr_backup_time;
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close().await;
}
}
}
match newest_backup_path {
Some(path) => Ok(path.to_string_lossy().into_owned()),
None => bail!("no backup found in {}", dir_name.display()),
@@ -240,8 +150,10 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
msg.param
.set(Param::MimeType, "application/autocrypt-setup");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
msg.param.set_int(Param::ForcePlaintext, 1);
msg.param.set_int(Param::SkipAutocrypt, 1);
msg.param.set_int(
Param::ForcePlaintext,
ForcePlaintext::NoAutocryptHeader as i32,
);
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
info!(context, "Wait for setup message being sent ...",);
@@ -266,18 +178,17 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
///
/// The `passphrase` must be at least 2 characters long.
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
passphrase_begin
} else {
bail!("Passphrase must be at least 2 chars long.");
};
ensure!(
passphrase.len() >= 2,
"Passphrase must be at least 2 chars long."
);
let private_key = SignedSecretKey::load_self(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
};
let private_key_asc = private_key.to_asc(ac_headers);
let encr = pgp::symm_encrypt(passphrase, private_key_asc.as_bytes()).await?;
let encr = pgp::symm_encrypt(&passphrase, private_key_asc.as_bytes()).await?;
let replacement = format!(
concat!(
@@ -285,12 +196,12 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
"Passphrase-Format: numeric9x4\r\n",
"Passphrase-Begin: {}"
),
passphrase_begin
&passphrase[..2]
);
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
let msg_subj = stock_str::ac_setup_msg_subject(context).await;
let msg_body = stock_str::ac_setup_msg_body(context).await;
let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject).await;
let msg_body = context.stock_str(StockMessage::AcSetupMsgBody).await;
let msg_body_html = msg_body.replace("\r", "").replace("\n", "<br>");
Ok(format!(
concat!(
@@ -334,7 +245,7 @@ pub fn create_setup_code(_context: &Context) -> String {
}
async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool("bcc_self").await? {
if !context.sql.get_raw_config_bool("bcc_self").await {
let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled.
msg.text = Some(
@@ -395,7 +306,7 @@ async fn set_self_key(
};
context
.sql
.set_raw_config_int("e2ee_enabled", e2ee_enabled)
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)
.await?;
}
None => {
@@ -405,7 +316,7 @@ async fn set_self_key(
}
};
let self_addr = context.get_config(Config::ConfiguredAddr).await?;
let self_addr = context.get_config(Config::ConfiguredAddr).await;
ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
let keypair = pgp::KeyPair {
@@ -423,8 +334,6 @@ async fn set_self_key(
},
)
.await?;
info!(context, "stored self key: {:?}", keypair.secret.key_id());
Ok(())
}
@@ -441,7 +350,7 @@ async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
pub fn normalize_setup_code(s: &str) -> String {
let mut out = String::new();
for c in s.chars() {
if ('0'..='9').contains(&c) {
if c >= '0' && c <= '9' {
out.push(c);
if let 4 | 9 | 14 | 19 | 24 | 29 | 34 | 39 = out.len() {
out += "-"
@@ -451,11 +360,19 @@ pub fn normalize_setup_code(s: &str) -> String {
out
}
async fn imex_inner(context: &Context, what: ImexMode, path: impl AsRef<Path>) -> Result<()> {
info!(context, "Import/export dir: {}", path.as_ref().display());
ensure!(context.sql.is_open().await, "Database not opened.");
context.emit_event(EventType::ImexProgress(10));
async fn imex_inner(
context: &Context,
what: ImexMode,
param: Option<impl AsRef<Path>>,
) -> Result<()> {
ensure!(param.is_some(), "No Import/export dir/file given.");
info!(context, "Import/export process started.");
context.emit_event(Event::ImexProgress(10));
ensure!(context.sql.is_open().await, "Database not opened.");
let path = param.unwrap();
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists
if e2ee::ensure_secret_key_exists(context).await.is_err() {
@@ -465,27 +382,29 @@ async fn imex_inner(context: &Context, what: ImexMode, path: impl AsRef<Path>) -
}
}
match what {
let success = match what {
ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
ImexMode::ExportBackup => export_backup(context, path).await,
// import_backup() will call import_backup_old() if this is an old backup.
ImexMode::ImportBackup => import_backup(context, path).await,
};
match success {
Ok(()) => {
info!(context, "IMEX successfully completed");
context.emit_event(Event::ImexProgress(1000));
Ok(())
}
Err(err) => {
error!(context, "IMEX FAILED: {}", err);
context.emit_event(Event::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err);
}
}
}
/// Import Backup
async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
if backup_to_import
.as_ref()
.to_string_lossy()
.ends_with(".bak")
{
// Backwards compability
return import_backup_old(context, backup_to_import).await;
}
info!(
context,
"Import \"{}\" to \"{}\".",
@@ -494,84 +413,9 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
);
ensure!(
!context.is_configured().await?,
!context.is_configured().await,
"Cannot import backups to accounts in use."
);
ensure!(
!context.scheduler.read().await.is_running(),
"cannot import backup, IO already running"
);
context.sql.close().await;
dc_delete_file(context, context.get_dbfile()).await;
ensure!(
!context.get_dbfile().exists().await,
"Cannot delete old database."
);
let backup_file = File::open(backup_to_import).await?;
let file_size = backup_file.metadata().await?.len();
let archive = Archive::new(backup_file);
let mut entries = archive.entries()?;
while let Some(file) = entries.next().await {
let f = &mut file?;
let current_pos = f.raw_file_position();
let progress = 1000 * current_pos / file_size;
if progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
context.emit_event(EventType::ImexProgress(progress as usize));
}
if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
// async_tar can't unpack to a specified file name, so we just unpack to the blobdir and then move the unpacked file.
f.unpack_in(context.get_blobdir()).await?;
fs::rename(
context.get_blobdir().join(DBFILE_BACKUP_NAME),
context.get_dbfile(),
)
.await?;
} else {
// async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards.
f.unpack_in(context.get_blobdir()).await?;
let from_path = context.get_blobdir().join(f.path()?);
if from_path.is_file().await {
if let Some(name) = from_path.file_name() {
fs::rename(&from_path, context.get_blobdir().join(name)).await?;
} else {
warn!(context, "No file name");
}
}
}
}
context
.sql
.open(context, &context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(context).await?;
Ok(())
}
async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
info!(
context,
"Import \"{}\" to \"{}\".",
backup_to_import.as_ref().display(),
context.get_dbfile().display()
);
ensure!(
!context.is_configured().await?,
"Cannot import backups to accounts in use."
);
ensure!(
!context.scheduler.read().await.is_running(),
"cannot import backup, IO already running"
);
context.sql.close().await;
dc_delete_file(context, context.get_dbfile()).await;
ensure!(
@@ -587,49 +431,33 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
/* re-open copied database file */
context
.sql
.open(context, &context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(context).await?;
let total_files_cnt = context
.sql
.count(sqlx::query("SELECT COUNT(*) FROM backup_blobs;"))
.open(&context, &context.get_dbfile(), false)
.await?;
delete_and_reset_all_device_msgs(&context).await?;
let total_files_cnt: i32 = context
.sql
.query_value("SELECT COUNT(*) FROM backup_blobs;", paramsx![])
.await
.unwrap_or_default();
let total_files_cnt = total_files_cnt as usize;
info!(
context,
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
);
// Load IDs only for now, without the file contents, to avoid
// consuming too much memory.
let file_ids = context
.sql
.fetch(sqlx::query("SELECT id FROM backup_blobs ORDER BY id"))
.await?
.map(|row| row?.try_get(0))
.collect::<sqlx::Result<Vec<i64>>>()
.await?;
let pool = context.sql.get_pool().await?;
let mut files = sqlx::query_as("SELECT file_name, file_content FROM backup_blobs ORDER BY id;")
.fetch(&pool);
let mut all_files_extracted = true;
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
// Load a single blob into memory
let row = context
.sql
.fetch_one(
sqlx::query("SELECT file_name, file_content FROM backup_blobs WHERE id = ?")
.bind(file_id),
)
.await?;
let file_name: String = row.try_get(0)?;
let file_blob: &[u8] = row.try_get(1)?;
let mut processed_files_cnt = 0;
while let Some(files_result) = files.next().await {
let (file_name, file_blob): (String, Vec<u8>) = files_result?;
if context.shall_stop_ongoing().await {
all_files_extracted = false;
break;
}
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
if permille < 10 {
permille = 10
@@ -637,132 +465,142 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
if permille > 990 {
permille = 990
}
context.emit_event(EventType::ImexProgress(permille));
context.emit_event(Event::ImexProgress(permille));
if file_blob.is_empty() {
continue;
}
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, file_blob).await?;
dc_write_file(context, &path_filename, &file_blob).await?;
processed_files_cnt += 1;
}
if all_files_extracted {
// only delete backup_blobs if all files were successfully extracted
context
.sql
.execute(sqlx::query("DROP TABLE backup_blobs;"))
.await?;
context.sql.execute(sqlx::query("VACUUM;")).await.ok();
Ok(())
} else {
bail!("received stop signal");
}
ensure!(
processed_files_cnt == total_files_cnt,
"received stop signal"
);
context
.sql
.execute("DROP TABLE backup_blobs;", paramsx![])
.await?;
context.sql.execute("VACUUM;", paramsx![]).await?;
Ok(())
}
/*******************************************************************************
* Export backup
******************************************************************************/
#[allow(unused)]
/* the FILE_PROGRESS macro calls the callback with the permille of files processed.
The macro avoids weird values of 0% or 100% while still working. */
async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
// FIXME: we should write to a temporary file first and rename it on success. this would guarantee the backup is complete.
// let dest_path_filename = dc_get_next_backup_file(context, dir, res);
let now = time();
let (temp_path, dest_path) = get_next_backup_path(dir, now).await?;
let _d = DeleteOnDrop(temp_path.clone());
let dest_path_filename = dc_get_next_backup_path(dir, now).await?;
let dest_path_string = dest_path_filename.to_string_lossy().to_string();
context
.sql
.set_raw_config_int("backup_time", now as i32)
.await?;
sql::housekeeping(context).await.ok_or_log(context);
sql::housekeeping(context).await?;
context
.sql
.execute(sqlx::query("VACUUM;"))
.await
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e));
context.sql.execute("VACUUM;", paramsx![]).await.ok();
ensure!(
!context.scheduler.read().await.is_running(),
"cannot export backup, IO already running"
);
// we close the database during the export
// we close the database during the copy of the dbfile
context.sql.close().await;
info!(
context,
"Backup '{}' to '{}'.",
context.get_dbfile().display(),
dest_path.display(),
dest_path_filename.display(),
);
let res = export_backup_inner(context, &temp_path).await;
// we re-open the database after export is finished
let copied = dc_copy_file(context, context.get_dbfile(), &dest_path_filename).await;
context
.sql
.open(context, &context.get_dbfile(), false)
.await;
match &res {
Ok(_) => {
fs::rename(temp_path, &dest_path).await?;
context.emit_event(EventType::ImexFileWritten(dest_path));
}
Err(e) => {
error!(context, "backup failed: {}", e);
}
}
res
}
struct DeleteOnDrop(PathBuf);
impl Drop for DeleteOnDrop {
fn drop(&mut self) {
let file = self.0.clone();
// Not using dc_delete_file() here because it would send a DeletedBlobFile event
async_std::task::block_on(async move { fs::remove_file(file).await.ok() });
}
}
async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<()> {
let file = File::create(temp_path).await?;
let mut builder = async_tar::Builder::new(file);
// append_path_with_name() wants the source path as the first argument, append_dir_all() wants it as the second argument.
builder
.append_path_with_name(context.get_dbfile(), DBFILE_BACKUP_NAME)
.open(&context, &context.get_dbfile(), false)
.await?;
let read_dir: Vec<_> = fs::read_dir(context.get_blobdir()).await?.collect().await;
let count = read_dir.len();
let mut written_files = 0;
if !copied {
bail!(
"could not copy file from '{}' to '{}'",
context.get_dbfile().display(),
dest_path_string
);
}
let dest_sql = Sql::new();
dest_sql.open(context, &dest_path_filename, false).await?;
for entry in read_dir.into_iter() {
let res = match add_files_to_export(context, &dest_sql).await {
Ok(()) => {
dest_sql
.set_raw_config_int(context, "backup_time", now as i32)
.await?;
context.emit_event(Event::ImexFileWritten(dest_path_filename));
Ok(())
}
Err(err) => {
dc_delete_file(context, &dest_path_filename).await;
error!(context, "backup failed: {}", err);
Err(err)
}
};
dest_sql.close().await;
Ok(res?)
}
async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
// add all files as blobs to the database copy (this does not require
// the source to be locked, neigher the destination as it is used only here)
if !sql.table_exists("backup_blobs").await? {
sql.execute(
"CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);",
paramsx![],
)
.await?;
}
// copy all files from BLOBDIR into backup-db
let mut total_files_cnt = 0;
let dir = context.get_blobdir();
let dir_handle = async_std::fs::read_dir(&dir).await?;
total_files_cnt += dir_handle.filter(|r| r.is_ok()).count().await;
info!(context, "EXPORT: total_files_cnt={}", total_files_cnt);
// scan directory, pass 2: copy files
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
let mut processed_files_cnt = 0;
while let Some(entry) = dir_handle.next().await {
let entry = entry?;
let name = entry.file_name();
if !entry.file_type().await?.is_file() {
warn!(
context,
"Export: Found dir entry {} that is not a file, ignoring",
name.to_string_lossy()
);
if context.shall_stop_ongoing().await {
return Ok(());
}
processed_files_cnt += 1;
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
context.emit_event(Event::ImexProgress(permille));
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
continue;
}
let mut file = File::open(entry.path()).await?;
let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(name);
builder.append_file(path_in_archive, &mut file).await?;
written_files += 1;
let progress = 1000 * written_files / count;
if progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
emit_event!(context, EventType::ImexProgress(progress));
info!(context, "EXPORT: copying filename={}", name);
let curr_path_filename = context.get_blobdir().join(entry.file_name());
if let Ok(buf) = dc_read_file(context, &curr_path_filename).await {
if buf.is_empty() {
continue;
}
// bail out if we can't insert
sql.execute(
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
paramsx![name.as_ref(), buf],
)
.await?;
}
}
builder.finish().await?;
Ok(())
}
@@ -801,12 +639,6 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
continue;
}
}
info!(
context,
"considering key file: {}",
path_plus_name.display()
);
match dc_read_file(context, &path_plus_name).await {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
@@ -830,27 +662,19 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let mut export_errors = 0;
let mut keys = context
.sql
.fetch(sqlx::query(
"SELECT id, public_key, private_key, is_default FROM keypairs;",
))
.await?
.map(|row| -> sqlx::Result<_> {
let row = row?;
let id = row.try_get(0)?;
let public_key_blob: &[u8] = row.try_get(1)?;
let public_key = SignedPublicKey::from_slice(public_key_blob);
let private_key_blob: &[u8] = row.try_get(2)?;
let private_key = SignedSecretKey::from_slice(private_key_blob);
let is_default: i32 = row.try_get(3)?;
let pool = context.sql.get_pool().await?;
Ok((id, public_key, private_key, is_default))
});
let mut keys = sqlx::query_as("SELECT id, public_key, private_key, is_default FROM keypairs;")
.fetch(&pool);
while let Some(keys_result) = keys.next().await {
let (id, public_key_blob, private_key_blob, is_default): (i64, Vec<u8>, Vec<u8>, i32) =
keys_result?;
let public_key = SignedPublicKey::from_slice(&public_key_blob);
let private_key = SignedSecretKey::from_slice(&private_key_blob);
while let Some(parts) = keys.next().await {
let (id, public_key, private_key, is_default) = parts?;
let id = Some(id).filter(|_| is_default != 0);
if let Ok(key) = public_key {
if export_key_to_asc_file(context, &dir, id, &key)
.await
@@ -874,6 +698,7 @@ async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
}
ensure!(export_errors == 0, "errors while exporting keys");
Ok(())
}
@@ -893,7 +718,7 @@ where
let any_key = key as &dyn Any;
let kind = if any_key.downcast_ref::<SignedPublicKey>().is_some() {
"public"
} else if any_key.downcast_ref::<SignedSecretKey>().is_some() {
} else if any_key.downcast_ref::<SignedPublicKey>().is_some() {
"private"
} else {
"unknown"
@@ -901,12 +726,7 @@ where
let id = id.map_or("default".into(), |i| i.to_string());
dir.as_ref().join(format!("{}-key-{}.asc", kind, &id))
};
info!(
context,
"Exporting key {:?} to {}",
key.key_id(),
file_name.display()
);
info!(context, "Exporting key {}", file_name.display());
dc_delete_file(context, &file_name).await;
let content = key.to_asc(None).into_bytes();
@@ -914,7 +734,7 @@ where
if res.is_err() {
error!(context, "Cannot write key to {}", file_name.display());
} else {
context.emit_event(EventType::ImexFileWritten(file_name));
context.emit_event(Event::ImexFileWritten(file_name));
}
res
}
@@ -922,11 +742,8 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::stock_str::StockMessage;
use crate::test_utils::{alice_keypair, TestContext};
use crate::test_utils::*;
use ::pgp::armor::BlockType;
#[async_std::test]
@@ -934,7 +751,7 @@ mod tests {
let t = TestContext::new().await;
t.configure_alice().await;
let msg = render_setup_file(&t, "hello").await.unwrap();
let msg = render_setup_file(&t.ctx, "hello").await.unwrap();
println!("{}", &msg);
// Check some substrings, indicating things got substituted.
// In particular note the mixing of `\r\n` and `\n` depending
@@ -951,11 +768,12 @@ mod tests {
#[async_std::test]
async fn test_render_setup_file_newline_replace() {
let t = TestContext::new().await;
t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
t.ctx
.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.await
.unwrap();
t.configure_alice().await;
let msg = render_setup_file(&t, "pw").await.unwrap();
let msg = render_setup_file(&t.ctx, "pw").await.unwrap();
println!("{}", &msg);
assert!(msg.contains("<p>hello<br>there</p>"));
}
@@ -963,7 +781,7 @@ mod tests {
#[async_std::test]
async fn test_create_setup_code() {
let t = TestContext::new().await;
let setupcode = create_setup_code(&t);
let setupcode = create_setup_code(&t.ctx);
assert_eq!(setupcode.len(), 44);
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
assert_eq!(setupcode.chars().nth(9).unwrap(), '-');
@@ -976,7 +794,7 @@ mod tests {
}
#[async_std::test]
async fn test_export_public_key_to_asc_file() {
async fn test_export_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().public;
let blobdir = "$BLOBDIR";
@@ -990,37 +808,6 @@ mod tests {
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[async_std::test]
async fn test_export_private_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().secret;
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
.await
.is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{}/private-key-default.asc", blobdir);
let bytes = async_std::fs::read(&filename).await.unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[async_std::test]
async fn test_export_and_import_key() {
let context = TestContext::new().await;
context.configure_alice().await;
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await {
panic!("got error on export: {:?}", err);
}
let context2 = TestContext::new().await;
context2.configure_alice().await;
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir).await {
panic!("got error on import: {:?}", err);
}
}
#[test]
fn test_normalize_setup_code() {
let norm = normalize_setup_code("123422343234423452346234723482349234");

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,10 @@ use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use sqlx::Row;
use thiserror::Error;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::{time, EmailAddress, InvalidEmailError};
use crate::sql;
@@ -42,10 +41,6 @@ pub enum Error {
InvalidConfiguredAddr(#[from] InvalidEmailError),
#[error("no data provided")]
Empty,
#[error("db: {}", _0)]
Sql(#[from] sqlx::Error),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -121,23 +116,25 @@ impl DcKey for SignedPublicKey {
type KeyType = SignedPublicKey;
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
let res: std::result::Result<Vec<u8>, _> = context
.sql
.fetch_optional(sqlx::query(
.query_value(
r#"
SELECT public_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
))
.await?
{
Some(row) => Self::from_slice(row.try_get(0)?),
None => {
paramsx![],
)
.await;
match res {
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
}
Err(err) => Err(err.into()),
}
}
@@ -163,23 +160,25 @@ impl DcKey for SignedSecretKey {
type KeyType = SignedSecretKey;
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
let res: std::result::Result<Vec<u8>, _> = context
.sql
.fetch_optional(sqlx::query(
.query_value(
r#"
SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
))
.await?
{
Some(row) => Self::from_slice(row.try_get(0)?),
None => {
paramsx![],
)
.await;
match res {
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
let keypair = generate_keypair(context).await?;
Ok(keypair.secret)
}
Err(err) => Err(err.into()),
}
}
@@ -220,35 +219,33 @@ impl DcSecretKey for SignedSecretKey {
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.await?
.ok_or(Error::NoConfiguredAddr)?;
.await
.ok_or_else(|| Error::NoConfiguredAddr)?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().await;
// Check if the key appeared while we were waiting on the lock.
match context
let res: std::result::Result<(Vec<u8>, Vec<u8>), _> = context
.sql
.fetch_optional(
sqlx::query(
r#"
.query_row(
r#"
SELECT public_key, private_key
FROM keypairs
WHERE addr=?1
WHERE addr=?
AND is_default=1;
"#,
)
.bind(addr.to_string()),
paramsx![addr.to_string()],
)
.await?
{
Some(row) => Ok(KeyPair {
.await;
match res {
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
addr,
public: SignedPublicKey::from_slice(row.try_get(0)?)?,
secret: SignedSecretKey::from_slice(row.try_get(1)?)?,
public: SignedPublicKey::from_slice(&pub_bytes)?,
secret: SignedSecretKey::from_slice(&sec_bytes)?,
}),
None => {
let start = std::time::SystemTime::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
let start = std::time::Instant::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
.unwrap_or_default();
info!(context, "Generating keypair with type {}", keytype);
let keypair =
@@ -258,10 +255,11 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().unwrap_or_default().as_secs()
start.elapsed().as_secs()
);
Ok(keypair)
}
Err(err) => Err(err.into()),
}
}
@@ -319,16 +317,15 @@ pub async fn store_self_keypair(
context
.sql
.execute(
sqlx::query("DELETE FROM keypairs WHERE public_key=? OR private_key=?;")
.bind(&public_key)
.bind(&secret_key),
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
paramsx![&public_key, &secret_key],
)
.await
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute(sqlx::query("UPDATE keypairs SET is_default=0;"))
.execute("UPDATE keypairs SET is_default=0;", paramsx![])
.await
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
@@ -340,18 +337,13 @@ pub async fn store_self_keypair(
let addr = keypair.addr.to_string();
let t = time();
let params = paramsx![addr, is_default, public_key, secret_key, t];
context
.sql
.execute(
sqlx::query(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
VALUES (?,?,?,?,?);",
)
.bind(addr)
.bind(is_default)
.bind(&public_key)
.bind(&secret_key)
.bind(t),
params,
)
.await
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
@@ -360,7 +352,7 @@ pub async fn store_self_keypair(
}
/// A key fingerprint
#[derive(Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Fingerprint(Vec<u8>);
impl Fingerprint {
@@ -380,14 +372,6 @@ impl Fingerprint {
}
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Fingerprint")
.field("hex", &self.hex())
.finish()
}
}
/// Make a human-readable fingerprint.
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -412,7 +396,7 @@ impl std::str::FromStr for Fingerprint {
let hex_repr: String = input
.to_uppercase()
.chars()
.filter(|&c| ('0'..='9').contains(&c) || ('A'..='F').contains(&c))
.filter(|&c| c >= '0' && c <= '9' || c >= 'A' && c <= 'F')
.collect();
let v: Vec<u8> = hex::decode(hex_repr)?;
let fp = Fingerprint::new(v)?;
@@ -431,14 +415,16 @@ pub enum FingerprintError {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{alice_keypair, TestContext};
use crate::test_utils::*;
use std::error::Error;
use async_std::sync::Arc;
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
lazy_static! {
static ref KEYPAIR: KeyPair = alice_keypair();
}
#[test]
fn test_from_armored_string() {
@@ -540,11 +526,11 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
#[test]
fn test_from_slice_bad_data() {
let mut bad_data: [u8; 4096] = [0; 4096];
for (i, v) in bad_data.iter_mut().enumerate() {
*v = (i & 0xff) as u8;
for i in 0..4096 {
bad_data[i] = (i & 0xff) as u8;
}
for j in 0..(4096 / 40) {
let slice = &bad_data.get(j..j + 4096 / 2 + j).unwrap();
let slice = &bad_data[j..j + 4096 / 2 + j];
assert!(SignedPublicKey::from_slice(slice).is_err());
assert!(SignedSecretKey::from_slice(slice).is_err());
}
@@ -563,29 +549,31 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
let alice = alice_keypair();
let t = TestContext::new().await;
t.configure_alice().await;
let pubkey = SignedPublicKey::load_self(&t).await.unwrap();
let pubkey = SignedPublicKey::load_self(&t.ctx).await.unwrap();
assert_eq!(alice.public, pubkey);
let seckey = SignedSecretKey::load_self(&t).await.unwrap();
let seckey = SignedSecretKey::load_self(&t.ctx).await.unwrap();
assert_eq!(alice.secret, seckey);
}
#[async_std::test]
async fn test_load_self_generate_public() {
let t = TestContext::new().await;
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap();
let key = SignedPublicKey::load_self(&t).await;
let key = SignedPublicKey::load_self(&t.ctx).await;
assert!(key.is_ok());
}
#[async_std::test]
async fn test_load_self_generate_secret() {
let t = TestContext::new().await;
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap();
let key = SignedSecretKey::load_self(&t).await;
let key = SignedSecretKey::load_self(&t.ctx).await;
assert!(key.is_ok());
}
@@ -594,17 +582,17 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
use std::thread;
let t = TestContext::new().await;
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap();
let thr0 = {
let ctx = t.clone();
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx)))
};
let thr1 = {
let ctx = t.clone();
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx)))
};
let ctx = t.ctx.clone();
let ctx0 = ctx.clone();
let thr0 =
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx0)));
let ctx1 = ctx.clone();
let thr1 =
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx1)));
let res0 = thr0.join().unwrap();
let res1 = thr1.join().unwrap();
assert_eq!(res0.unwrap(), res1.unwrap());
@@ -621,13 +609,16 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
// Saving the same key twice should result in only one row in
// the keypairs table.
let t = TestContext::new().await;
let ctx = Arc::new(t);
let ctx = Arc::new(t.ctx);
let ctx1 = ctx.clone();
let nrows = || async {
ctx.sql
.count(sqlx::query("SELECT COUNT(*) FROM keypairs;"))
let val: i32 = ctx1
.sql
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
.await
.unwrap()
.unwrap();
val as usize
};
assert_eq!(nrows().await, 0);
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)

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