mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 13:32:11 +03:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0743459001 | ||
|
|
9ea57e5862 | ||
|
|
a3a918a0ea | ||
|
|
5e555c6f9d | ||
|
|
799c56b492 | ||
|
|
1019a93991 | ||
|
|
e5ff196e27 | ||
|
|
063a10294e | ||
|
|
60863c4f91 | ||
|
|
81fab2d783 | ||
|
|
80a1884f00 | ||
|
|
9286ea8174 | ||
|
|
2601235f82 | ||
|
|
beb134edaa | ||
|
|
27b4cb084e | ||
|
|
794abd5bf6 | ||
|
|
d367beea6f | ||
|
|
468e749651 | ||
|
|
4c0aa78633 | ||
|
|
2bf27dd5cd | ||
|
|
94ec142044 | ||
|
|
ecded4fd18 | ||
|
|
63dd3c91e1 | ||
|
|
8729b9f403 | ||
|
|
d1b93f6978 | ||
|
|
82c3352b27 | ||
|
|
dc065ccbe3 | ||
|
|
f7d6230a97 | ||
|
|
41bcb2dcbb | ||
|
|
85970a146a | ||
|
|
9912dd45b9 | ||
|
|
dafe900d22 | ||
|
|
a860758f8a | ||
|
|
0202ed7ca8 | ||
|
|
aace6bad2f | ||
|
|
62f424452a | ||
|
|
c43f6964c5 | ||
|
|
0131980372 | ||
|
|
04c90e2d87 | ||
|
|
b4c412ee68 | ||
|
|
74fbd4fd16 | ||
|
|
72d95075a0 | ||
|
|
39364d1f6c | ||
|
|
f3b9f671ba | ||
|
|
e054a49198 | ||
|
|
e66ca5b018 | ||
|
|
0520ec8ab7 | ||
|
|
b9d3e6b342 | ||
|
|
f39abd6d51 | ||
|
|
4227dec127 | ||
|
|
29d4197340 | ||
|
|
11b369db0f | ||
|
|
0b2bce8334 | ||
|
|
b6a48ad39b | ||
|
|
bf8e83d816 | ||
|
|
6594fdf33a | ||
|
|
71c7b30db7 | ||
|
|
ada46b8f25 | ||
|
|
dcf6a41239 | ||
|
|
94035d6286 | ||
|
|
e53c88ecb8 | ||
|
|
2cbf2d8f65 | ||
|
|
60b3550952 | ||
|
|
35542189d8 | ||
|
|
3bde37eabf | ||
|
|
632416cf58 | ||
|
|
861325591e | ||
|
|
06166f7956 | ||
|
|
bb2e8b4392 | ||
|
|
8895dc36c7 | ||
|
|
017bdc88dd | ||
|
|
142225f0f4 | ||
|
|
f9befa8f39 | ||
|
|
1c73021d77 | ||
|
|
933b14eedf | ||
|
|
650bd822bf | ||
|
|
37943d3d16 | ||
|
|
6067d40a6f | ||
|
|
cde587fefa | ||
|
|
fc12beda24 | ||
|
|
ccebca5f99 | ||
|
|
e07869ae95 | ||
|
|
90be708791 | ||
|
|
a27b379ce0 | ||
|
|
f461e2a2fd | ||
|
|
40dc72b2b1 | ||
|
|
99babcc4bd | ||
|
|
6e6823f395 | ||
|
|
964f60ff4b | ||
|
|
7624e574bb | ||
|
|
667364b90e | ||
|
|
ef954ed99e | ||
|
|
7bb6890f26 | ||
|
|
2aa808756e | ||
|
|
81a2e510f5 | ||
|
|
82a3af97df | ||
|
|
f1b3527ad0 | ||
|
|
6902250d6b | ||
|
|
64ab86a1a6 | ||
|
|
4b445b7dd7 | ||
|
|
6cb75114c1 | ||
|
|
d54ade5891 | ||
|
|
0da21aa9f6 | ||
|
|
1e84e81e7d | ||
|
|
d3eb209d27 | ||
|
|
49a6a5b23c | ||
|
|
4f78e2e14e | ||
|
|
7da69a4644 | ||
|
|
8efe7cade7 | ||
|
|
18e4abc1df | ||
|
|
ee7b7eb4f2 | ||
|
|
4378fe21ee | ||
|
|
b50410ab15 | ||
|
|
9f7567c1d1 | ||
|
|
68e3bce60e | ||
|
|
86bc54508f | ||
|
|
ae2fd4014a | ||
|
|
2c23433185 | ||
|
|
3f2e67f07a | ||
|
|
06a4f15995 | ||
|
|
e2c532704a | ||
|
|
baa0dffdfd | ||
|
|
f28a0db7d0 | ||
|
|
e5d5009d6a | ||
|
|
2071478e11 | ||
|
|
797375ff43 | ||
|
|
18045c9c14 | ||
|
|
14d09ce75f | ||
|
|
0ae8663eed | ||
|
|
d1ec0e2de6 | ||
|
|
7f8f871813 | ||
|
|
43c4816739 | ||
|
|
1b5d08e6ee | ||
|
|
bb9603661a | ||
|
|
7d08397b48 | ||
|
|
3df0ef50a4 | ||
|
|
6a99e31de4 | ||
|
|
d9314227ee | ||
|
|
6050f0e2a1 | ||
|
|
d4dea0d5c6 | ||
|
|
0b187131b2 | ||
|
|
5a28b669f9 | ||
|
|
d59475f9bb | ||
|
|
db6623d0cf | ||
|
|
059caee527 | ||
|
|
97599bd78e | ||
|
|
d6b30c9703 | ||
|
|
7a7dcc8b8f | ||
|
|
d79c918c9e | ||
|
|
56518420bc | ||
|
|
615a76f35e | ||
|
|
0c47489a3b | ||
|
|
f931a905a7 | ||
|
|
7d048ac419 | ||
|
|
41fe3db79d | ||
|
|
42f6a7c77c | ||
|
|
09833eb74d | ||
|
|
2c11df46a7 | ||
|
|
443ad04f46 | ||
|
|
f2d09cc51e | ||
|
|
83dde57afa | ||
|
|
fdacf98b69 |
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.43.1
|
||||
toolchain: 1.45.0
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
@@ -32,21 +32,34 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.43.1
|
||||
toolchain: 1.45.0
|
||||
components: clippy
|
||||
override: true
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all
|
||||
|
||||
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
rust: [nightly, 1.43.1]
|
||||
rust: [1.45.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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
@@ -79,11 +92,10 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --workspace --all --bins --examples --tests
|
||||
args: --all --bins --examples --tests
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace
|
||||
|
||||
args: --all
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
|
||||
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
|
||||
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -1,5 +1,64 @@
|
||||
# Changelog
|
||||
|
||||
## 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 #1756
|
||||
|
||||
- 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
|
||||
|
||||
1031
Cargo.lock
generated
1031
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.39.0"
|
||||
version = "1.42.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
#lto = true
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
@@ -20,7 +20,7 @@ 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"
|
||||
async-smtp = { version = "0.3" }
|
||||
async-smtp = "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"
|
||||
@@ -36,12 +36,14 @@ indexmap = "1.3.0"
|
||||
kamadak-exif = "0.5"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.1.6"
|
||||
rusqlite = { version = "0.23", features = ["bundled"] }
|
||||
r2d2_sqlite = "0.16.0"
|
||||
r2d2 = "0.8.5"
|
||||
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"
|
||||
itertools = "0.9.0"
|
||||
quick-xml = "0.18.1"
|
||||
escaper = "0.1.0"
|
||||
bitflags = "1.1.0"
|
||||
@@ -57,22 +59,20 @@ anyhow = "1.0.28"
|
||||
async-trait = "0.1.31"
|
||||
url = "2.1.1"
|
||||
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 }
|
||||
log = {version = "0.4.8", optional = true }
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
pretty_assertions = "0.6.1"
|
||||
pretty_env_logger = "0.4.0"
|
||||
proptest = "0.10"
|
||||
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
|
||||
smol = "0.1.11"
|
||||
log = "0.4.8"
|
||||
smol = "0.1.10"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
@@ -97,3 +97,4 @@ internals = []
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
|
||||
@@ -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 nightly
|
||||
# Install Rust
|
||||
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.43.1-x86_64-unknown-linux-gnu -y
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.45.0-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.43.1-x86_64-unknown-linux-gnu/share
|
||||
rm -rf /root/.rustup/toolchains/1.45.0-x86_64-unknown-linux-gnu/share
|
||||
|
||||
@@ -15,6 +15,7 @@ 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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.39.0"
|
||||
version = "1.42.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -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("".to_string()),
|
||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
|
||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||
libs_priv = libs_priv,
|
||||
prefix = env::var("PREFIX").unwrap_or("/usr/local".to_string()),
|
||||
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
|
||||
@@ -318,6 +318,11 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* The library uses the `media_quality` setting to use different defaults
|
||||
* for recoding images sent with type DC_MSG_IMAGE.
|
||||
* If needed, recoding other file types is up to the UI.
|
||||
* - `webrtc_instance` = webrtc instance to use for videochats in the form
|
||||
* `[basicwebrtc:]https://example.com/subdir#roomname=$ROOM`
|
||||
* if the url is prefixed by `basicwebrtc`, the server is assumed to be of the type
|
||||
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
|
||||
* If no type is prefixed, the videochat is handled completely in a browser.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -370,12 +375,13 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
|
||||
|
||||
|
||||
/**
|
||||
* Set configuration values from a QR code containing an account.
|
||||
* Set configuration values from a QR code.
|
||||
* Before this function is called, dc_check_qr() should confirm the type of the
|
||||
* QR code is DC_QR_ACCOUNT.
|
||||
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* Internally, the function will call dc_set_config()
|
||||
* at least with the keys `addr` and `mail_pw`.
|
||||
* Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
* eg. `addr` and `mail_pw` for DC_QR_ACCOUNT
|
||||
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object
|
||||
@@ -834,6 +840,42 @@ uint32_t dc_send_msg_sync (dc_context_t* context, uint32
|
||||
uint32_t dc_send_text_msg (dc_context_t* context, uint32_t chat_id, const char* text_to_send);
|
||||
|
||||
|
||||
/**
|
||||
* Send invitation to a videochat.
|
||||
*
|
||||
* This function reads the `webrtc_instance` config value,
|
||||
* may check that the server is working in some way
|
||||
* and creates a unique room for this chat, if needed doing a TOKEN roundtrip for that.
|
||||
*
|
||||
* After that, the function sends out a message that contains information to join the room:
|
||||
*
|
||||
* - To allow non-delta-clients to join the chat,
|
||||
* the message contains a text-area with some descriptive text
|
||||
* and a url that can be opened in a supported browser to join the videochat
|
||||
*
|
||||
* - delta-clients can get all information needed from
|
||||
* the message object, using eg.
|
||||
* dc_msg_get_videochat_url() and check dc_msg_get_viewtype() for #DC_MSG_VIDEOCHAT_INVITATION
|
||||
*
|
||||
* dc_send_videochat_invitation() is blocking and may take a while,
|
||||
* so the UIs will typically call the function from within a thread.
|
||||
* Moreover, UIs will typically enter the room directly without an additional click on the message,
|
||||
* for this purpose, the function returns the message-id directly.
|
||||
*
|
||||
* As for other messages sent, this function
|
||||
* sends the event #DC_EVENT_MSGS_CHANGED on succcess, the message has a delivery state, and so on.
|
||||
* The recipient will get noticed by the call as usual by #DC_EVENT_INCOMING_MSG or #DC_EVENT_MSGS_CHANGED,
|
||||
* However, UIs might some things differently, eg. play a different sound.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat to start a videochat for.
|
||||
* @return The id if the message sent out
|
||||
* or 0 for errors.
|
||||
*/
|
||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Save a draft for a chat in the database.
|
||||
*
|
||||
@@ -977,6 +1019,7 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
* @param chat_id The chat ID of which the messages IDs should be queried.
|
||||
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
|
||||
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
||||
* To get the concrete time of the marker, use dc_array_get_timestamp().
|
||||
* @param marker1before An optional message ID. If set, the id DC_MSG_ID_MARKER1 will be added just
|
||||
* before the given ID in the returned array. Set this to 0 if you do not want this behaviour.
|
||||
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
@@ -1171,6 +1214,16 @@ void dc_delete_chat (dc_context_t* context, uint32_t ch
|
||||
*/
|
||||
dc_array_t* dc_get_chat_contacts (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
/**
|
||||
* Get the chat's ephemeral message timer.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param chat_id The chat ID.
|
||||
*
|
||||
* @return ephemeral timer value in seconds, 0 if the timer is disabled or if there is an error
|
||||
*/
|
||||
uint32_t dc_get_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
/**
|
||||
* Search messages containing the given query string.
|
||||
@@ -1303,6 +1356,21 @@ int dc_remove_contact_from_chat (dc_context_t* context, uint32_t ch
|
||||
*/
|
||||
int dc_set_chat_name (dc_context_t* context, uint32_t chat_id, const char* name);
|
||||
|
||||
/**
|
||||
* Set the chat's ephemeral message timer.
|
||||
*
|
||||
* This timer is applied to all messages in a chat and starts when the
|
||||
* message is read. The setting is synchronized to all clients
|
||||
* participating in a chat.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param chat_id The chat ID to set the ephemeral message timer for.
|
||||
* @param timer The timer value in seconds or 0 to disable the timer.
|
||||
*
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32_t timer);
|
||||
|
||||
/**
|
||||
* Set group profile image.
|
||||
@@ -1860,6 +1928,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
#define DC_QR_URL 332 // text1=URL
|
||||
@@ -1878,6 +1947,9 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID
|
||||
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
|
||||
* - DC_QR_ACCOUNT allows creation of an account, dc_lot_t::text1=domain
|
||||
* - DC_QR_WEBRTC_INSTANCE - a shared webrtc-instance
|
||||
* that will be set if dc_set_config_from_qr() is called with the qr-code,
|
||||
* dc_lot_t::text1=domain could be used to ask the user
|
||||
* - DC_QR_ADDR with dc_lot_t::id=Contact ID
|
||||
* - DC_QR_TEXT with dc_lot_t::text1=Text
|
||||
* - DC_QR_URL with dc_lot_t::text1=URL
|
||||
@@ -2285,17 +2357,6 @@ int dc_array_is_independent (const dc_array_t* array, size_t in
|
||||
int dc_array_search_id (const dc_array_t* array, uint32_t needle, size_t* ret_index);
|
||||
|
||||
|
||||
/**
|
||||
* Get raw pointer to the data.
|
||||
*
|
||||
* @memberof dc_array_t
|
||||
* @param array The array object.
|
||||
* @return Raw pointer to the array. You MUST NOT free the data. You MUST NOT access the data beyond the current item count.
|
||||
* It is not possible to enlarge the array this way. Calling any other dc_array*()-function may discard the returned pointer.
|
||||
*/
|
||||
const uint32_t* dc_array_get_raw (const dc_array_t* array);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_chatlist_t
|
||||
*
|
||||
@@ -2414,6 +2475,28 @@ uint32_t dc_chatlist_get_msg_id (const dc_chatlist_t* chatlist, siz
|
||||
dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, size_t index, dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Create a chatlist summary item when the chatlist object is already unref()'d.
|
||||
*
|
||||
* This function is similar to dc_chatlist_get_summary(), however,
|
||||
* takes the chat-id and message-id as returned by dc_chatlist_get_chat_id() and dc_chatlist_get_msg_id()
|
||||
* as arguments. The chatlist object itself is not needed directly.
|
||||
*
|
||||
* This maybe useful if you convert the complete object into a different represenation
|
||||
* as done eg. in the node-bindings.
|
||||
* If you have access to the chatlist object in some way, using this function is not recommended,
|
||||
* use dc_chatlist_get_summary() in this case instead.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new()
|
||||
* @param chat_id Chat to get a summary for.
|
||||
* @param msg_id Messasge to get a summary for.
|
||||
* @return The summary as an dc_lot_t object, see dc_chatlist_get_summary() for details.
|
||||
* Must be freed using dc_lot_unref(). NULL is never returned.
|
||||
*/
|
||||
dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t chat_id, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to get the associated context object.
|
||||
*
|
||||
@@ -3011,6 +3094,32 @@ int dc_msg_get_duration (const dc_msg_t* msg);
|
||||
int dc_msg_get_showpadlock (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get ephemeral timer duration for message.
|
||||
*
|
||||
* To check if the timer is started and calculate remaining time,
|
||||
* use dc_msg_get_ephemeral_timestamp().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Duration in seconds, or 0 if no timer is set.
|
||||
*/
|
||||
uint32_t dc_msg_get_ephemeral_timer (const dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* Get timestamp of ephemeral message removal.
|
||||
*
|
||||
* If returned value is non-zero, you can calculate the * fraction of
|
||||
* time remaining by divinding the difference between the current timestamp
|
||||
* and this timestamp by dc_msg_get_ephemeral_timer().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Time of message removal, 0 if the timer is not started.
|
||||
*/
|
||||
int64_t dc_msg_get_ephemeral_timestamp (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get a summary for a message.
|
||||
*
|
||||
@@ -3194,6 +3303,57 @@ int dc_msg_is_setupmessage (const dc_msg_t* msg);
|
||||
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get url of a videochat invitation.
|
||||
*
|
||||
* Videochat invitations are sent out using dc_send_videochat_invitation()
|
||||
* and dc_msg_get_viewtype() returns #DC_MSG_VIDEOCHAT_INVITATION for such invitations.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return If the message contains a videochat invitation,
|
||||
* the url of the invitation is returned.
|
||||
* If the message is no videochat invitation, NULL is returned.
|
||||
* Must be released using dc_str_unref() when done.
|
||||
*/
|
||||
char* dc_msg_get_videochat_url (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get type of videochat.
|
||||
*
|
||||
* Calling this functions only makes sense for messages of type #DC_MSG_VIDEOCHAT_INVITATION,
|
||||
* in this case, if "basic webrtc" as of https://github.com/cracker0dks/basicwebrtc was used to initiate the videochat,
|
||||
* dc_msg_get_videochat_type() returns DC_VIDEOCHATTYPE_BASICWEBRTC.
|
||||
* "basic webrtc" videochat may be processed natively by the app
|
||||
* whereas for other urls just the browser is opened.
|
||||
*
|
||||
* The videochat-url can be retrieved using dc_msg_get_videochat_url().
|
||||
* To check if a message is a videochat invitation at all, check the message type for #DC_MSG_VIDEOCHAT_INVITATION.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return Type of the videochat as of DC_VIDEOCHATTYPE_BASICWEBRTC or DC_VIDEOCHATTYPE_UNKNOWN.
|
||||
*
|
||||
* Example:
|
||||
* ~~~
|
||||
* if (dc_msg_get_viewtype(msg) == DC_MSG_VIDEOCHAT_INVITATION) {
|
||||
* if (dc_msg_get_videochat_type(msg) == DC_VIDEOCHATTYPE_BASICWEBRTC) {
|
||||
* // videochat invitation that we ship a client for
|
||||
* } else {
|
||||
* // use browser for videochat, just open the url
|
||||
* }
|
||||
* } else {
|
||||
* // not a videochat invitation
|
||||
* }
|
||||
* ~~~
|
||||
*/
|
||||
int dc_msg_get_videochat_type (const dc_msg_t* msg);
|
||||
|
||||
#define DC_VIDEOCHATTYPE_UNKNOWN 0
|
||||
#define DC_VIDEOCHATTYPE_BASICWEBRTC 1
|
||||
|
||||
|
||||
/**
|
||||
* Set the text of a message object.
|
||||
* This does not alter any information in the database; this may be done by dc_send_msg() later.
|
||||
@@ -3729,6 +3889,18 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
#define DC_MSG_FILE 60
|
||||
|
||||
|
||||
/**
|
||||
* Message indicating an incoming or outgoing videochat.
|
||||
* The message was created via dc_send_videochat_invitation() on this or a remote device.
|
||||
*
|
||||
* Typically, such messages are rendered differently by the UIs,
|
||||
* eg. contain a button to join the videochat.
|
||||
* The url for joining can be retrieved using dc_msg_get_videochat_url().
|
||||
*/
|
||||
#define DC_MSG_VIDEOCHAT_INVITATION 70
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -4086,14 +4258,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
* 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
|
||||
* instead of the string from data2.
|
||||
*
|
||||
* @param data1 (int) 1=first/new network error, should be reported the user;
|
||||
* 0=subsequent network error, should be logged only
|
||||
* @param data1 0
|
||||
* @param data2 (char*) Error string, always set, never NULL.
|
||||
*/
|
||||
#define DC_EVENT_ERROR_NETWORK 401
|
||||
@@ -4179,6 +4349,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_CHAT_MODIFIED 2020
|
||||
|
||||
/**
|
||||
* Chat ephemeral timer changed.
|
||||
*/
|
||||
#define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, verified, blocked or deleted.
|
||||
@@ -4461,8 +4636,17 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_STR_UNKNOWN_SENDER_FOR_CHAT 72
|
||||
#define DC_STR_SUBJECT_FOR_NEW_CONTACT 73
|
||||
#define DC_STR_FAILED_SENDING_TO 74
|
||||
#define DC_STR_EPHEMERAL_DISABLED 75
|
||||
#define DC_STR_EPHEMERAL_SECONDS 76
|
||||
#define DC_STR_EPHEMERAL_MINUTE 77
|
||||
#define DC_STR_EPHEMERAL_HOUR 78
|
||||
#define DC_STR_EPHEMERAL_DAY 79
|
||||
#define DC_STR_EPHEMERAL_WEEK 80
|
||||
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
|
||||
#define DC_STR_VIDEOCHAT_INVITATION 82
|
||||
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
|
||||
|
||||
#define DC_STR_COUNT 74
|
||||
#define DC_STR_COUNT 83
|
||||
|
||||
/*
|
||||
* @}
|
||||
|
||||
@@ -1,46 +1,56 @@
|
||||
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 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 {
|
||||
pub(crate) 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] as u32,
|
||||
Self::Uint(array) => array[index],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_location(&self, index: usize) -> &Location {
|
||||
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 {
|
||||
if let Self::Locations(array) = self {
|
||||
&array[index]
|
||||
} else {
|
||||
@@ -48,55 +58,18 @@ impl dc_array_t {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
pub(crate) 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 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");
|
||||
}
|
||||
pub(crate) fn search_id(&self, needle: u32) -> Option<usize> {
|
||||
(0..self.len()).find(|i| self.get_id(*i) == needle)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +79,18 @@ 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)
|
||||
@@ -118,12 +103,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_dc_array() {
|
||||
let mut arr = dc_array_t::new(7);
|
||||
assert!(arr.is_empty());
|
||||
let arr: dc_array_t = Vec::<u32>::new().into();
|
||||
assert!(arr.len() == 0);
|
||||
|
||||
for i in 0..1000 {
|
||||
arr.add_id(i + 2);
|
||||
}
|
||||
let ids: Vec<u32> = (2..1002).collect();
|
||||
let arr: dc_array_t = ids.into();
|
||||
|
||||
assert_eq!(arr.len(), 1000);
|
||||
|
||||
@@ -131,31 +115,15 @@ mod tests {
|
||||
assert_eq!(arr.get_id(i), (i + 2) as u32);
|
||||
}
|
||||
|
||||
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);
|
||||
assert_eq!(arr.search_id(10), Some(8));
|
||||
assert_eq!(arr.search_id(1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_dc_array_out_of_bounds() {
|
||||
let mut arr = dc_array_t::new(7);
|
||||
for i in 0..1000 {
|
||||
arr.add_id(i + 2);
|
||||
}
|
||||
let ids: Vec<u32> = (2..1002).collect();
|
||||
let arr: dc_array_t = ids.into();
|
||||
arr.get_id(1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![deny(clippy::all)]
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
@@ -27,6 +28,7 @@ use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::stock::StockMessage;
|
||||
@@ -36,6 +38,7 @@ mod dc_array;
|
||||
|
||||
mod string;
|
||||
use self::string::*;
|
||||
use deltachat::chatlist::Chatlist;
|
||||
|
||||
// as C lacks a good and portable error handling,
|
||||
// in general, the C Interface is forgiving wrt to bad parameters.
|
||||
@@ -349,7 +352,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| Event::MsgDelivered { chat_id, .. }
|
||||
| Event::MsgFailed { chat_id, .. }
|
||||
| Event::MsgRead { chat_id, .. }
|
||||
| Event::ChatModified(chat_id) => chat_id.to_u32().unwrap_or_default() as libc::c_int,
|
||||
| Event::ChatModified(chat_id)
|
||||
| Event::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
|
||||
let id = id.unwrap_or_default();
|
||||
id as libc::c_int
|
||||
@@ -396,9 +400,10 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| Event::IncomingMsg { msg_id, .. }
|
||||
| Event::MsgDelivered { msg_id, .. }
|
||||
| Event::MsgFailed { msg_id, .. }
|
||||
| Event::MsgRead { msg_id, .. } => msg_id.to_u32().unwrap_or_default() as libc::c_int,
|
||||
| Event::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
Event::SecurejoinInviterProgress { progress, .. }
|
||||
| Event::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||
Event::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +444,8 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| Event::ConfigureProgress(_)
|
||||
| Event::ImexProgress(_)
|
||||
| Event::SecurejoinInviterProgress { .. }
|
||||
| Event::SecurejoinJoinerProgress { .. } => ptr::null_mut(),
|
||||
| Event::SecurejoinJoinerProgress { .. }
|
||||
| Event::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
Event::ImexFileWritten(file) => {
|
||||
let data2 = file.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
@@ -482,7 +488,7 @@ pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *
|
||||
events
|
||||
.recv_sync()
|
||||
.map(|ev| Box::into_raw(Box::new(ev)))
|
||||
.unwrap_or_else(|| ptr::null_mut())
|
||||
.unwrap_or_else(ptr::null_mut)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -705,6 +711,25 @@ pub unsafe extern "C" fn dc_send_text_msg(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_videochat_invitation()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(&ctx, "Failed to send video chat invitation")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -830,15 +855,11 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
let arr = dc_array_t::from(
|
||||
Box::into_raw(Box::new(
|
||||
chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed get_chat_msgs")
|
||||
.into_iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
.into(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -899,7 +920,7 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
|
||||
let arr = dc_array_t::from(
|
||||
ctx.get_fresh_msgs()
|
||||
.await
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
@@ -967,7 +988,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {}", or_msg_type3));
|
||||
|
||||
block_on(async move {
|
||||
let arr = dc_array_t::from(
|
||||
Box::into_raw(Box::new(
|
||||
chat::get_chat_media(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
@@ -976,12 +997,8 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
or_msg_type3,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed get_chat_media")
|
||||
.into_iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
.into(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1021,9 +1038,8 @@ pub unsafe extern "C" fn dc_get_next_media(
|
||||
or_msg_type3,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed get_next_media")
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_else(|| 0)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1089,10 +1105,7 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
let list = chat::get_chat_contacts(&ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed get_chat_contacts");
|
||||
let arr = dc_array_t::from(list);
|
||||
let arr = dc_array_t::from(chat::get_chat_contacts(&ctx, ChatId::new(chat_id)).await);
|
||||
Box::into_raw(Box::new(arr))
|
||||
})
|
||||
}
|
||||
@@ -1113,7 +1126,7 @@ pub unsafe extern "C" fn dc_search_msgs(
|
||||
let arr = dc_array_t::from(
|
||||
ctx.search_msgs(ChatId::new(chat_id), to_string_lossy(query))
|
||||
.await
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
@@ -1291,6 +1304,49 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_chat_ephemeral_timer(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_chat_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
// Timer value 0 is returned in the rare case of a database error,
|
||||
// but it is not dangerous since it is only meant to be used as a
|
||||
// default when changing the value. Such errors should not be
|
||||
// ignored when ephemeral timer value is used to construct
|
||||
// message headers.
|
||||
block_on(async move { ChatId::new(chat_id).get_ephemeral_timer(ctx).await })
|
||||
.log_err(ctx, "Failed to get ephemeral timer")
|
||||
.unwrap_or_default()
|
||||
.to_u32()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_chat_ephemeral_timer(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
timer: u32,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_set_chat_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
ChatId::new(chat_id)
|
||||
.set_ephemeral_timer(ctx, EphemeralTimer::from_u32(timer))
|
||||
.await
|
||||
.log_err(ctx, "Failed to set ephemeral timer")
|
||||
.is_ok() as libc::c_int
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_msg_info(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1302,12 +1358,7 @@ pub unsafe extern "C" fn dc_get_msg_info(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
message::get_msg_info(ctx, MsgId::new(msg_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed get_msg_info")
|
||||
})
|
||||
.strdup()
|
||||
block_on(message::get_msg_info(&ctx, MsgId::new(msg_id))).strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1866,11 +1917,7 @@ pub unsafe extern "C" fn dc_set_location(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
location::set(ctx, latitude, longitude, accuracy)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed location::set")
|
||||
}) as _
|
||||
block_on(location::set(&ctx, latitude, longitude, accuracy)) as _
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1995,7 +2042,7 @@ pub unsafe extern "C" fn dc_array_get_timestamp(
|
||||
return 0;
|
||||
}
|
||||
|
||||
(*array).get_location(index).timestamp
|
||||
(*array).get_timestamp(index).unwrap_or_default()
|
||||
}
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_get_chat_id(
|
||||
@@ -2042,7 +2089,7 @@ pub unsafe extern "C" fn dc_array_get_marker(
|
||||
return std::ptr::null_mut(); // NULL explicitly defined as "no markers"
|
||||
}
|
||||
|
||||
if let Some(s) = &(*array).get_location(index).marker {
|
||||
if let Some(s) = (*array).get_marker(index) {
|
||||
s.strdup()
|
||||
} else {
|
||||
std::ptr::null_mut()
|
||||
@@ -2070,16 +2117,6 @@ pub unsafe extern "C" fn dc_array_search_id(
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_get_raw(array: *const dc_array_t) -> *const u32 {
|
||||
if array.is_null() {
|
||||
eprintln!("ignoring careless call to dc_array_get_raw()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
(*array).as_ptr()
|
||||
}
|
||||
|
||||
// Return the independent-state of the location at the given index.
|
||||
// Independent locations do not belong to the track of the user.
|
||||
// Returns 1 if location belongs to the track of the user,
|
||||
@@ -2191,6 +2228,24 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chatlist_get_summary2(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
) -> *mut dc_lot_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chatlist_get_summary2()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
let lot =
|
||||
Chatlist::get_summary2(&ctx, ChatId::new(chat_id), MsgId::new(msg_id), None).await;
|
||||
Box::into_raw(Box::new(lot))
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chatlist_get_context(
|
||||
chatlist: *mut dc_chatlist_t,
|
||||
@@ -2270,12 +2325,7 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
|
||||
let ctx = &*ffi_chat.context;
|
||||
|
||||
block_on(async move {
|
||||
match ffi_chat
|
||||
.chat
|
||||
.get_profile_image(&ctx)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed get_profile_image")
|
||||
{
|
||||
match ffi_chat.chat.get_profile_image(&ctx).await {
|
||||
Some(p) => p.to_string_lossy().strdup(),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -2291,13 +2341,7 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
|
||||
let ffi_chat = &*chat;
|
||||
let ctx = &*ffi_chat.context;
|
||||
|
||||
block_on(async move {
|
||||
ffi_chat
|
||||
.chat
|
||||
.get_color(&ctx)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed dc_chat_get_color")
|
||||
})
|
||||
block_on(ffi_chat.chat.get_color(&ctx))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2670,6 +2714,26 @@ pub unsafe extern "C" fn dc_msg_get_showpadlock(msg: *mut dc_msg_t) -> libc::c_i
|
||||
ffi_msg.message.get_showpadlock() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_ephemeral_timer(msg: *mut dc_msg_t) -> u32 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_ephemeral_timer()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_ephemeral_timestamp(msg: *mut dc_msg_t) -> i64 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_ephemeral_timer()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_ephemeral_timestamp()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_summary(
|
||||
msg: *mut dc_msg_t,
|
||||
@@ -2794,6 +2858,31 @@ pub unsafe extern "C" fn dc_msg_is_setupmessage(msg: *mut dc_msg_t) -> libc::c_i
|
||||
ffi_msg.message.is_setupmessage().into()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_videochat_url(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_videochat_url()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
|
||||
ffi_msg
|
||||
.message
|
||||
.get_videochat_url()
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_videochat_type(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_videochat_type()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_videochat_type().unwrap_or_default() as i32
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
|
||||
@@ -105,8 +105,9 @@ impl<T: AsRef<std::ffi::OsStr>> OsStrExt for T {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn to_c_string(&self) -> Result<CString, CStringError> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
CString::new(self.as_ref().as_bytes()).map_err(|err| match err {
|
||||
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
||||
CString::new(self.as_ref().as_bytes()).map_err(|err| {
|
||||
let std::ffi::NulError { .. } = err;
|
||||
CStringError::InteriorNullByte
|
||||
})
|
||||
}
|
||||
|
||||
@@ -122,8 +123,9 @@ fn os_str_to_c_string_unicode(
|
||||
os_str: &dyn AsRef<std::ffi::OsStr>,
|
||||
) -> Result<CString, CStringError> {
|
||||
match os_str.as_ref().to_str() {
|
||||
Some(val) => CString::new(val.as_bytes()).map_err(|err| match err {
|
||||
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
||||
Some(val) => CString::new(val.as_bytes()).map_err(|err| {
|
||||
let std::ffi::NulError { .. } = err;
|
||||
CStringError::InteriorNullByte
|
||||
}),
|
||||
None => Err(CStringError::NotUnicode),
|
||||
}
|
||||
|
||||
@@ -8,33 +8,36 @@ use quote::quote;
|
||||
// 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 {
|
||||
#[proc_macro_derive(ToSql)]
|
||||
pub fn to_sql_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 rusqlite::types::ToSql for #name {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let num = *self as i64;
|
||||
let value = rusqlite::types::Value::Integer(num);
|
||||
let output = rusqlite::types::ToSqlOutput::Owned(value);
|
||||
std::result::Result::Ok(output)
|
||||
}
|
||||
}
|
||||
};
|
||||
gen.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(FromSql)]
|
||||
pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).unwrap();
|
||||
let name = &ast.ident;
|
||||
|
||||
let gen = quote! {
|
||||
impl rusqlite::types::FromSql for #name {
|
||||
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let inner = rusqlite::types::FromSql::column_result(col)?;
|
||||
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
66
draft/msgwork_new_imap_jobs.rst
Normal file
66
draft/msgwork_new_imap_jobs.rst
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
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.
|
||||
@@ -2,7 +2,7 @@ use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use async_std::path::Path;
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
@@ -28,7 +28,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 1 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM jobs;", paramsx![])
|
||||
.execute("DELETE FROM jobs;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(1) Jobs reset.");
|
||||
@@ -36,7 +36,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 2 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM acpeerstates;", paramsx![])
|
||||
.execute("DELETE FROM acpeerstates;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(2) Peerstates reset.");
|
||||
@@ -44,7 +44,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", paramsx![])
|
||||
.execute("DELETE FROM keypairs;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
@@ -52,35 +52,35 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM contacts WHERE id>9;", paramsx![])
|
||||
.execute("DELETE FROM contacts WHERE id>9;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", paramsx![])
|
||||
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", paramsx![])
|
||||
.execute("DELETE FROM chats_contacts;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", paramsx![])
|
||||
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
paramsx![],
|
||||
paramsv![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM leftgrps;", paramsx![])
|
||||
.execute("DELETE FROM leftgrps;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(8) Rest but server config reset.");
|
||||
@@ -118,7 +118,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
let rs = context.sql().get_raw_config("import_spec").await;
|
||||
let rs = context.sql().get_raw_config(context, "import_spec").await;
|
||||
if rs.is_none() {
|
||||
error!(context, "Import: No file or folder given.");
|
||||
return false;
|
||||
@@ -183,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 { "" },
|
||||
@@ -202,6 +202,15 @@ 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 {
|
||||
@@ -215,7 +224,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.is_daymarker() {
|
||||
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
|
||||
println!(
|
||||
"--------------------------------------------------------------------------------"
|
||||
);
|
||||
@@ -276,7 +285,7 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
}
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr).await;
|
||||
if peerstate.is_ok() && contact_id != 1 as libc::c_uint {
|
||||
if peerstate.is_some() && contact_id != 1 as libc::c_uint {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
@@ -359,6 +368,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
send-garbage\n\
|
||||
sendimage <file> [<text>]\n\
|
||||
sendfile <file> [<text>]\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
listmedia\n\
|
||||
@@ -484,7 +494,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
context.maybe_network().await;
|
||||
}
|
||||
"housekeeping" => {
|
||||
sql::housekeeping(&context).await?;
|
||||
sql::housekeeping(&context).await;
|
||||
}
|
||||
"listchats" | "listarchived" | "chats" => {
|
||||
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
|
||||
@@ -573,8 +583,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "Failed to select chat");
|
||||
let sel_chat = sel_chat.as_ref().unwrap();
|
||||
|
||||
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?;
|
||||
let members = chat::get_chat_contacts(&context, sel_chat.id).await?;
|
||||
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() {
|
||||
@@ -594,7 +613,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
} else {
|
||||
""
|
||||
},
|
||||
match sel_chat.get_profile_image(&context).await? {
|
||||
match sel_chat.get_profile_image(&context).await {
|
||||
Some(icon) => match icon.to_str() {
|
||||
Some(icon) => format!(" Icon: {}", icon),
|
||||
_ => " Icon: Err".to_string(),
|
||||
@@ -691,7 +710,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
let contacts =
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await;
|
||||
println!("Memberlist:");
|
||||
|
||||
log_contactlist(&context, &contacts).await;
|
||||
@@ -762,7 +781,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 {
|
||||
@@ -799,6 +818,10 @@ 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?;
|
||||
}
|
||||
"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.");
|
||||
|
||||
@@ -858,7 +881,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Viewtype::Gif,
|
||||
Viewtype::Video,
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
println!("{} images or videos: ", images.len());
|
||||
for (i, data) in images.iter().enumerate() {
|
||||
if 0 == i {
|
||||
@@ -892,7 +915,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"msginfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
let res = message::get_msg_info(&context, id).await?;
|
||||
let res = message::get_msg_info(&context, id).await;
|
||||
println!("{}", res);
|
||||
}
|
||||
"listfresh" => {
|
||||
|
||||
@@ -158,7 +158,7 @@ const DB_COMMANDS: [&str; 9] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 26] = [
|
||||
const CHAT_COMMANDS: [&str; 27] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
@@ -178,6 +178,7 @@ const CHAT_COMMANDS: [&str; 26] = [
|
||||
"send",
|
||||
"sendimage",
|
||||
"sendfile",
|
||||
"videochat",
|
||||
"draft",
|
||||
"listmedia",
|
||||
"archive",
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
0.900.0 (DRAFT)
|
||||
1.40.1
|
||||
---------------
|
||||
|
||||
- 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
|
||||
@@ -10,6 +21,7 @@
|
||||
- introduced two documented examples for an echo and a group-membership
|
||||
tracking plugin.
|
||||
|
||||
|
||||
0.800.0
|
||||
-------
|
||||
|
||||
|
||||
@@ -7,76 +7,14 @@ 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.
|
||||
without any "build-from-source" steps. Otherwise you need to `compile the Delta Chat bindings
|
||||
yourself <sourceinstall>`_.
|
||||
|
||||
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
||||
We recommend to first `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
|
||||
@@ -103,6 +41,78 @@ 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
|
||||
=============
|
||||
|
||||
|
||||
3
python/doc/_templates/globaltoc.html
vendored
3
python/doc/_templates/globaltoc.html
vendored
@@ -9,8 +9,7 @@
|
||||
</ul>
|
||||
<b>external links:</b>
|
||||
<ul>
|
||||
<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://github.com/deltachat/deltachat-core-rust">github repository</a></li>
|
||||
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -32,16 +32,16 @@ class GroupTrackingPlugin:
|
||||
print("chat member: {}".format(member.addr))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
print("ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||
contact.addr, chat.id, actor or 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, message):
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
print("ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
|
||||
@@ -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 {}*
|
||||
""".format(contact3.addr))
|
||||
*ac_member_added {}*from*{}*
|
||||
""".format(contact3.addr, ac1.get_config("addr")))
|
||||
|
||||
lp.sec("contact successfully added, now removing")
|
||||
ch.remove_contact(contact3)
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_member_removed {}*
|
||||
""".format(contact3.addr))
|
||||
*ac_member_removed {}*from*{}*
|
||||
""".format(contact3.addr, ac1.get_config("addr")))
|
||||
|
||||
@@ -17,8 +17,12 @@ if __name__ == "__main__":
|
||||
os.environ["DCC_RS_DEV"] = dn
|
||||
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
|
||||
|
||||
if target == 'release':
|
||||
extra = " -C lto=on -C embed-bitcode=yes"
|
||||
os.environ["RUSTFLAGS"] = os.environ.get("RUSTFLAGS", "") + extra
|
||||
cmd.append("--release")
|
||||
|
||||
print("running:", " ".join(cmd))
|
||||
subprocess.check_call(cmd)
|
||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
|
||||
|
||||
@@ -246,7 +246,6 @@ class Account(object):
|
||||
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):
|
||||
@@ -607,12 +606,24 @@ class Account(object):
|
||||
self.stop_io()
|
||||
|
||||
self.log("remove dc_context references")
|
||||
# the dc_context_unref triggers get_next_event to return ffi.NULL
|
||||
# which in turns makes the event thread finish execution
|
||||
|
||||
# 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()
|
||||
self._dc_context = None
|
||||
|
||||
self.log("wait for event thread to finish")
|
||||
self._event_thread.wait()
|
||||
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._shutdown_event.set()
|
||||
|
||||
|
||||
@@ -139,6 +139,22 @@ class Chat(object):
|
||||
"""
|
||||
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
|
||||
|
||||
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)
|
||||
|
||||
def get_type(self):
|
||||
""" (deprecated) return type of this chat.
|
||||
|
||||
|
||||
@@ -159,9 +159,13 @@ 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[HEADER]', FLAGS]
|
||||
requested = [b'BODY.PEEK[]', FLAGS]
|
||||
for uid, data in self.conn.fetch(messages, requested).items():
|
||||
body_bytes = data[b'BODY[HEADER]']
|
||||
body_bytes = data[b'BODY[]']
|
||||
if not body_bytes:
|
||||
log("Message", uid, "has empty body")
|
||||
continue
|
||||
|
||||
flags = data[FLAGS]
|
||||
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
@@ -192,6 +196,7 @@ 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):
|
||||
|
||||
@@ -86,11 +86,11 @@ class FFIEventTracker:
|
||||
if rex.match(ev.name):
|
||||
return ev
|
||||
|
||||
def get_info_matching(self, regex):
|
||||
rex = re.compile("(?:{}).*".format(regex))
|
||||
def get_info_contains(self, regex):
|
||||
rex = re.compile(regex)
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
if rex.match(ev.data2):
|
||||
if rex.search(ev.data2):
|
||||
return ev
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
@@ -139,6 +139,7 @@ class EventThread(threading.Thread):
|
||||
self.account = account
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.setDaemon(True)
|
||||
self._marked_for_shutdown = False
|
||||
self.start()
|
||||
|
||||
@contextmanager
|
||||
@@ -147,12 +148,15 @@ class EventThread(threading.Thread):
|
||||
yield
|
||||
self.account.log(message + " FINISHED")
|
||||
|
||||
def wait(self):
|
||||
def mark_shutdown(self):
|
||||
self._marked_for_shutdown = True
|
||||
|
||||
def wait(self, timeout=None):
|
||||
if self == threading.current_thread():
|
||||
# we are in the callback thread and thus cannot
|
||||
# wait for the thread-loop to finish.
|
||||
return
|
||||
self.join()
|
||||
self.join(timeout=timeout)
|
||||
|
||||
def run(self):
|
||||
""" get and run events until shutdown. """
|
||||
@@ -164,10 +168,12 @@ class EventThread(threading.Thread):
|
||||
lib.dc_get_event_emitter(self.account._dc_context),
|
||||
lib.dc_event_emitter_unref,
|
||||
)
|
||||
while 1:
|
||||
while not self._marked_for_shutdown:
|
||||
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
|
||||
|
||||
@@ -16,7 +16,7 @@ class PerAccount:
|
||||
""" per-Account-instance hook specifications.
|
||||
|
||||
All hooks are executed in a dedicated Event thread.
|
||||
Hooks are not allowed to block/last long as this
|
||||
Hooks are generally not allowed to block/last long as this
|
||||
blocks overall event processing on the python side.
|
||||
"""
|
||||
@classmethod
|
||||
@@ -31,10 +31,6 @@ 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
|
||||
@@ -55,19 +51,37 @@ class PerAccount:
|
||||
|
||||
@account_hookspec
|
||||
def ac_message_delivered(self, message):
|
||||
""" Called when an outgoing message has been delivered to SMTP. """
|
||||
""" Called when an outgoing message has been delivered to SMTP.
|
||||
|
||||
:param message: Message that was just delivered.
|
||||
"""
|
||||
|
||||
@account_hookspec
|
||||
def ac_chat_modified(self, chat):
|
||||
""" Chat was created or modified regarding membership, avatar, title. """
|
||||
""" Chat was created or modified regarding membership, avatar, title.
|
||||
|
||||
:param chat: Chat which was modified.
|
||||
"""
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_added(self, chat, contact, message):
|
||||
""" Called for each contact added to an accepted chat. """
|
||||
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.
|
||||
"""
|
||||
|
||||
@account_hookspec
|
||||
def ac_member_removed(self, chat, contact, message):
|
||||
""" Called for each contact removed from a chat. """
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class Global:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
""" The Message object. """
|
||||
|
||||
import os
|
||||
import re
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
@@ -154,6 +155,26 @@ 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)
|
||||
|
||||
def get_mime_headers(self):
|
||||
""" return mime-header object for an incoming message.
|
||||
|
||||
@@ -336,20 +357,43 @@ 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 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
|
||||
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()
|
||||
|
||||
|
||||
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 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.
|
||||
text = text.lower()
|
||||
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("()")
|
||||
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
|
||||
|
||||
@@ -32,6 +32,10 @@ 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):
|
||||
@@ -152,7 +156,7 @@ class SessionLiveConfigFromURL:
|
||||
assert index == len(self.configlist), index
|
||||
res = requests.post(self.url)
|
||||
if res.status_code != 200:
|
||||
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
||||
pytest.skip("creating newtmpuser failed with code {}: '{}'".format(res.status_code, res.text))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
self.configlist.append(config)
|
||||
@@ -231,10 +235,13 @@ 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:
|
||||
ac.add_account_plugin(FFIEventLogger(ac))
|
||||
logger = FFIEventLogger(ac)
|
||||
logger.init_time = self.init_time
|
||||
ac.add_account_plugin(logger)
|
||||
self._accounts.append(ac)
|
||||
return ac
|
||||
|
||||
@@ -244,10 +251,7 @@ 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)
|
||||
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
|
||||
return self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
|
||||
def _preconfigure_key(self, account, addr):
|
||||
# Only set a key if we haven't used it yet for another account.
|
||||
@@ -282,16 +286,15 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
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)
|
||||
|
||||
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,
|
||||
@@ -332,8 +335,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
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"),
|
||||
|
||||
@@ -11,8 +11,21 @@ from datetime import datetime, timedelta
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msgtext,res", [
|
||||
("Member Me (tmp1@x.org) removed by tmp2@x.org.", ("removed", "tmp1@x.org")),
|
||||
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org")),
|
||||
("Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by me",
|
||||
("removed", "tmp1@x.org", "me")),
|
||||
("Group left by some one (tmp1@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Group left by tmp1@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member nothing bla bla", None),
|
||||
("Another unknown system message", None),
|
||||
])
|
||||
def test_parse_system_add_remove(msgtext, res):
|
||||
from deltachat.message import parse_system_add_remove
|
||||
@@ -116,6 +129,11 @@ class TestOfflineContact:
|
||||
assert not contact1.is_blocked()
|
||||
assert not contact1.is_verified()
|
||||
|
||||
def test_create_self_contact(self, acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
contact1 = ac1.create_contact(ac1.get_config("addr"))
|
||||
assert contact1.id == 1
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
@@ -452,12 +470,12 @@ class TestOfflineChat:
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact):
|
||||
in_list.append(("added", chat, contact))
|
||||
def ac_member_added(self, chat, contact, actor):
|
||||
in_list.append(("added", chat, contact, actor))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact):
|
||||
in_list.append(("removed", chat, contact))
|
||||
def ac_member_removed(self, chat, contact, actor):
|
||||
in_list.append(("removed", chat, contact, actor))
|
||||
|
||||
ac1.add_account_plugin(InPlugin())
|
||||
|
||||
@@ -486,10 +504,11 @@ class TestOfflineChat:
|
||||
|
||||
assert len(in_list) == 10
|
||||
chat_contacts = chat.get_contacts()
|
||||
for in_cmd, in_chat, in_contact in in_list:
|
||||
for in_cmd, in_chat, in_contact, in_actor in in_list:
|
||||
assert in_cmd == "added"
|
||||
assert in_chat == chat
|
||||
assert in_contact in chat_contacts
|
||||
assert in_actor is None
|
||||
chat_contacts.remove(in_contact)
|
||||
|
||||
assert chat_contacts[0].id == 1 # self contact
|
||||
@@ -517,7 +536,7 @@ def test_basic_imap_api(acfactory, tmpdir):
|
||||
|
||||
imap2 = ac2.direct_imap
|
||||
|
||||
ac2.direct_imap.idle_start()
|
||||
imap2.idle_start()
|
||||
chat12.send_text("hello")
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
@@ -1542,6 +1561,97 @@ class TestOnlineAccount:
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac2.create_chat(ac4)
|
||||
|
||||
def test_immediate_autodelete(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=False, move=False, sentbox=False)
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
imap2 = ac2.direct_imap
|
||||
imap2.idle_start()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
sent_msg = chat1.send_text("hello")
|
||||
imap2.idle_check(terminate=False)
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
imap2.idle_check(terminate=True)
|
||||
ac2._evtracker.get_info_contains("close/expunge succeeded")
|
||||
|
||||
assert len(imap2.get_all_messages()) == 0
|
||||
|
||||
# Mark deleted message as seen and check that read receipt arrives
|
||||
msg.mark_seen()
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 == chat1.id
|
||||
assert ev.data2 == sent_msg.id
|
||||
|
||||
def test_ephemeral_timer(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
chat2 = ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("ac1: set ephemeral timer to 60")
|
||||
chat1.set_ephemeral_timer(60)
|
||||
|
||||
lp.sec("ac1: check that ephemeral timer is set for chat")
|
||||
assert chat1.get_ephemeral_timer() == 60
|
||||
chat1_summary = chat1.get_summary()
|
||||
assert chat1_summary["ephemeral_timer"] == {'Enabled': {'duration': 60}}
|
||||
|
||||
lp.sec("ac2: receive system message about ephemeral timer modification")
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
system_message1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert chat2.get_ephemeral_timer() == 60
|
||||
assert system_message1.is_system_message()
|
||||
|
||||
# Disabled until markers are implemented
|
||||
# assert "Ephemeral timer: 60\n" in system_message1.get_message_info()
|
||||
|
||||
lp.sec("ac2: send message to ac1")
|
||||
sent_message = chat2.send_text("message")
|
||||
assert sent_message.ephemeral_timer == 60
|
||||
assert "Ephemeral timer: 60\n" in sent_message.get_message_info()
|
||||
|
||||
# Timer is started immediately for sent messages
|
||||
assert sent_message.ephemeral_timestamp is not None
|
||||
assert "Expires: " in sent_message.get_message_info()
|
||||
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
text_message = ac1._evtracker.wait_next_incoming_message()
|
||||
assert text_message.text == "message"
|
||||
assert text_message.ephemeral_timer == 60
|
||||
assert "Ephemeral timer: 60\n" in text_message.get_message_info()
|
||||
|
||||
# Timer should not start until message is displayed
|
||||
assert text_message.ephemeral_timestamp is None
|
||||
assert "Expires: " not in text_message.get_message_info()
|
||||
text_message.mark_seen()
|
||||
text_message = ac1.get_message_by_id(text_message.id)
|
||||
assert text_message.ephemeral_timestamp is not None
|
||||
assert "Expires: " in text_message.get_message_info()
|
||||
|
||||
lp.sec("ac2: set ephemeral timer to 0")
|
||||
chat2.set_ephemeral_timer(0)
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
|
||||
lp.sec("ac1: receive system message about ephemeral timer modification")
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
system_message2 = ac1._evtracker.wait_next_incoming_message()
|
||||
assert system_message2.ephemeral_timer is None
|
||||
assert "Ephemeral timer: " not in system_message2.get_message_info()
|
||||
assert chat1.get_ephemeral_timer() == 0
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
def test_group_many_members_add_leave_remove(self, acfactory, lp):
|
||||
|
||||
@@ -7,7 +7,7 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
|
||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
passenv =
|
||||
TRAVIS
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.43.1
|
||||
1.45.0
|
||||
|
||||
@@ -5,18 +5,27 @@ import sys
|
||||
import re
|
||||
import pathlib
|
||||
import subprocess
|
||||
from argparse import ArgumentParser
|
||||
|
||||
rex = re.compile(r'version = "(\S+)"')
|
||||
|
||||
def read_toml_version(relpath):
|
||||
|
||||
def regex_matches(relpath, regex=rex):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
for line in open(str(p)):
|
||||
m = rex.match(line)
|
||||
m = regex.match(line)
|
||||
if m is not None:
|
||||
return m.group(1)
|
||||
return m
|
||||
|
||||
|
||||
def read_toml_version(relpath):
|
||||
res = regex_matches(relpath, rex)
|
||||
if res is not None:
|
||||
return res.group(1)
|
||||
raise ValueError("no version found in {}".format(relpath))
|
||||
|
||||
|
||||
def replace_toml_version(relpath, newversion):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
@@ -25,18 +34,28 @@ 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__":
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
|
||||
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:
|
||||
print("{}: {}".format(x, read_toml_version(x)))
|
||||
print()
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
newversion = sys.argv[1]
|
||||
|
||||
newversion = opts.newversion
|
||||
if newversion.count(".") < 2:
|
||||
raise SystemExit("need at least two dots in version")
|
||||
|
||||
@@ -55,7 +74,10 @@ if __name__ == "__main__":
|
||||
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"])
|
||||
|
||||
@@ -63,3 +85,8 @@ if __name__ == "__main__":
|
||||
print("")
|
||||
print(" git tag {}".format(newversion))
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -6,15 +6,13 @@ use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, str};
|
||||
|
||||
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, Sqlx)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum EncryptPreference {
|
||||
NoPreference = 0,
|
||||
|
||||
@@ -168,6 +168,9 @@ 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
|
||||
@@ -179,6 +182,11 @@ 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
|
||||
}
|
||||
|
||||
1178
src/chat.rs
1178
src/chat.rs
File diff suppressed because it is too large
Load Diff
103
src/chatlist.rs
103
src/chatlist.rs
@@ -5,6 +5,7 @@ use crate::chat::*;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::*;
|
||||
use crate::ephemeral::delete_expired_messages;
|
||||
use crate::error::{bail, ensure, Result};
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
@@ -76,7 +77,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)
|
||||
@@ -99,12 +100,23 @@ 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_device_expired_messages(context).await {
|
||||
if let Err(err) = delete_expired_messages(context).await {
|
||||
warn!(context, "Failed to hide expired messages: {}", err);
|
||||
}
|
||||
|
||||
let mut add_archived_link_item = false;
|
||||
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let msg_id: MsgId = row.get(1).unwrap_or_default();
|
||||
Ok((chat_id, msg_id))
|
||||
};
|
||||
|
||||
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
};
|
||||
|
||||
let skip_id = if flag_for_forwarding {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.await
|
||||
@@ -131,7 +143,7 @@ impl Chatlist {
|
||||
// shown at all permanent in the chatlist.
|
||||
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
||||
// show chats shared with a given contact
|
||||
context.sql.query_rows(
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -140,14 +152,16 @@ impl Chatlist {
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?)
|
||||
AND (hidden=0 OR state=?1)
|
||||
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=?)
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
GROUP BY c.id
|
||||
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],
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?
|
||||
} else if flag_archived_only {
|
||||
// show archived chats
|
||||
@@ -156,7 +170,7 @@ impl Chatlist {
|
||||
// and adapting the number requires larger refactorings and seems not to be worth the effort)
|
||||
context
|
||||
.sql
|
||||
.query_rows(
|
||||
.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -172,7 +186,9 @@ impl Chatlist {
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsx![MessageState::OutDraft],
|
||||
paramsv![MessageState::OutDraft],
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?
|
||||
} else if let Some(query) = query {
|
||||
@@ -188,7 +204,7 @@ impl Chatlist {
|
||||
let str_like_cmd = format!("%{}%", query);
|
||||
context
|
||||
.sql
|
||||
.query_rows(
|
||||
.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -204,7 +220,9 @@ impl Chatlist {
|
||||
AND c.name LIKE ?3
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsx![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -217,7 +235,7 @@ impl Chatlist {
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
let mut ids = context.sql.query_rows(
|
||||
let mut ids = context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
@@ -233,9 +251,10 @@ impl Chatlist {
|
||||
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;",
|
||||
paramsx![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?;
|
||||
|
||||
if !flag_no_specials {
|
||||
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
|
||||
{
|
||||
@@ -310,20 +329,30 @@ 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 ret;
|
||||
return Lot::new();
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
@@ -332,7 +361,7 @@ impl Chatlist {
|
||||
|
||||
let mut lastcontact = None;
|
||||
|
||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id).await {
|
||||
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)
|
||||
{
|
||||
@@ -369,15 +398,15 @@ impl Chatlist {
|
||||
|
||||
/// Returns the number of archived chats
|
||||
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
|
||||
let v: i32 = context
|
||||
context
|
||||
.sql
|
||||
.query_value(
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
|
||||
paramsx![],
|
||||
paramsv![],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
v as u32
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
@@ -385,21 +414,21 @@ async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
// sufficient as there are typically only few fresh messages.
|
||||
context
|
||||
.sql
|
||||
.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![],
|
||||
.query_get_value(
|
||||
context,
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN chats c",
|
||||
" ON c.id=m.chat_id",
|
||||
" WHERE m.state=10",
|
||||
" AND m.hidden=0",
|
||||
" AND c.blocked=2",
|
||||
" ORDER BY m.timestamp DESC, m.id DESC;"
|
||||
),
|
||||
paramsv![],
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -117,24 +117,32 @@ pub enum Config {
|
||||
|
||||
#[strum(serialize = "sys.config_keys")]
|
||||
SysConfigKeys,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
/// 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)
|
||||
NotifyAboutWrongPw,
|
||||
|
||||
/// address to webrtc instance to use for videochats
|
||||
WebrtcInstance,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub async fn config_exists(&self, key: Config) -> bool {
|
||||
self.sql.get_raw_config(key).await.is_some()
|
||||
self.sql.get_raw_config(self, 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) -> 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(self, 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(self, key).await,
|
||||
};
|
||||
|
||||
if value.is_some() {
|
||||
@@ -189,7 +197,7 @@ impl Context {
|
||||
match key {
|
||||
Config::Selfavatar => {
|
||||
self.sql
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsx![])
|
||||
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
|
||||
.await?;
|
||||
self.sql
|
||||
.set_raw_config_bool(self, "attach_selfavatar", true)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Email accounts autoconfiguration process module
|
||||
|
||||
#![forbid(clippy::indexing_slicing)]
|
||||
|
||||
mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
mod read_url;
|
||||
@@ -37,7 +35,7 @@ macro_rules! progress {
|
||||
impl Context {
|
||||
/// Checks if the context is already configured.
|
||||
pub async fn is_configured(&self) -> bool {
|
||||
self.sql.get_raw_config_bool("configured").await
|
||||
self.sql.get_raw_config_bool(self, "configured").await
|
||||
}
|
||||
|
||||
/// Configures this account with the currently set parameters.
|
||||
@@ -72,6 +70,7 @@ impl Context {
|
||||
|
||||
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) = provider::get_provider_info(¶m.addr) {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
@@ -102,11 +101,12 @@ impl Context {
|
||||
|
||||
match success {
|
||||
Ok(_) => {
|
||||
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
progress!(self, 1000);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
error!(self, "Configure Failed: {}", err);
|
||||
progress!(self, 0);
|
||||
Err(err)
|
||||
}
|
||||
@@ -232,7 +232,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
}
|
||||
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 {
|
||||
param.server_flags |= if param.mail_port == 143 {
|
||||
DC_LP_IMAP_SOCKET_STARTTLS as i32
|
||||
} else {
|
||||
DC_LP_IMAP_SOCKET_SSL as i32
|
||||
@@ -459,7 +459,10 @@ async fn try_imap_connections(
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(()); // we directly return here if it was autoconfig or the connection succeeded
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
bail!("autoconfig did not succeed");
|
||||
}
|
||||
|
||||
progress!(context, 670);
|
||||
@@ -493,7 +496,7 @@ async fn try_imap_connection(
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
return Ok(());
|
||||
bail!("autoconfig did not succeed");
|
||||
}
|
||||
|
||||
progress!(context, 650 + variation * 30);
|
||||
@@ -553,7 +556,7 @@ async fn try_smtp_connections(
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
return Ok(());
|
||||
bail!("No SMTP connection");
|
||||
}
|
||||
progress!(context, 850);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! # Constants
|
||||
#![allow(dead_code)]
|
||||
|
||||
use deltachat_derive::*;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -24,11 +25,12 @@ const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
sqlx::Type,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
#[repr(u8)]
|
||||
pub enum Blocked {
|
||||
Not = 0,
|
||||
Manually = 1,
|
||||
@@ -41,8 +43,8 @@ impl Default for Blocked {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(u8)]
|
||||
pub enum ShowEmails {
|
||||
Off = 0,
|
||||
AcceptedContacts = 1,
|
||||
@@ -55,8 +57,8 @@ impl Default for ShowEmails {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(u8)]
|
||||
pub enum MediaQuality {
|
||||
Balanced = 0,
|
||||
Worse = 1,
|
||||
@@ -68,7 +70,7 @@ impl Default for MediaQuality {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyGenType {
|
||||
Default = 0,
|
||||
@@ -82,6 +84,19 @@ impl Default for KeyGenType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(i8)]
|
||||
pub enum VideochatType {
|
||||
Unknown = 0,
|
||||
BasicWebrtc = 1,
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -125,12 +140,13 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
IntoStaticStr,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
sqlx::Type,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
#[repr(u32)]
|
||||
pub enum Chattype {
|
||||
Undefined = 0,
|
||||
Single = 100,
|
||||
@@ -240,9 +256,10 @@ pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
sqlx::Type,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
pub enum Viewtype {
|
||||
@@ -292,6 +309,9 @@ 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 {
|
||||
@@ -319,54 +339,6 @@ 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;
|
||||
|
||||
|
||||
339
src/contact.rs
339
src/contact.rs
@@ -1,14 +1,10 @@
|
||||
//! Contacts module
|
||||
|
||||
#![forbid(clippy::indexing_slicing)]
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::prelude::*;
|
||||
use deltachat_derive::*;
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::ChatId;
|
||||
@@ -74,7 +70,7 @@ pub struct Contact {
|
||||
|
||||
/// Possible origins of a contact.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, sqlx::Type,
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
pub enum Origin {
|
||||
@@ -135,31 +131,6 @@ impl Default for Origin {
|
||||
Origin::Unknown
|
||||
}
|
||||
}
|
||||
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Contact {
|
||||
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
|
||||
let contact = Self {
|
||||
id: u32::try_from(row.try_get::<i32, _>("id")?)
|
||||
.map_err(|err| sqlx::Error::Decode(err.into()))?,
|
||||
name: row
|
||||
.try_get::<Option<String>, _>("name")?
|
||||
.unwrap_or_default(),
|
||||
authname: row
|
||||
.try_get::<Option<String>, _>("authname")?
|
||||
.unwrap_or_default(),
|
||||
addr: row.try_get::<String, _>("addr")?,
|
||||
blocked: row
|
||||
.try_get::<Option<i32>, _>("blocked")?
|
||||
.unwrap_or_default()
|
||||
!= 0,
|
||||
origin: row.try_get("origin")?,
|
||||
param: row
|
||||
.try_get::<String, _>("param")?
|
||||
.parse()
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
Ok(contact)
|
||||
}
|
||||
}
|
||||
|
||||
impl Origin {
|
||||
/// Contacts that are known, i. e. they came in via accepted contacts or
|
||||
@@ -190,18 +161,27 @@ pub enum VerifiedStatus {
|
||||
|
||||
impl Contact {
|
||||
pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
|
||||
let mut res: Contact = context
|
||||
let mut res = context
|
||||
.sql
|
||||
.query_row(
|
||||
r#"
|
||||
SELECT id, name, addr, origin, blocked, authname, param
|
||||
FROM contacts
|
||||
WHERE id=?;
|
||||
"#,
|
||||
paramsx![contact_id as i32],
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
|row| {
|
||||
let contact = Self {
|
||||
id: contact_id,
|
||||
name: row.get::<_, String>(0)?,
|
||||
authname: row.get::<_, String>(4)?,
|
||||
addr: row.get::<_, String>(1)?,
|
||||
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
|
||||
origin: row.get(2)?,
|
||||
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
|
||||
};
|
||||
Ok(contact)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if contact_id == DC_CONTACT_ID_SELF {
|
||||
res.name = context.stock_str(StockMessage::SelfMsg).await.to_string();
|
||||
res.addr = context
|
||||
@@ -287,11 +267,8 @@ SELECT id, name, addr, origin, blocked, authname, param
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
UPDATE msgs SET state=?
|
||||
WHERE from_id=? AND state=?;
|
||||
"#,
|
||||
paramsx![MessageState::InNoticed, id as i32, MessageState::InFresh],
|
||||
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -326,15 +303,15 @@ WHERE from_id=? AND state=?;
|
||||
if addr_cmp(addr_normalized, addr_self) {
|
||||
return DC_CONTACT_ID_SELF;
|
||||
}
|
||||
let v: i32 = context.sql.query_value(
|
||||
"SELECT id FROM contacts WHERE addr=? COLLATE NOCASE AND id>? AND origin>=? AND blocked=0;",
|
||||
paramsx![
|
||||
context.sql.query_get_value(
|
||||
context,
|
||||
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||
paramsv![
|
||||
addr_normalized,
|
||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||
min_origin as i32
|
||||
min_origin as u32,
|
||||
],
|
||||
).await.unwrap_or_default();
|
||||
v as u32
|
||||
).await.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Lookup a contact and create it if it does not exist yet.
|
||||
@@ -405,32 +382,39 @@ WHERE from_id=? AND state=?;
|
||||
let mut update_authname = false;
|
||||
let mut row_id = 0;
|
||||
|
||||
let res: Result<(i32, String, String, Origin, String), _> = context.sql.query_row(
|
||||
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context.sql.query_row(
|
||||
"SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE;",
|
||||
paramsx![addr.to_string()],
|
||||
)
|
||||
.await;
|
||||
paramsv![addr.to_string()],
|
||||
|row| {
|
||||
let row_id = row.get(0)?;
|
||||
let row_name: String = row.get(1)?;
|
||||
let row_addr: String = row.get(2)?;
|
||||
let row_origin: Origin = row.get(3)?;
|
||||
let row_authname: String = row.get(4)?;
|
||||
|
||||
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = res {
|
||||
if !name.as_ref().is_empty() {
|
||||
if !row_name.is_empty() {
|
||||
if (origin >= row_origin || row_name == row_authname)
|
||||
&& name.as_ref() != row_name
|
||||
{
|
||||
if !name.as_ref().is_empty() {
|
||||
if !row_name.is_empty() {
|
||||
if (origin >= row_origin || row_name == row_authname)
|
||||
&& name.as_ref() != row_name
|
||||
{
|
||||
update_name = true;
|
||||
}
|
||||
} else {
|
||||
update_name = true;
|
||||
}
|
||||
} else {
|
||||
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
|
||||
update_authname = true;
|
||||
}
|
||||
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
|
||||
// no name given on manual edit, this will update the name to the authname
|
||||
update_name = true;
|
||||
}
|
||||
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
|
||||
update_authname = true;
|
||||
}
|
||||
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
|
||||
// no name given on manual edit, this will update the name to the authname
|
||||
update_name = true;
|
||||
}
|
||||
|
||||
row_id = id as u32;
|
||||
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
||||
},
|
||||
)
|
||||
.await {
|
||||
row_id = id;
|
||||
if origin as i32 >= row_origin as i32 && addr != row_addr {
|
||||
update_addr = true;
|
||||
}
|
||||
@@ -449,13 +433,9 @@ WHERE from_id=? AND state=?;
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
||||
paramsx![
|
||||
&new_name,
|
||||
if update_addr {
|
||||
addr.to_string()
|
||||
} else {
|
||||
row_addr
|
||||
},
|
||||
paramsv![
|
||||
new_name,
|
||||
if update_addr { addr.to_string() } else { row_addr },
|
||||
if origin > row_origin {
|
||||
origin
|
||||
} else {
|
||||
@@ -466,7 +446,7 @@ WHERE from_id=? AND state=?;
|
||||
} else {
|
||||
row_authname
|
||||
},
|
||||
row_id as i32
|
||||
row_id
|
||||
],
|
||||
)
|
||||
.await
|
||||
@@ -477,7 +457,7 @@ WHERE from_id=? AND state=?;
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
context.sql.execute(
|
||||
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
|
||||
paramsx![&new_name, Chattype::Single, row_id as i32]
|
||||
paramsv![new_name, Chattype::Single, row_id]
|
||||
).await.ok();
|
||||
}
|
||||
sth_modified = Modifier::Modified;
|
||||
@@ -491,17 +471,20 @@ WHERE from_id=? AND state=?;
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
|
||||
paramsx![
|
||||
name.as_ref(),
|
||||
&addr,
|
||||
paramsv![
|
||||
name.as_ref().to_string(),
|
||||
addr,
|
||||
origin,
|
||||
if update_authname { name.as_ref() } else { "" }
|
||||
if update_authname { name.as_ref().to_string() } else { "".to_string() }
|
||||
],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
row_id = context.sql.get_rowid("contacts", "addr", &addr).await?;
|
||||
row_id = context
|
||||
.sql
|
||||
.get_rowid(context, "contacts", "addr", &addr)
|
||||
.await?;
|
||||
sth_modified = Modifier::Created;
|
||||
info!(context, "added contact id={} addr={}", row_id, &addr);
|
||||
} else {
|
||||
@@ -588,32 +571,35 @@ WHERE from_id=? AND state=?;
|
||||
.map(|s| s.as_ref().to_string())
|
||||
.unwrap_or_default()
|
||||
);
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(
|
||||
r#"
|
||||
SELECT c.id FROM contacts c
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr
|
||||
WHERE c.addr!=?
|
||||
AND c.id>?
|
||||
AND c.origin>=?
|
||||
AND c.blocked=0
|
||||
AND (c.name LIKE ? OR c.addr LIKE ?)
|
||||
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0)
|
||||
ORDER BY LOWER(c.name||c.addr),c.id
|
||||
"#,
|
||||
)
|
||||
.bind(&self_addr)
|
||||
.bind(DC_CONTACT_ID_LAST_SPECIAL as i32)
|
||||
.bind(Origin::IncomingReplyTo)
|
||||
.bind(&s3str_like_cmd)
|
||||
.bind(&s3str_like_cmd)
|
||||
.bind(if flag_verified_only { 0i32 } else { 1i32 })
|
||||
.fetch(&pool);
|
||||
|
||||
while let Some(id) = rows.next().await {
|
||||
let (id,): (i32,) = id?;
|
||||
ret.push(id as u32);
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.id FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
|
||||
WHERE c.addr!=?1 \
|
||||
AND c.id>?2 \
|
||||
AND c.origin>=?3 \
|
||||
AND c.blocked=0 \
|
||||
AND (c.name LIKE ?4 OR c.addr LIKE ?5) \
|
||||
AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \
|
||||
ORDER BY LOWER(c.name||c.addr),c.id;",
|
||||
paramsv![
|
||||
self_addr,
|
||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||
Origin::IncomingReplyTo,
|
||||
s3str_like_cmd,
|
||||
s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 },
|
||||
],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
ret.push(id? as u32);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let self_name = context
|
||||
.get_config(Config::Displayname)
|
||||
@@ -634,15 +620,17 @@ SELECT c.id FROM contacts c
|
||||
} else {
|
||||
add_self = true;
|
||||
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(
|
||||
"SELECT id FROM contacts WHERE addr!=? AND id>? AND origin>=? AND blocked=0 ORDER BY LOWER(name || addr), id;"
|
||||
).bind(self_addr).bind(DC_CONTACT_ID_LAST_SPECIAL as i32).bind(0x100).fetch(&pool);
|
||||
|
||||
while let Some(id) = rows.next().await {
|
||||
let (id,): (i32,) = id?;
|
||||
ret.push(id as u32);
|
||||
}
|
||||
context.sql.query_map(
|
||||
"SELECT id FROM contacts WHERE addr!=?1 AND id>?2 AND origin>=?3 AND blocked=0 ORDER BY LOWER(name||addr),id;",
|
||||
paramsv![self_addr, DC_CONTACT_ID_LAST_SPECIAL as i32, 0x100],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
ret.push(id? as u32);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
).await?;
|
||||
}
|
||||
|
||||
if flag_add_self && add_self {
|
||||
@@ -653,30 +641,32 @@ SELECT c.id FROM contacts c
|
||||
}
|
||||
|
||||
pub async fn get_blocked_cnt(context: &Context) -> usize {
|
||||
let v: i32 = context
|
||||
context
|
||||
.sql
|
||||
.query_value(
|
||||
.query_get_value::<isize>(
|
||||
context,
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
|
||||
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
v as usize
|
||||
.unwrap_or_default() as usize
|
||||
}
|
||||
|
||||
/// Get blocked contacts.
|
||||
pub async fn get_all_blocked(context: &Context) -> Vec<u32> {
|
||||
context
|
||||
.sql
|
||||
.query_values(
|
||||
.query_map(
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(name||addr),id;",
|
||||
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
|row| row.get::<_, u32>(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|id: i32| id as u32)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns a textual summary of the encryption state for the contact.
|
||||
@@ -691,10 +681,9 @@ SELECT c.id FROM contacts c
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await;
|
||||
let loginparam = LoginParam::from_database(context, "configured_").await;
|
||||
|
||||
if peerstate.is_ok()
|
||||
if peerstate.is_some()
|
||||
&& peerstate
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|p| p.peek_key(PeerstateVerifiedStatus::Unverified))
|
||||
.is_some()
|
||||
{
|
||||
@@ -763,9 +752,10 @@ SELECT c.id FROM contacts c
|
||||
|
||||
let count_contacts: i32 = context
|
||||
.sql
|
||||
.query_value(
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
|
||||
paramsx![contact_id as i32],
|
||||
paramsv![contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
@@ -773,9 +763,10 @@ SELECT c.id FROM contacts c
|
||||
let count_msgs: i32 = if count_contacts > 0 {
|
||||
context
|
||||
.sql
|
||||
.query_value(
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
|
||||
paramsx![contact_id as i32, contact_id as i32],
|
||||
paramsv![contact_id as i32, contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -788,7 +779,7 @@ SELECT c.id FROM contacts c
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM contacts WHERE id=?;",
|
||||
paramsx![contact_id as i32],
|
||||
paramsv![contact_id as i32],
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -826,7 +817,7 @@ SELECT c.id FROM contacts c
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET param=? WHERE id=?",
|
||||
paramsx![self.param.to_string(), self.id as i32],
|
||||
paramsv![self.param.to_string(), self.id as i32],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -934,7 +925,7 @@ SELECT c.id FROM contacts c
|
||||
pub async fn is_verified_ex(
|
||||
&self,
|
||||
context: &Context,
|
||||
peerstate: Option<&Peerstate>,
|
||||
peerstate: Option<&Peerstate<'_>>,
|
||||
) -> VerifiedStatus {
|
||||
// We're always sort of secured-verified as we could verify the key on this device any time with the key
|
||||
// on this device
|
||||
@@ -949,7 +940,7 @@ SELECT c.id FROM contacts c
|
||||
}
|
||||
|
||||
let peerstate = Peerstate::from_addr(context, &self.addr).await;
|
||||
if let Ok(ps) = peerstate {
|
||||
if let Some(ps) = peerstate {
|
||||
if ps.verified_key.is_some() {
|
||||
return VerifiedStatus::BidirectVerified;
|
||||
}
|
||||
@@ -984,15 +975,15 @@ SELECT c.id FROM contacts c
|
||||
return 0;
|
||||
}
|
||||
|
||||
let v: i32 = context
|
||||
context
|
||||
.sql
|
||||
.query_value(
|
||||
.query_get_value::<isize>(
|
||||
context,
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>?;",
|
||||
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
v as usize
|
||||
.unwrap_or_default() as usize
|
||||
}
|
||||
|
||||
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
|
||||
@@ -1004,7 +995,7 @@ SELECT c.id FROM contacts c
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM contacts WHERE id=?;",
|
||||
paramsx![contact_id as i32],
|
||||
paramsv![contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -1015,7 +1006,7 @@ SELECT c.id FROM contacts c
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||
paramsx![origin, contact_id as i32, origin],
|
||||
paramsv![origin, contact_id as i32, origin],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1077,7 +1068,7 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||
paramsx![new_blocking as i32, contact_id as i32],
|
||||
paramsv![new_blocking as i32, contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1087,42 +1078,56 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
||||
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
|
||||
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
|
||||
// this would result in recreating the same group...)
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
UPDATE chats
|
||||
SET blocked=?
|
||||
WHERE type=? AND id
|
||||
IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);
|
||||
"#,
|
||||
paramsx![new_blocking, 100i32, contact_id as i32],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Contact::mark_noticed(context, contact_id).await;
|
||||
context.emit_event(Event::ContactsChanged(None));
|
||||
}
|
||||
if context.sql.execute(
|
||||
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
|
||||
paramsv![new_blocking, 100, contact_id as i32],
|
||||
).await.is_ok() {
|
||||
Contact::mark_noticed(context, contact_id).await;
|
||||
context.emit_event(Event::ContactsChanged(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set profile image for a contact.
|
||||
///
|
||||
/// The given profile image is expected to be already in the blob directory
|
||||
/// as profile images can be set only by receiving messages, this should be always the case, however.
|
||||
///
|
||||
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar;
|
||||
/// this typically happens if we see message with our own profile image, sent from another device.
|
||||
pub(crate) async fn set_profile_image(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
profile_image: &AvatarAction,
|
||||
was_encrypted: bool,
|
||||
) -> Result<()> {
|
||||
// the given profile image is expected to be already in the blob directory
|
||||
// as profile images can be set only by receiving messages, this should be always the case, however.
|
||||
let mut contact = Contact::load_from_db(context, contact_id).await?;
|
||||
let changed = match profile_image {
|
||||
AvatarAction::Change(profile_image) => {
|
||||
contact.param.set(Param::ProfileImage, profile_image);
|
||||
if contact_id == DC_CONTACT_ID_SELF {
|
||||
if was_encrypted {
|
||||
context
|
||||
.set_config(Config::Selfavatar, Some(profile_image))
|
||||
.await?;
|
||||
} else {
|
||||
info!(context, "Do not use unencrypted selfavatar.");
|
||||
}
|
||||
} else {
|
||||
contact.param.set(Param::ProfileImage, profile_image);
|
||||
}
|
||||
true
|
||||
}
|
||||
AvatarAction::Delete => {
|
||||
contact.param.remove(Param::ProfileImage);
|
||||
if contact_id == DC_CONTACT_ID_SELF {
|
||||
if was_encrypted {
|
||||
context.set_config(Config::Selfavatar, None).await?;
|
||||
} else {
|
||||
info!(context, "Do not use unencrypted selfavatar deletion.");
|
||||
}
|
||||
} else {
|
||||
contact.param.remove(Param::ProfileImage);
|
||||
}
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
177
src/context.rs
177
src/context.rs
@@ -6,6 +6,7 @@ use std::ops::Deref;
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
|
||||
use async_std::task;
|
||||
|
||||
use crate::chat::*;
|
||||
use crate::config::Config;
|
||||
@@ -14,12 +15,10 @@ 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::lot::Lot;
|
||||
use crate::message::{self, Message, MessengerMessage, MsgId};
|
||||
use crate::param::Params;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::sql::Sql;
|
||||
use std::time::SystemTime;
|
||||
@@ -52,10 +51,13 @@ pub struct InnerContext {
|
||||
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<()>>>,
|
||||
|
||||
creation_time: SystemTime,
|
||||
}
|
||||
@@ -76,11 +78,7 @@ 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));
|
||||
|
||||
let version =
|
||||
String::from_utf8(libsqlite3_sys::SQLITE_VERSION.to_vec()).expect("invalid version");
|
||||
res.insert("sqlite_version", version);
|
||||
|
||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||
res.insert("level", "awesome".into());
|
||||
res
|
||||
@@ -89,6 +87,8 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
impl Context {
|
||||
/// Creates new context.
|
||||
pub async fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
|
||||
// pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let mut blob_fname = OsString::new();
|
||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
blob_fname.push("-blobs");
|
||||
@@ -120,17 +120,21 @@ impl Context {
|
||||
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(),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
inner: Arc::new(inner),
|
||||
};
|
||||
|
||||
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
|
||||
ensure!(
|
||||
ctx.sql.open(&ctx, &ctx.dbfile, false).await,
|
||||
"Failed opening sqlite database"
|
||||
);
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
@@ -263,29 +267,27 @@ impl Context {
|
||||
let is_configured = self.get_config_int(Config::Configured).await;
|
||||
let dbversion = self
|
||||
.sql
|
||||
.get_raw_config_int("dbversion")
|
||||
.get_raw_config_int(self, "dbversion")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let journal_mode = self
|
||||
.sql
|
||||
.query_value("PRAGMA journal_mode;", paramsx![])
|
||||
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
|
||||
.await
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
.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: Option<i32> = self
|
||||
let prv_key_cnt: Option<isize> = self
|
||||
.sql
|
||||
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
|
||||
.await
|
||||
.ok();
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||
.await;
|
||||
|
||||
let pub_key_cnt: Option<i32> = self
|
||||
let pub_key_cnt: Option<isize> = self
|
||||
.sql
|
||||
.query_value("SELECT COUNT(*) FROM acpeerstates;", paramsx![])
|
||||
.await
|
||||
.ok();
|
||||
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
|
||||
.await;
|
||||
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
||||
Ok(key) => key.fingerprint().hex(),
|
||||
Err(err) => format!("<key failure: {}>", err),
|
||||
@@ -297,7 +299,7 @@ impl Context {
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
|
||||
let folders_configured = self
|
||||
.sql
|
||||
.get_raw_config_int("folders_configured")
|
||||
.get_raw_config_int(self, "folders_configured")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -358,22 +360,30 @@ impl Context {
|
||||
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 }],
|
||||
.query_map(
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
" ON m.from_id=ct.id",
|
||||
" LEFT JOIN chats c",
|
||||
" ON m.chat_id=c.id",
|
||||
" WHERE m.state=?",
|
||||
" AND m.hidden=0",
|
||||
" AND m.chat_id>?",
|
||||
" AND ct.blocked=0",
|
||||
" AND (c.blocked=0 OR c.blocked=?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
),
|
||||
paramsv![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for row in rows {
|
||||
ret.push(row?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -389,36 +399,47 @@ SELECT m.id
|
||||
let strLikeBeg = format!("{}%", real_query);
|
||||
|
||||
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;
|
||||
"#
|
||||
concat!(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
" ON m.from_id=ct.id",
|
||||
" WHERE m.chat_id=?",
|
||||
" AND m.hidden=0",
|
||||
" AND ct.blocked=0",
|
||||
" AND (txt LIKE ? OR ct.name LIKE ?)",
|
||||
" ORDER BY m.timestamp,m.id;"
|
||||
)
|
||||
} else {
|
||||
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;
|
||||
"#
|
||||
concat!(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
" ON m.from_id=ct.id",
|
||||
" LEFT JOIN chats c",
|
||||
" ON m.chat_id=c.id",
|
||||
" WHERE m.chat_id>9",
|
||||
" AND m.hidden=0",
|
||||
" AND (c.blocked=0 OR c.blocked=?)",
|
||||
" AND ct.blocked=0",
|
||||
" AND (m.txt LIKE ? OR ct.name LIKE ?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
)
|
||||
};
|
||||
|
||||
self.sql
|
||||
.query_values(query, paramsx![chat_id, strLikeInText, strLikeBeg])
|
||||
.query_map(
|
||||
query,
|
||||
paramsv![chat_id, strLikeInText, strLikeBeg],
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -437,34 +458,6 @@ SELECT m.id
|
||||
self.get_config(Config::ConfiguredMvboxFolder).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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InnerContext {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use async_std::prelude::*;
|
||||
use itertools::join;
|
||||
use mailparse::SingleInfo;
|
||||
use num_traits::FromPrimitive;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use mailparse::SingleInfo;
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
|
||||
use crate::error::{bail, ensure, format_err, Result};
|
||||
use crate::events::Event;
|
||||
use crate::headerdef::HeaderDef;
|
||||
@@ -196,7 +197,14 @@ pub async fn dc_receive_imf(
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.user_avatar {
|
||||
match contact::set_profile_image(&context, from_id, avatar_action).await {
|
||||
match contact::set_profile_image(
|
||||
&context,
|
||||
from_id,
|
||||
avatar_action,
|
||||
mime_parser.was_encrypted(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
}
|
||||
@@ -223,24 +231,29 @@ pub async fn dc_receive_imf(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
} else if insert_msg_id
|
||||
.needs_move(context, server_folder.as_ref())
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// Move message if we don't delete it immediately.
|
||||
context
|
||||
.do_heuristics_moves(server_folder.as_ref(), insert_msg_id)
|
||||
.await;
|
||||
if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
|
||||
// This is a Delta Chat MDN. Mark as read.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(
|
||||
Action::MarkseenMsgOnImap,
|
||||
insert_msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MoveMsg, insert_msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
|
||||
// This is a Delta Chat MDN. Mark as read.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(
|
||||
Action::MarkseenMsgOnImap,
|
||||
insert_msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,16 +632,78 @@ async fn add_parts(
|
||||
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract ephemeral timer from the message.
|
||||
let mut ephemeral_timer = if let Some(value) = mime_parser.get(HeaderDef::EphemeralTimer) {
|
||||
match value.parse::<EphemeralTimer>() {
|
||||
Ok(timer) => timer,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"can't parse ephemeral timer \"{}\": {}", value, err
|
||||
);
|
||||
EphemeralTimer::Disabled
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EphemeralTimer::Disabled
|
||||
};
|
||||
|
||||
let location_kml_is = mime_parser.location_kml.is_some();
|
||||
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
||||
|
||||
// Apply ephemeral timer changes to the chat.
|
||||
//
|
||||
// Only non-hidden timers are applied now. Timers from hidden
|
||||
// messages such as read receipts can be useful to detect
|
||||
// ephemeral timer support, but timer changes without visible
|
||||
// received messages may be confusing to the user.
|
||||
if !*hidden
|
||||
&& !location_kml_is
|
||||
&& !is_mdn
|
||||
&& (*chat_id).get_ephemeral_timer(context).await? != ephemeral_timer
|
||||
{
|
||||
match (*chat_id)
|
||||
.inner_set_ephemeral_timer(context, ephemeral_timer)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
|
||||
set_better_msg(
|
||||
mime_parser,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
);
|
||||
|
||||
// Do not delete the system message itself.
|
||||
//
|
||||
// This prevents confusion when timer is changed
|
||||
// to 1 week, and then changed to 1 hour: after 1
|
||||
// hour, only the message about the change to 1
|
||||
// week is left.
|
||||
ephemeral_timer = EphemeralTimer::Disabled;
|
||||
} else {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
*chat_id,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"failed to modify timer for chat {}: {}", chat_id, err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// correct message_timestamp, it should not be used before,
|
||||
// however, we cannot do this earlier as we need from_id to be set
|
||||
let in_fresh = state == MessageState::InFresh;
|
||||
let rcvd_timestamp = time();
|
||||
let sort_timestamp = calc_sort_timestamp(
|
||||
context,
|
||||
*sent_timestamp,
|
||||
*chat_id,
|
||||
state == MessageState::InFresh,
|
||||
)
|
||||
.await;
|
||||
let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, *chat_id, in_fresh).await;
|
||||
*sent_timestamp = std::cmp::min(*sent_timestamp, rcvd_timestamp);
|
||||
|
||||
// unarchive chat
|
||||
@@ -652,7 +727,9 @@ async fn add_parts(
|
||||
let icnt = mime_parser.parts.len();
|
||||
|
||||
let subject = mime_parser.get_subject().unwrap_or_default();
|
||||
let location_kml_is = mime_parser.location_kml.is_some();
|
||||
|
||||
let mut parts = std::mem::replace(&mut mime_parser.parts, Vec::new());
|
||||
let server_folder = server_folder.as_ref().to_string();
|
||||
let is_system_message = mime_parser.is_system_message;
|
||||
let mime_headers = if save_mime_headers {
|
||||
Some(String::from_utf8_lossy(imf_raw).to_string())
|
||||
@@ -660,43 +737,59 @@ async fn add_parts(
|
||||
None
|
||||
};
|
||||
let sent_timestamp = *sent_timestamp;
|
||||
let is_hidden = *hidden;
|
||||
let chat_id = *chat_id;
|
||||
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
||||
|
||||
for part in &mut mime_parser.parts {
|
||||
let mut txt_raw = "".to_string();
|
||||
// TODO: can this clone be avoided?
|
||||
let rfc724_mid = rfc724_mid.to_string();
|
||||
|
||||
let is_location_kml =
|
||||
location_kml_is && icnt == 1 && (part.msg == "-location-" || part.msg.is_empty());
|
||||
let (new_parts, ids, is_hidden) = context
|
||||
.sql
|
||||
.with_conn(move |mut conn| {
|
||||
let mut ids = Vec::with_capacity(parts.len());
|
||||
let mut is_hidden = is_hidden;
|
||||
|
||||
if is_mdn || is_location_kml {
|
||||
*hidden = true;
|
||||
if state == MessageState::InFresh {
|
||||
state = MessageState::InNoticed;
|
||||
}
|
||||
}
|
||||
for part in &mut parts {
|
||||
let mut txt_raw = "".to_string();
|
||||
let mut stmt = conn.prepare_cached(
|
||||
"INSERT INTO msgs \
|
||||
(rfc724_mid, server_folder, server_uid, chat_id, from_id, to_id, timestamp, \
|
||||
timestamp_sent, timestamp_rcvd, type, state, msgrmsg, txt, txt_raw, param, \
|
||||
bytes, hidden, mime_headers, mime_in_reply_to, mime_references, error, ephemeral_timer, ephemeral_timestamp) \
|
||||
VALUES (?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?, ?,?, ?,?,?);",
|
||||
)?;
|
||||
|
||||
if part.typ == Viewtype::Text {
|
||||
let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default();
|
||||
txt_raw = format!("{}\n\n{}", subject, msg_raw);
|
||||
}
|
||||
if is_system_message != SystemMessage::Unknown {
|
||||
part.param.set_int(Param::Cmd, is_system_message as i32);
|
||||
}
|
||||
let is_location_kml = location_kml_is
|
||||
&& icnt == 1
|
||||
&& (part.msg == "-location-" || part.msg.is_empty());
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
INSERT INTO msgs (
|
||||
rfc724_mid, server_folder, server_uid, chat_id, from_id, to_id, timestamp,
|
||||
timestamp_sent, timestamp_rcvd, type, state, msgrmsg, txt, txt_raw, param,
|
||||
bytes, hidden, mime_headers, mime_in_reply_to, mime_references)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);
|
||||
"#,
|
||||
paramsx![
|
||||
rfc724_mid.to_owned(),
|
||||
server_folder.as_ref().to_owned(),
|
||||
if is_mdn || is_location_kml {
|
||||
is_hidden = true;
|
||||
if state == MessageState::InFresh {
|
||||
state = MessageState::InNoticed;
|
||||
}
|
||||
}
|
||||
|
||||
if part.typ == Viewtype::Text {
|
||||
let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default();
|
||||
txt_raw = format!("{}\n\n{}", subject, msg_raw);
|
||||
}
|
||||
if is_system_message != SystemMessage::Unknown {
|
||||
part.param.set_int(Param::Cmd, is_system_message as i32);
|
||||
}
|
||||
|
||||
let ephemeral_timestamp = if in_fresh {
|
||||
0
|
||||
} else {
|
||||
match ephemeral_timer {
|
||||
EphemeralTimer::Disabled => 0,
|
||||
EphemeralTimer::Enabled { duration } => rcvd_timestamp + i64::from(duration)
|
||||
}
|
||||
};
|
||||
|
||||
stmt.execute(paramsv![
|
||||
rfc724_mid,
|
||||
server_folder,
|
||||
server_uid as i32,
|
||||
chat_id,
|
||||
from_id as i32,
|
||||
@@ -707,30 +800,40 @@ INSERT INTO msgs (
|
||||
part.typ,
|
||||
state,
|
||||
is_dc_message,
|
||||
part.msg.clone(),
|
||||
part.msg,
|
||||
// txt_raw might contain invalid utf8
|
||||
txt_raw,
|
||||
part.param.to_string(),
|
||||
part.bytes as i64,
|
||||
*hidden,
|
||||
mime_headers.clone(),
|
||||
mime_in_reply_to.clone(),
|
||||
mime_references.clone(),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
part.bytes as isize,
|
||||
is_hidden,
|
||||
mime_headers,
|
||||
mime_in_reply_to,
|
||||
mime_references,
|
||||
part.error,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp
|
||||
])?;
|
||||
|
||||
let msg_id = MsgId::new(
|
||||
context
|
||||
.sql
|
||||
.get_rowid("msgs", "rfc724_mid", &rfc724_mid)
|
||||
.await?,
|
||||
);
|
||||
drop(stmt);
|
||||
ids.push(MsgId::new(crate::sql::get_rowid(
|
||||
&mut conn,
|
||||
"msgs",
|
||||
"rfc724_mid",
|
||||
&rfc724_mid,
|
||||
)?));
|
||||
}
|
||||
Ok((parts, ids, is_hidden))
|
||||
})
|
||||
.await?;
|
||||
|
||||
*insert_msg_id = msg_id;
|
||||
created_db_entries.push((chat_id, msg_id));
|
||||
if let Some(id) = ids.iter().last() {
|
||||
*insert_msg_id = *id;
|
||||
}
|
||||
|
||||
*hidden = is_hidden;
|
||||
created_db_entries.extend(ids.iter().map(|id| (chat_id, *id)));
|
||||
mime_parser.parts = new_parts;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Message has {} parts and is assigned to chat #{}.", icnt, chat_id,
|
||||
@@ -764,15 +867,17 @@ INSERT INTO msgs (
|
||||
chat.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
update_last_subject(context, chat_id, mime_parser)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
context,
|
||||
"Could not update LastSubject of chat: {}",
|
||||
e.to_string()
|
||||
)
|
||||
});
|
||||
if !is_mdn {
|
||||
update_last_subject(context, chat_id, mime_parser)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
context,
|
||||
"Could not update LastSubject of chat: {}",
|
||||
e.to_string()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -848,17 +953,18 @@ async fn calc_sort_timestamp(
|
||||
// get newest non fresh message for this chat
|
||||
// update sort_timestamp if less than that
|
||||
if is_fresh_msg {
|
||||
let last_msg_time: Result<i32, _> = context
|
||||
let last_msg_time: Option<i64> = context
|
||||
.sql
|
||||
.query_value(
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?",
|
||||
paramsx![chat_id, MessageState::InFresh],
|
||||
paramsv![chat_id, MessageState::InFresh],
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(last_msg_time) = last_msg_time {
|
||||
if last_msg_time as i64 > sort_timestamp {
|
||||
sort_timestamp = last_msg_time as i64;
|
||||
if let Some(last_msg_time) = last_msg_time {
|
||||
if last_msg_time > sort_timestamp {
|
||||
sort_timestamp = last_msg_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1093,23 +1199,19 @@ async fn create_or_lookup_group(
|
||||
|
||||
// again, check chat_id
|
||||
if chat_id.is_special() {
|
||||
return if group_explicitly_left {
|
||||
Ok((ChatId::new(DC_CHAT_ID_TRASH), chat_id_blocked))
|
||||
if mime_parser.decrypting_failed {
|
||||
// It is possible that the message was sent to a valid,
|
||||
// yet unknown group, which was rejected because
|
||||
// Chat-Group-Name, which is in the encrypted part, was
|
||||
// not found. We can't create a properly named group in
|
||||
// this case, so assign error message to 1:1 chat with the
|
||||
// sender instead.
|
||||
return Ok((ChatId::new(0), Blocked::Not));
|
||||
} else {
|
||||
create_or_lookup_adhoc_group(
|
||||
context,
|
||||
mime_parser,
|
||||
allow_creation,
|
||||
create_blocked,
|
||||
from_id,
|
||||
to_ids,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!(context, "failed to create ad-hoc group: {:?}", err);
|
||||
err
|
||||
})
|
||||
};
|
||||
// The message was decrypted successfully, but contains a late "quit" or otherwise
|
||||
// unwanted message.
|
||||
return Ok((ChatId::new(DC_CHAT_ID_TRASH), chat_id_blocked));
|
||||
}
|
||||
}
|
||||
|
||||
// We have a valid chat_id > DC_CHAT_ID_LAST_SPECIAL.
|
||||
@@ -1135,7 +1237,7 @@ async fn create_or_lookup_group(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=?;",
|
||||
paramsx![grpname, chat_id],
|
||||
paramsv![grpname.to_string(), chat_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1171,7 +1273,7 @@ async fn create_or_lookup_group(
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||
paramsx![chat_id],
|
||||
paramsv![chat_id],
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
@@ -1253,10 +1355,10 @@ async fn create_or_lookup_adhoc_group(
|
||||
let chat_ids = search_chat_ids_by_contact_ids(context, &member_ids).await?;
|
||||
if !chat_ids.is_empty() {
|
||||
let chat_ids_str = join(chat_ids.iter().map(|x| x.to_string()), ",");
|
||||
let res: Result<(ChatId, Option<Blocked>), _> = context
|
||||
let res = context
|
||||
.sql
|
||||
.query_row(
|
||||
&format!(
|
||||
format!(
|
||||
"SELECT c.id,
|
||||
c.blocked
|
||||
FROM chats c
|
||||
@@ -1268,13 +1370,19 @@ async fn create_or_lookup_adhoc_group(
|
||||
LIMIT 1;",
|
||||
chat_ids_str
|
||||
),
|
||||
paramsx![],
|
||||
paramsv![],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, ChatId>(0)?,
|
||||
row.get::<_, Option<Blocked>>(1)?.unwrap_or_default(),
|
||||
))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok((id, id_blocked)) = res {
|
||||
/* success, chat found */
|
||||
return Ok((id, id_blocked.unwrap_or_default()));
|
||||
return Ok((id, id_blocked));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1288,12 +1396,10 @@ async fn create_or_lookup_adhoc_group(
|
||||
// decrypted.
|
||||
//
|
||||
// The subject may be encrypted and contain a placeholder such
|
||||
// as "...". Besides that, it is possible that the message was
|
||||
// sent to a valid, yet unknown group, which was rejected
|
||||
// because Chat-Group-Name, which is in the encrypted part,
|
||||
// was not found. Generating a new ID in this case would
|
||||
// result in creation of a twin group with a different group
|
||||
// ID.
|
||||
// as "...". It can also be a COI group, with encrypted
|
||||
// Chat-Group-ID and incompatible Message-ID format.
|
||||
//
|
||||
// Instead, assign the message to 1:1 chat with the sender.
|
||||
warn!(
|
||||
context,
|
||||
"not creating ad-hoc group for message that cannot be decrypted"
|
||||
@@ -1306,7 +1412,7 @@ async fn create_or_lookup_adhoc_group(
|
||||
|
||||
// create a new ad-hoc group
|
||||
// - there is no need to check if this group exists; otherwise we would have caught it above
|
||||
let grpid = create_adhoc_grp_id(context, &member_ids).await?;
|
||||
let grpid = create_adhoc_grp_id(context, &member_ids).await;
|
||||
if grpid.is_empty() {
|
||||
warn!(
|
||||
context,
|
||||
@@ -1346,7 +1452,7 @@ async fn create_group_record(
|
||||
) -> ChatId {
|
||||
if context.sql.execute(
|
||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?);",
|
||||
paramsx![
|
||||
paramsv![
|
||||
if VerifiedStatus::Unverified != create_verified {
|
||||
Chattype::VerifiedGroup
|
||||
} else {
|
||||
@@ -1355,7 +1461,7 @@ async fn create_group_record(
|
||||
grpname.as_ref(),
|
||||
grpid.as_ref(),
|
||||
create_blocked,
|
||||
time()
|
||||
time(),
|
||||
],
|
||||
).await
|
||||
.is_err()
|
||||
@@ -1370,7 +1476,7 @@ async fn create_group_record(
|
||||
}
|
||||
let row_id = context
|
||||
.sql
|
||||
.get_rowid("chats", "grpid", grpid.as_ref())
|
||||
.get_rowid(context, "chats", "grpid", grpid.as_ref())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -1385,7 +1491,7 @@ async fn create_group_record(
|
||||
chat_id
|
||||
}
|
||||
|
||||
async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> Result<String> {
|
||||
async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String {
|
||||
/* algorithm:
|
||||
- sort normalized, lowercased, e-mail addresses alphabetically
|
||||
- put all e-mail addresses into a single string, separate the address by a single comma
|
||||
@@ -1399,22 +1505,33 @@ async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> Result<St
|
||||
.unwrap_or_else(|| "no-self".to_string())
|
||||
.to_lowercase();
|
||||
|
||||
let query = format!(
|
||||
"SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF
|
||||
member_ids_str
|
||||
);
|
||||
let mut addrs: Vec<String> = context.sql.query_values(&query, paramsx![]).await?;
|
||||
addrs.sort();
|
||||
let members = context
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
"SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF
|
||||
member_ids_str
|
||||
),
|
||||
paramsv![],
|
||||
|row| row.get::<_, String>(0),
|
||||
|rows| {
|
||||
let mut addrs = rows.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
addrs.sort();
|
||||
let mut acc = member_cs.clone();
|
||||
for addr in &addrs {
|
||||
acc += ",";
|
||||
acc += &addr.to_lowercase();
|
||||
}
|
||||
Ok(acc)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|_| member_cs);
|
||||
|
||||
let mut acc = member_cs;
|
||||
for addr in &addrs {
|
||||
acc += ",";
|
||||
acc += &addr.to_lowercase();
|
||||
}
|
||||
|
||||
Ok(hex_hash(&acc))
|
||||
hex_hash(&members)
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn hex_hash(s: impl AsRef<str>) -> String {
|
||||
let bytes = s.as_ref().as_bytes();
|
||||
let result = Sha256::digest(bytes);
|
||||
@@ -1439,7 +1556,8 @@ async fn search_chat_ids_by_contact_ids(
|
||||
if !contact_ids.is_empty() {
|
||||
contact_ids.sort();
|
||||
let contact_ids_str = join(contact_ids.iter().map(|x| x.to_string()), ",");
|
||||
let query = format!(
|
||||
context.sql.query_map(
|
||||
format!(
|
||||
"SELECT DISTINCT cc.chat_id, cc.contact_id
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN chats c ON c.id=cc.chat_id
|
||||
@@ -1448,37 +1566,37 @@ async fn search_chat_ids_by_contact_ids(
|
||||
AND cc.contact_id!=1
|
||||
ORDER BY cc.chat_id, cc.contact_id;", // 1=DC_CONTACT_ID_SELF
|
||||
contact_ids_str
|
||||
);
|
||||
),
|
||||
paramsv![],
|
||||
|row| Ok((row.get::<_, ChatId>(0)?, row.get::<_, u32>(1)?)),
|
||||
|rows| {
|
||||
let mut last_chat_id = ChatId::new(0);
|
||||
let mut matches = 0;
|
||||
let mut mismatches = 0;
|
||||
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(&query).fetch(&pool);
|
||||
for row in rows {
|
||||
let (chat_id, contact_id) = row?;
|
||||
if chat_id != last_chat_id {
|
||||
if matches == contact_ids.len() && mismatches == 0 {
|
||||
chat_ids.push(last_chat_id);
|
||||
}
|
||||
last_chat_id = chat_id;
|
||||
matches = 0;
|
||||
mismatches = 0;
|
||||
}
|
||||
if contact_ids.get(matches) == Some(&contact_id) {
|
||||
matches += 1;
|
||||
} else {
|
||||
mismatches += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut last_chat_id = ChatId::new(0);
|
||||
let mut matches = 0;
|
||||
let mut mismatches = 0;
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (chat_id, contact_id): (ChatId, i32) = row?;
|
||||
let contact_id = contact_id as u32;
|
||||
|
||||
if chat_id != last_chat_id {
|
||||
if matches == contact_ids.len() && mismatches == 0 {
|
||||
chat_ids.push(last_chat_id);
|
||||
}
|
||||
last_chat_id = chat_id;
|
||||
matches = 0;
|
||||
mismatches = 0;
|
||||
Ok(())
|
||||
}
|
||||
if matches < contact_ids.len() && contact_id == contact_ids[matches] {
|
||||
matches += 1;
|
||||
} else {
|
||||
mismatches += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if matches == contact_ids.len() && mismatches == 0 {
|
||||
chat_ids.push(last_chat_id);
|
||||
}
|
||||
).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1502,10 +1620,8 @@ async fn check_verified_properties(
|
||||
if from_id != DC_CONTACT_ID_SELF {
|
||||
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await;
|
||||
|
||||
if peerstate.is_err()
|
||||
|| contact
|
||||
.is_verified_ex(context, peerstate.as_ref().ok())
|
||||
.await
|
||||
if peerstate.is_none()
|
||||
|| contact.is_verified_ex(context, peerstate.as_ref()).await
|
||||
!= VerifiedStatus::BidirectVerified
|
||||
{
|
||||
bail!(
|
||||
@@ -1514,7 +1630,7 @@ async fn check_verified_properties(
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(peerstate) = peerstate {
|
||||
if let Some(peerstate) = peerstate {
|
||||
ensure!(
|
||||
peerstate.has_verified_key(&mimeparser.signatures),
|
||||
"The message was sent with non-verified encryption."
|
||||
@@ -1531,30 +1647,36 @@ async fn check_verified_properties(
|
||||
}
|
||||
let to_ids_str = join(to_ids.iter().map(|x| x.to_string()), ",");
|
||||
|
||||
let query = format!(
|
||||
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
|
||||
to_ids_str
|
||||
);
|
||||
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(&query).fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (to_addr, is_verified): (String, i32) = row?;
|
||||
to_ids_str
|
||||
),
|
||||
paramsv![],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, i32>(1).unwrap_or(0))),
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
for (to_addr, _is_verified) in rows.into_iter() {
|
||||
info!(
|
||||
context,
|
||||
"check_verified_properties: {:?} self={:?}",
|
||||
to_addr,
|
||||
context.is_self_addr(&to_addr).await
|
||||
);
|
||||
let mut is_verified = is_verified != 0;
|
||||
let mut is_verified = _is_verified != 0;
|
||||
let peerstate = Peerstate::from_addr(context, &to_addr).await;
|
||||
|
||||
// mark gossiped keys (if any) as verified
|
||||
if mimeparser.gossipped_addr.contains(&to_addr) {
|
||||
if let Ok(mut peerstate) = peerstate {
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// if we're here, we know the gossip key is verified:
|
||||
// - use the gossip-key as verified-key if there is no verified-key
|
||||
// - OR if the verified-key does not match public-key or gossip-key
|
||||
@@ -1590,10 +1712,11 @@ async fn check_verified_properties(
|
||||
|
||||
fn set_better_msg(mime_parser: &mut MimeMessage, better_msg: impl AsRef<str>) {
|
||||
let msg = better_msg.as_ref();
|
||||
if !msg.is_empty() && !mime_parser.parts.is_empty() {
|
||||
let part = &mut mime_parser.parts[0];
|
||||
if part.typ == Viewtype::Text {
|
||||
part.msg = msg.to_string();
|
||||
if !msg.is_empty() {
|
||||
if let Some(part) = mime_parser.parts.get_mut(0) {
|
||||
if part.typ == Viewtype::Text {
|
||||
part.msg = msg.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1644,7 +1767,7 @@ async fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||
LEFT JOIN chats c ON m.chat_id=c.id \
|
||||
WHERE m.rfc724_mid=? \
|
||||
AND m.chat_id>9 AND c.blocked=0;",
|
||||
paramsx![rfc724_mid],
|
||||
paramsv![rfc724_mid],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -1690,7 +1813,7 @@ async fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM msgs WHERE rfc724_mid=? AND msgrmsg!=0 AND chat_id>9;",
|
||||
paramsx![rfc724_mid],
|
||||
paramsv![rfc724_mid],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -1752,7 +1875,7 @@ fn dc_create_incoming_rfc724_mid(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::ChatVisibility;
|
||||
use crate::chat::{ChatItem, ChatVisibility};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::*;
|
||||
@@ -1961,32 +2084,14 @@ mod tests {
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert_eq!(chat.name, "Bob");
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&t.ctx, chat_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t.ctx, chat_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 1);
|
||||
assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.len(), 1);
|
||||
|
||||
// receive a non-delta-message from Bob, shows up because of the show_emails setting
|
||||
dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 2, false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t.ctx, chat_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.len(), 2);
|
||||
|
||||
// let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 3, false)
|
||||
@@ -2000,13 +2105,7 @@ mod tests {
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat.name, "group with Alice, Bob and Claire");
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&t.ctx, chat_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 3);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -2030,13 +2129,7 @@ mod tests {
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat.name, "group with Alice, Bob and Claire");
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&t.ctx, chat_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 3);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -2062,10 +2155,7 @@ mod tests {
|
||||
.unwrap();
|
||||
chat::add_contact_to_chat(&t.ctx, group_id, bob_id).await;
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t.ctx, group_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
chat::get_chat_msgs(&t.ctx, group_id, 0, None).await.len(),
|
||||
0
|
||||
);
|
||||
group_id
|
||||
@@ -2108,11 +2198,13 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, group_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, group_id, 0, None).await;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = msgs.first().unwrap();
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2161,10 +2253,7 @@ mod tests {
|
||||
)
|
||||
.await.unwrap();
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t.ctx, group_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
chat::get_chat_msgs(&t.ctx, group_id, 0, None).await.len(),
|
||||
1
|
||||
);
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
@@ -2248,9 +2337,13 @@ mod tests {
|
||||
.get_authname(),
|
||||
"Имя, Фамилия",
|
||||
);
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = msgs.first().unwrap();
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2512,12 +2605,13 @@ mod tests {
|
||||
|
||||
assert_eq!(msg.state, MessageState::OutFailed);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, msg.chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let last_msg = Message::load_from_db(&t.ctx, *msgs.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, msg.chat_id, 0, None).await;
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let last_msg = Message::load_from_db(&t.ctx, *msg_id).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
last_msg.text,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
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};
|
||||
|
||||
@@ -22,6 +23,7 @@ pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
||||
|
||||
/// 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 = "[...]";
|
||||
|
||||
@@ -54,6 +56,7 @@ const COLORS: [u32; 16] = [
|
||||
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
|
||||
];
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
|
||||
let str_lower = s.as_ref().to_lowercase();
|
||||
let mut checksum = 0;
|
||||
@@ -198,7 +201,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('@')
|
||||
.map(|k| &from_addr[k..])
|
||||
.and_then(|k| from_addr.get(k..))
|
||||
.unwrap_or("@nohost");
|
||||
match grpid {
|
||||
Some(grpid) => format!("Gr.{}.{}{}", grpid, dc_create_id(), hostname),
|
||||
@@ -240,9 +243,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 meta = image_meta::load_from_buf(buf)?;
|
||||
|
||||
Ok((meta.dimensions.width, meta.dimensions.height))
|
||||
let image = image::io::Reader::new(Cursor::new(buf));
|
||||
let dimensions = image.into_dimensions()?;
|
||||
Ok(dimensions)
|
||||
}
|
||||
|
||||
/// Expand paths relative to $BLOBDIR into absolute paths.
|
||||
@@ -568,6 +571,14 @@ impl FromStr for EmailAddress {
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for EmailAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Text(self.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
46
src/e2ee.rs
46
src/e2ee.rs
@@ -91,7 +91,7 @@ impl EncryptHelper {
|
||||
context: &Context,
|
||||
min_verified: PeerstateVerifiedStatus,
|
||||
mail_to_encrypt: lettre_email::PartBuilder,
|
||||
peerstates: Vec<(Option<Peerstate>, &str)>,
|
||||
peerstates: Vec<(Option<Peerstate<'_>>, &str)>,
|
||||
) -> Result<String> {
|
||||
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
|
||||
|
||||
@@ -115,6 +115,12 @@ impl EncryptHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to decrypt a message, but only if it is structured as an
|
||||
/// Autocrypt message, i.e. encrypted and signed with a valid
|
||||
/// signature.
|
||||
///
|
||||
/// Returns decrypted body and a set of valid signature fingerprints
|
||||
/// if successful.
|
||||
pub async fn try_decrypt(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
@@ -132,7 +138,7 @@ pub async fn try_decrypt(
|
||||
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
|
||||
|
||||
if message_time > 0 {
|
||||
peerstate = Peerstate::from_addr(context, &from).await.ok();
|
||||
peerstate = Peerstate::from_addr(context, &from).await;
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if let Some(ref header) = autocryptheader {
|
||||
@@ -143,7 +149,7 @@ pub async fn try_decrypt(
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
} else if let Some(ref header) = autocryptheader {
|
||||
let p = Peerstate::from_header(header, message_time);
|
||||
let p = Peerstate::from_header(context, header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
@@ -155,7 +161,7 @@ pub async fn try_decrypt(
|
||||
let mut signatures = HashSet::default();
|
||||
|
||||
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
|
||||
peerstate = Peerstate::from_addr(&context, &from).await.ok();
|
||||
peerstate = Peerstate::from_addr(&context, &from).await;
|
||||
}
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.degrade_event.is_some() {
|
||||
@@ -187,24 +193,23 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
|
||||
"Not a multipart/encrypted message: {}",
|
||||
mail.ctype.mimetype
|
||||
);
|
||||
ensure!(
|
||||
mail.subparts.len() == 2,
|
||||
"Invalid Autocrypt Level 1 Mime Parts"
|
||||
);
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
ensure!(
|
||||
first_part.ctype.mimetype == "application/pgp-encrypted",
|
||||
"Invalid Autocrypt Level 1 version part: {:?}",
|
||||
first_part.ctype,
|
||||
);
|
||||
|
||||
ensure!(
|
||||
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
|
||||
"Invalid Autocrypt Level 1 version part: {:?}",
|
||||
mail.subparts[0].ctype,
|
||||
);
|
||||
ensure!(
|
||||
second_part.ctype.mimetype == "application/octet-stream",
|
||||
"Invalid Autocrypt Level 1 encrypted part: {:?}",
|
||||
second_part.ctype
|
||||
);
|
||||
|
||||
ensure!(
|
||||
mail.subparts[1].ctype.mimetype == "application/octet-stream",
|
||||
"Invalid Autocrypt Level 1 encrypted part: {:?}",
|
||||
mail.subparts[1].ctype
|
||||
);
|
||||
|
||||
Ok(&mail.subparts[1])
|
||||
Ok(second_part)
|
||||
} else {
|
||||
bail!("Invalid Autocrypt Level 1 Mime Parts")
|
||||
}
|
||||
}
|
||||
|
||||
async fn decrypt_if_autocrypt_message<'a>(
|
||||
@@ -267,6 +272,7 @@ async fn decrypt_part(
|
||||
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 {
|
||||
|
||||
528
src/ephemeral.rs
Normal file
528
src/ephemeral.rs
Normal file
@@ -0,0 +1,528 @@
|
||||
//! # 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 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::error::{ensure, Error};
|
||||
use crate::events::Event;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
use async_std::task;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[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 rusqlite::types::ToSql for Timer {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Integer(match self {
|
||||
Self::Disabled => 0,
|
||||
Self::Enabled { duration } => i64::from(*duration),
|
||||
});
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::FromSql for Timer {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|value| {
|
||||
if value == 0 {
|
||||
Ok(Self::Disabled)
|
||||
} else if let Ok(duration) = u32::try_from(value) {
|
||||
Ok(Self::Enabled { duration })
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(value))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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_result(
|
||||
"SELECT ephemeral_timer FROM chats WHERE id=?;",
|
||||
paramsv![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(
|
||||
"UPDATE chats
|
||||
SET ephemeral_timer=?
|
||||
WHERE id=?;",
|
||||
paramsv![timer, self],
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(Event::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 {
|
||||
let stock_message = match timer {
|
||||
Timer::Disabled => StockMessage::MsgEphemeralTimerDisabled,
|
||||
Timer::Enabled { duration } => match duration {
|
||||
60 => StockMessage::MsgEphemeralTimerMinute,
|
||||
3600 => StockMessage::MsgEphemeralTimerHour,
|
||||
86400 => StockMessage::MsgEphemeralTimerDay,
|
||||
604_800 => StockMessage::MsgEphemeralTimerWeek,
|
||||
2_419_200 => StockMessage::MsgEphemeralTimerFourWeeks,
|
||||
_ => StockMessage::MsgEphemeralTimerEnabled,
|
||||
},
|
||||
};
|
||||
|
||||
context
|
||||
.stock_system_msg(stock_message, timer.to_string(), "", from_id)
|
||||
.await
|
||||
}
|
||||
|
||||
impl MsgId {
|
||||
/// Returns ephemeral message timer value for the message.
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT ephemeral_timer FROM msgs WHERE id=?",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?
|
||||
{
|
||||
None | Some(0) => Timer::Disabled,
|
||||
Some(duration) => Timer::Enabled { 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) -> crate::sql::Result<()> {
|
||||
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
|
||||
let ephemeral_timestamp = time() + i64::from(duration);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? \
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
|
||||
AND id = ?",
|
||||
paramsv![ephemeral_timestamp, ephemeral_timestamp, 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(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE \
|
||||
ephemeral_timestamp != 0 \
|
||||
AND ephemeral_timestamp < ? \
|
||||
AND chat_id != ?",
|
||||
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
|
||||
)
|
||||
.await?
|
||||
> 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(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', chat_id = ? \
|
||||
WHERE timestamp < ? \
|
||||
AND chat_id > ? \
|
||||
AND chat_id != ? \
|
||||
AND chat_id != ?",
|
||||
paramsv![
|
||||
DC_CHAT_ID_TRASH,
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
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_result(
|
||||
"SELECT ephemeral_timestamp \
|
||||
FROM msgs \
|
||||
WHERE ephemeral_timestamp != 0 \
|
||||
AND chat_id != ? \
|
||||
ORDER BY ephemeral_timestamp ASC \
|
||||
LIMIT 1",
|
||||
paramsv![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,
|
||||
Event::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,
|
||||
Event::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,
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs \
|
||||
WHERE ( \
|
||||
timestamp < ? \
|
||||
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp < ?) \
|
||||
) \
|
||||
AND server_uid != 0 \
|
||||
LIMIT 1",
|
||||
paramsv![threshold_timestamp, now],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 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(
|
||||
"UPDATE msgs \
|
||||
SET ephemeral_timestamp = ? + ephemeral_timer \
|
||||
WHERE ephemeral_timer > 0 \
|
||||
AND ephemeral_timestamp = 0 \
|
||||
AND state NOT IN (?, ?, ?)",
|
||||
paramsv![
|
||||
time(),
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
MessageState::OutDraft
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_ephemeral_messages() {
|
||||
let context = TestContext::new().await.ctx;
|
||||
|
||||
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::Disabled, 0).await,
|
||||
"Message deletion timer is disabled."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, 0).await,
|
||||
"Message deletion timer is set to 1 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, 0).await,
|
||||
"Message deletion timer is set to 30 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, 0).await,
|
||||
"Message deletion timer is set to 1 minute."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 * 60 }, 0).await,
|
||||
"Message deletion timer is set to 1 hour."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 day."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 7 * 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 week."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 4 * 7 * 24 * 60 * 60
|
||||
},
|
||||
0
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 4 weeks."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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)]
|
||||
@@ -139,7 +140,6 @@ pub enum Event {
|
||||
/// 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
|
||||
@@ -189,9 +189,19 @@ pub enum Event {
|
||||
/// 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.
|
||||
|
||||
@@ -35,6 +35,7 @@ pub enum HeaderDef {
|
||||
ChatContent,
|
||||
ChatDuration,
|
||||
ChatDispositionNotificationTo,
|
||||
ChatWebrtcRoom,
|
||||
Autocrypt,
|
||||
AutocryptSetupMessage,
|
||||
SecureJoin,
|
||||
@@ -42,6 +43,7 @@ pub enum HeaderDef {
|
||||
SecureJoinFingerprint,
|
||||
SecureJoinInvitenumber,
|
||||
SecureJoinAuth,
|
||||
EphemeralTimer,
|
||||
_TestHeader,
|
||||
}
|
||||
|
||||
|
||||
225
src/imap/idle.rs
225
src/imap/idle.rs
@@ -1,6 +1,7 @@
|
||||
use super::Imap;
|
||||
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use async_imap::types::UnsolicitedResponse;
|
||||
use async_std::prelude::*;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -13,19 +14,19 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("IMAP IDLE protocol failed to init/complete")]
|
||||
#[error("IMAP IDLE protocol failed to init/complete: {0}")]
|
||||
IdleProtocolFailed(#[from] async_imap::error::Error),
|
||||
|
||||
#[error("IMAP IDLE protocol timed out")]
|
||||
#[error("IMAP IDLE protocol timed out: {0}")]
|
||||
IdleTimeout(#[from] async_std::future::TimeoutError),
|
||||
|
||||
#[error("IMAP server does not have IDLE capability")]
|
||||
IdleAbilityMissing,
|
||||
|
||||
#[error("IMAP select folder error")]
|
||||
#[error("IMAP select folder error: {0}")]
|
||||
SelectFolderError(#[from] select_folder::Error),
|
||||
|
||||
#[error("Setup handle error")]
|
||||
#[error("Setup handle error: {0}")]
|
||||
SetupHandleError(#[from] super::Error),
|
||||
}
|
||||
|
||||
@@ -48,11 +49,27 @@ impl Imap {
|
||||
|
||||
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) = session {
|
||||
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);
|
||||
}
|
||||
|
||||
let mut handle = session.idle();
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
@@ -65,68 +82,43 @@ impl Imap {
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
|
||||
if self.skip_next_idle_wait {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait = false;
|
||||
drop(idle_wait);
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
|
||||
let probe_network = self.idle_interrupt.recv().await;
|
||||
|
||||
// cancel imap idle connection properly
|
||||
drop(interrupt);
|
||||
|
||||
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()))
|
||||
}),
|
||||
);
|
||||
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// if we can't properly terminate the idle
|
||||
// protocol let's break the connection.
|
||||
let res = handle
|
||||
let session = handle
|
||||
.done()
|
||||
.timeout(Duration::from_secs(15))
|
||||
.await
|
||||
.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));
|
||||
}
|
||||
}
|
||||
.map_err(Error::IdleTimeout)??;
|
||||
self.session = Some(Session { inner: session });
|
||||
} else {
|
||||
warn!(context, "Attempted to idle without a session");
|
||||
}
|
||||
|
||||
Ok(info)
|
||||
@@ -148,73 +140,66 @@ impl Imap {
|
||||
return self.idle_interrupt.recv().await.unwrap_or_default();
|
||||
}
|
||||
|
||||
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));
|
||||
// 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.
|
||||
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.
|
||||
|
||||
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()
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Interrupt(info) => {
|
||||
// Interrupt
|
||||
break info;
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Event::Interrupt(info) => {
|
||||
// Interrupt
|
||||
break info;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
context,
|
||||
|
||||
114
src/imap/mod.rs
114
src/imap/mod.rs
@@ -3,8 +3,6 @@
|
||||
//! uses [async-email/async-imap](https://github.com/async-email/async-imap)
|
||||
//! to implement connect, fetch, delete functionality with standard IMAP servers.
|
||||
|
||||
#![forbid(clippy::indexing_slicing)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use async_imap::{
|
||||
@@ -25,12 +23,12 @@ use crate::events::Event;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::job::{self, Action};
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::message::{self, update_server_uid};
|
||||
use crate::message::{self, update_server_uid, MessageState};
|
||||
use crate::mimeparser;
|
||||
use crate::oauth2::dc_get_oauth2_access_token;
|
||||
use crate::param::Params;
|
||||
use crate::provider::get_provider_info;
|
||||
use crate::{scheduler::InterruptInfo, stock::StockMessage};
|
||||
use crate::{chat, scheduler::InterruptInfo, stock::StockMessage};
|
||||
|
||||
mod client;
|
||||
mod idle;
|
||||
@@ -38,6 +36,7 @@ pub mod select_folder;
|
||||
mod session;
|
||||
|
||||
use client::Client;
|
||||
use message::Message;
|
||||
use session::Session;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -117,8 +116,8 @@ pub struct Imap {
|
||||
session: Option<Session>,
|
||||
connected: bool,
|
||||
interrupt: Option<stop_token::StopSource>,
|
||||
skip_next_idle_wait: bool,
|
||||
should_reconnect: bool,
|
||||
login_failed_once: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -191,8 +190,8 @@ impl Imap {
|
||||
session: Default::default(),
|
||||
connected: Default::default(),
|
||||
interrupt: Default::default(),
|
||||
skip_next_idle_wait: Default::default(),
|
||||
should_reconnect: Default::default(),
|
||||
login_failed_once: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,18 +296,40 @@ impl Imap {
|
||||
// needs to be set here to ensure it is set on reconnects.
|
||||
self.connected = true;
|
||||
self.session = Some(session);
|
||||
self.login_failed_once = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Err((err, _)) => {
|
||||
let imap_user = self.config.imap_user.to_owned();
|
||||
let message = context
|
||||
.stock_string_repl_str(StockMessage::CannotLogin, &imap_user)
|
||||
.await;
|
||||
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorNetwork(format!("{} ({})", message, err))
|
||||
);
|
||||
warn!(context, "{} ({})", message, err);
|
||||
emit_event!(context, Event::ErrorNetwork(message.clone()));
|
||||
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await
|
||||
{
|
||||
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
|
||||
warn!(context, "{}", e);
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(message);
|
||||
if let Err(e) =
|
||||
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
|
||||
.await
|
||||
{
|
||||
warn!(context, "{}", e);
|
||||
}
|
||||
} else {
|
||||
self.login_failed_once = true;
|
||||
}
|
||||
|
||||
self.trigger_reconnect();
|
||||
Err(Error::LoginFailed(format!("cannot login as {}", imap_user)))
|
||||
}
|
||||
@@ -473,7 +494,7 @@ impl Imap {
|
||||
folder: S,
|
||||
) -> (u32, u32) {
|
||||
let key = format!("imap.mailbox.{}", folder.as_ref());
|
||||
if let Some(entry) = context.sql.get_raw_config(&key).await {
|
||||
if let Some(entry) = context.sql.get_raw_config(context, &key).await {
|
||||
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
|
||||
let mut parts = entry.split(':');
|
||||
(
|
||||
@@ -783,7 +804,6 @@ impl Imap {
|
||||
let mut last_uid = None;
|
||||
let mut count = 0;
|
||||
|
||||
let mut tasks = Vec::with_capacity(server_uids.len());
|
||||
while let Some(Ok(msg)) = msgs.next().await {
|
||||
let server_uid = msg.uid.unwrap_or_default();
|
||||
|
||||
@@ -803,31 +823,17 @@ impl Imap {
|
||||
let context = context.clone();
|
||||
let folder = folder.clone();
|
||||
|
||||
let task = async_std::task::spawn(async move {
|
||||
// safe, as we checked above that there is a body.
|
||||
let body = msg.body().unwrap();
|
||||
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
|
||||
// safe, as we checked above that there is a body.
|
||||
let body = msg.body().unwrap();
|
||||
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
|
||||
|
||||
match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await {
|
||||
Ok(_) => Some(server_uid),
|
||||
Err(err) => {
|
||||
warn!(context, "dc_receive_imf error: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
for task in futures::future::join_all(tasks).await {
|
||||
match task {
|
||||
Some(uid) => {
|
||||
last_uid = Some(uid);
|
||||
}
|
||||
None => {
|
||||
match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await {
|
||||
Ok(_) => last_uid = Some(server_uid),
|
||||
Err(err) => {
|
||||
warn!(context, "dc_receive_imf error: {}", err);
|
||||
read_errors += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if count != server_uids.len() {
|
||||
@@ -1129,7 +1135,10 @@ impl Imap {
|
||||
context: &Context,
|
||||
create_mvbox: bool,
|
||||
) -> Result<()> {
|
||||
let folders_configured = context.sql.get_raw_config_int("folders_configured").await;
|
||||
let folders_configured = context
|
||||
.sql
|
||||
.get_raw_config_int(context, "folders_configured")
|
||||
.await;
|
||||
if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1295,7 +1304,7 @@ impl Imap {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET server_folder='',server_uid=0 WHERE server_folder=?",
|
||||
paramsx![folder],
|
||||
paramsv![folder],
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1357,14 +1366,26 @@ async fn precheck_imf(
|
||||
let delete_server_after = context.get_config_delete_server_after().await;
|
||||
|
||||
if delete_server_after != Some(0) {
|
||||
context
|
||||
.do_heuristics_moves(server_folder.as_ref(), msg_id)
|
||||
if msg_id
|
||||
.needs_move(context, server_folder)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// If the bcc-self message is not moved, directly
|
||||
// add MarkSeen job, otherwise MarkSeen job is
|
||||
// added after the Move Job completed.
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MoveMsg, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
} else if old_server_folder != server_folder {
|
||||
info!(
|
||||
@@ -1399,6 +1420,13 @@ async fn precheck_imf(
|
||||
|
||||
if old_server_folder != server_folder || old_server_uid != server_uid {
|
||||
update_server_uid(context, rfc724_mid, server_folder, server_uid).await;
|
||||
if let Ok(MessageState::InSeen) = msg_id.get_state(context).await {
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
};
|
||||
context
|
||||
.interrupt_inbox(InterruptInfo::new(false, Some(msg_id)))
|
||||
.await;
|
||||
|
||||
@@ -51,6 +51,14 @@ 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.
|
||||
pub(super) async fn select_folder<S: AsRef<str>>(
|
||||
@@ -76,10 +84,7 @@ impl Imap {
|
||||
}
|
||||
|
||||
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
|
||||
let needs_expunge = { self.config.selected_folder_needs_expunge };
|
||||
if needs_expunge {
|
||||
self.close_folder(context).await?;
|
||||
}
|
||||
self.maybe_close_folder(context).await?;
|
||||
|
||||
// select new folder
|
||||
if let Some(ref folder) = folder {
|
||||
|
||||
227
src/imex.rs
227
src/imex.rs
@@ -96,21 +96,21 @@ pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result
|
||||
let name = name.to_string_lossy();
|
||||
if name.starts_with("delta-chat") && name.ends_with(".bak") {
|
||||
let sql = Sql::new();
|
||||
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;
|
||||
if sql.open(context, &path, true).await {
|
||||
let curr_backup_time = sql
|
||||
.get_raw_config_int(context, "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;
|
||||
}
|
||||
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()),
|
||||
@@ -178,10 +178,11 @@ 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> {
|
||||
ensure!(
|
||||
passphrase.len() >= 2,
|
||||
"Passphrase must be at least 2 chars long."
|
||||
);
|
||||
let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
|
||||
passphrase_begin
|
||||
} else {
|
||||
bail!("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 {
|
||||
false => None,
|
||||
@@ -196,7 +197,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
||||
"Passphrase-Format: numeric9x4\r\n",
|
||||
"Passphrase-Begin: {}"
|
||||
),
|
||||
&passphrase[..2]
|
||||
passphrase_begin
|
||||
);
|
||||
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
|
||||
|
||||
@@ -245,7 +246,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(context, "bcc_self").await {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
// TODO: define this as a stockstring once the wording is settled.
|
||||
msg.text = Some(
|
||||
@@ -396,7 +397,6 @@ async fn imex_inner(
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "IMEX FAILED: {}", err);
|
||||
context.emit_event(Event::ImexProgress(0));
|
||||
bail!("IMEX FAILED to complete: {}", err);
|
||||
}
|
||||
@@ -429,35 +429,57 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
);
|
||||
/* error already logged */
|
||||
/* re-open copied database file */
|
||||
context
|
||||
.sql
|
||||
.open(&context, &context.get_dbfile(), false)
|
||||
.await?;
|
||||
ensure!(
|
||||
context
|
||||
.sql
|
||||
.open(&context, &context.get_dbfile(), false)
|
||||
.await,
|
||||
"could not re-open db"
|
||||
);
|
||||
|
||||
delete_and_reset_all_device_msgs(&context).await?;
|
||||
|
||||
let total_files_cnt: i32 = context
|
||||
let total_files_cnt = context
|
||||
.sql
|
||||
.query_value("SELECT COUNT(*) FROM backup_blobs;", paramsx![])
|
||||
.query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![])
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let total_files_cnt = total_files_cnt as usize;
|
||||
.unwrap_or_default() as usize;
|
||||
info!(
|
||||
context,
|
||||
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
|
||||
);
|
||||
|
||||
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);
|
||||
// Load IDs only for now, without the file contents, to avoid
|
||||
// consuming too much memory.
|
||||
let file_ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM backup_blobs ORDER BY id",
|
||||
paramsv![],
|
||||
|row| row.get(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<i64>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
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 (file_name, file_blob) = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
|
||||
paramsv![file_id],
|
||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
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
|
||||
@@ -472,22 +494,19 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
|
||||
let path_filename = context.get_blobdir().join(file_name);
|
||||
dc_write_file(context, &path_filename, &file_blob).await?;
|
||||
|
||||
processed_files_cnt += 1;
|
||||
}
|
||||
|
||||
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(())
|
||||
if all_files_extracted {
|
||||
// only delete backup_blobs if all files were successfully extracted
|
||||
context
|
||||
.sql
|
||||
.execute("DROP TABLE backup_blobs;", paramsv![])
|
||||
.await?;
|
||||
context.sql.execute("VACUUM;", paramsv![]).await.ok();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("received stop signal");
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -503,9 +522,9 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
let dest_path_filename = dc_get_next_backup_path(dir, now).await?;
|
||||
let dest_path_string = dest_path_filename.to_string_lossy().to_string();
|
||||
|
||||
sql::housekeeping(context).await?;
|
||||
sql::housekeeping(context).await;
|
||||
|
||||
context.sql.execute("VACUUM;", paramsx![]).await.ok();
|
||||
context.sql.execute("VACUUM;", paramsv![]).await.ok();
|
||||
|
||||
// we close the database during the copy of the dbfile
|
||||
context.sql.close().await;
|
||||
@@ -519,7 +538,7 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.open(&context, &context.get_dbfile(), false)
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
if !copied {
|
||||
bail!(
|
||||
@@ -529,9 +548,17 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
);
|
||||
}
|
||||
let dest_sql = Sql::new();
|
||||
dest_sql.open(context, &dest_path_filename, false).await?;
|
||||
|
||||
ensure!(
|
||||
dest_sql.open(context, &dest_path_filename, false).await,
|
||||
"could not open exported database {}",
|
||||
dest_path_string
|
||||
);
|
||||
let res = match add_files_to_export(context, &dest_sql).await {
|
||||
Err(err) => {
|
||||
dc_delete_file(context, &dest_path_filename).await;
|
||||
error!(context, "backup failed: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
Ok(()) => {
|
||||
dest_sql
|
||||
.set_raw_config_int(context, "backup_time", now as i32)
|
||||
@@ -539,11 +566,6 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
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;
|
||||
|
||||
@@ -556,7 +578,7 @@ async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
|
||||
if !sql.table_exists("backup_blobs").await? {
|
||||
sql.execute(
|
||||
"CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);",
|
||||
paramsx![],
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -568,38 +590,41 @@ async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
|
||||
|
||||
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?;
|
||||
sql.with_conn_async(|conn| async move {
|
||||
// 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?;
|
||||
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 mut processed_files_cnt = 0;
|
||||
while let Some(entry) = dir_handle.next().await {
|
||||
let entry = entry?;
|
||||
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;
|
||||
}
|
||||
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() {
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
if name.starts_with("delta-chat") && name.ends_with(".bak") {
|
||||
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?;
|
||||
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
|
||||
let mut stmt = conn.prepare_cached(
|
||||
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
|
||||
)?;
|
||||
stmt.execute(paramsv![name, buf])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -662,19 +687,30 @@ 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 pool = context.sql.get_pool().await?;
|
||||
let keys = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, public_key, private_key, is_default FROM keypairs;",
|
||||
paramsv![],
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
let public_key_blob: Vec<u8> = row.get(1)?;
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_blob);
|
||||
let private_key_blob: Vec<u8> = row.get(2)?;
|
||||
let private_key = SignedSecretKey::from_slice(&private_key_blob);
|
||||
let is_default: i32 = row.get(3)?;
|
||||
|
||||
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);
|
||||
Ok((id, public_key, private_key, is_default))
|
||||
},
|
||||
|keys| {
|
||||
keys.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
for (id, public_key, private_key, is_default) in keys {
|
||||
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
|
||||
@@ -698,7 +734,6 @@ async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
|
||||
}
|
||||
|
||||
ensure!(export_errors == 0, "errors while exporting keys");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
299
src/job.rs
299
src/job.rs
@@ -6,13 +6,13 @@
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use async_smtp::smtp::response::Category;
|
||||
use async_smtp::smtp::response::Code;
|
||||
use async_smtp::smtp::response::Detail;
|
||||
use async_std::prelude::*;
|
||||
use deltachat_derive::*;
|
||||
use itertools::Itertools;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, ChatId};
|
||||
@@ -21,6 +21,7 @@ use crate::constants::*;
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::load_imap_deletion_msgid;
|
||||
use crate::error::{bail, ensure, format_err, Error, Result};
|
||||
use crate::events::Event;
|
||||
use crate::imap::*;
|
||||
@@ -37,7 +38,7 @@ use crate::{scheduler::InterruptInfo, sql};
|
||||
const JOB_RETRIES: u32 = 17;
|
||||
|
||||
/// Thread IDs
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(i32)]
|
||||
pub(crate) enum Thread {
|
||||
Unknown = 0,
|
||||
@@ -75,7 +76,17 @@ impl Default for Thread {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, FromPrimitive, ToPrimitive, Sqlx,
|
||||
Debug,
|
||||
Display,
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
pub enum Action {
|
||||
@@ -84,7 +95,6 @@ pub enum Action {
|
||||
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
|
||||
Housekeeping = 105, // low priority ...
|
||||
EmptyServer = 107,
|
||||
OldDeleteMsgOnImap = 110,
|
||||
MarkseenMsgOnImap = 130,
|
||||
|
||||
// Moving message is prioritized lower than deletion so we don't
|
||||
@@ -113,7 +123,6 @@ impl From<Action> for Thread {
|
||||
Unknown => Thread::Unknown,
|
||||
|
||||
Housekeeping => Thread::Imap,
|
||||
OldDeleteMsgOnImap => Thread::Imap,
|
||||
DeleteMsgOnImap => Thread::Imap,
|
||||
EmptyServer => Thread::Imap,
|
||||
MarkseenMsgOnImap => Thread::Imap,
|
||||
@@ -145,32 +154,6 @@ impl fmt::Display for Job {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Job {
|
||||
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
|
||||
use sqlx::Row;
|
||||
let foreign_id: i32 = row.try_get("foreign_id")?;
|
||||
if foreign_id < 0 {
|
||||
return Err(sqlx::Error::Decode(
|
||||
anyhow::anyhow!("invalid foreign_id").into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Job {
|
||||
job_id: row.try_get::<i32, _>("id")? as u32,
|
||||
action: row.try_get("action")?,
|
||||
foreign_id: foreign_id as u32,
|
||||
desired_timestamp: row.try_get_unchecked("desired_timestamp")?,
|
||||
added_timestamp: row.try_get_unchecked("added_timestamp")?,
|
||||
tries: row.try_get::<i32, _>("tries")? as u32,
|
||||
param: row
|
||||
.try_get::<String, _>("param")?
|
||||
.parse()
|
||||
.unwrap_or_default(),
|
||||
pending_error: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Job {
|
||||
pub fn new(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Self {
|
||||
let timestamp = time();
|
||||
@@ -196,7 +179,7 @@ impl Job {
|
||||
if self.job_id != 0 {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsx![self.job_id as i32])
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -216,22 +199,22 @@ impl Job {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
||||
paramsx![
|
||||
paramsv![
|
||||
self.desired_timestamp,
|
||||
self.tries as i64,
|
||||
self.param.to_string(),
|
||||
self.job_id as i32
|
||||
self.job_id as i32,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
context.sql.execute(
|
||||
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
|
||||
paramsx![
|
||||
paramsv![
|
||||
self.added_timestamp,
|
||||
thread,
|
||||
self.action,
|
||||
self.foreign_id as i32,
|
||||
self.foreign_id,
|
||||
self.param.to_string(),
|
||||
self.desired_timestamp
|
||||
]
|
||||
@@ -270,40 +253,48 @@ impl Job {
|
||||
|
||||
let res = match err {
|
||||
async_smtp::smtp::error::Error::Permanent(ref response) => {
|
||||
match response.code {
|
||||
// Workaround for incorrectly configured servers returning permanent errors
|
||||
// instead of temporary ones.
|
||||
let maybe_transient = match response.code {
|
||||
// Sometimes servers send a permanent error when actually it is a temporary error
|
||||
// For documentation see https://tools.ietf.org/html/rfc3463
|
||||
|
||||
// Code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
|
||||
Code {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
..
|
||||
} => Status::RetryLater,
|
||||
|
||||
_ => {
|
||||
// If we do not retry, add an info message to the chat
|
||||
// Error 5.7.1 should definitely go here: Yandex sends 5.7.1 with a link when it thinks that the email is SPAM.
|
||||
match Message::load_from_db(context, MsgId::new(self.foreign_id))
|
||||
.await
|
||||
{
|
||||
Ok(message) => {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
message.chat_id,
|
||||
err.to_string(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(e) => warn!(
|
||||
context,
|
||||
"couldn't load chat_id to inform user about SMTP error: {}",
|
||||
e
|
||||
),
|
||||
};
|
||||
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
} => {
|
||||
// Ignore status code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
|
||||
// Maybe incorrectly configured Postfix milter with "reject" instead of "tempfail", which returns
|
||||
// "550 5.5.0 Service unavailable" instead of "451 4.7.1 Service unavailable - try again later".
|
||||
//
|
||||
// Other enhanced status codes, such as Postfix
|
||||
// "550 5.1.1 <foobar@example.org>: Recipient address rejected: User unknown in local recipient table"
|
||||
// are not ignored.
|
||||
response.message.get(0) == Some(&"5.5.0".to_string())
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if maybe_transient {
|
||||
Status::RetryLater
|
||||
} else {
|
||||
// If we do not retry, add an info message to the chat.
|
||||
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
|
||||
// should definitely go here, because user has to open the link to
|
||||
// resume message sending.
|
||||
let msg_id = MsgId::new(self.foreign_id);
|
||||
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(message) => {
|
||||
chat::add_info_msg(context, message.chat_id, err.to_string())
|
||||
.await
|
||||
}
|
||||
Err(e) => error!(
|
||||
context,
|
||||
"couldn't load chat_id to inform user about SMTP error: {}", e
|
||||
),
|
||||
};
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
}
|
||||
}
|
||||
async_smtp::smtp::error::Error::Transient(_) => {
|
||||
@@ -410,32 +401,40 @@ impl Job {
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
) -> sql::Result<(Vec<u32>, Vec<String>)> {
|
||||
// Extract message IDs from job parameters
|
||||
let res: Vec<(u32, MsgId)> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?",
|
||||
paramsv![contact_id, self.job_id],
|
||||
|row| {
|
||||
let job_id: u32 = row.get(0)?;
|
||||
let params_str: String = row.get(1)?;
|
||||
let params: Params = params_str.parse().unwrap_or_default();
|
||||
Ok((job_id, params))
|
||||
},
|
||||
|jobs| {
|
||||
let res = jobs
|
||||
.filter_map(|row| {
|
||||
let (job_id, params) = row.ok()?;
|
||||
let msg_id = params.get_msg_id()?;
|
||||
Some((job_id, msg_id))
|
||||
})
|
||||
.collect();
|
||||
Ok(res)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Load corresponding RFC724 message IDs
|
||||
let mut job_ids = Vec::new();
|
||||
let mut rfc724_mids = Vec::new();
|
||||
|
||||
let pool = context.sql.get_pool().await?;
|
||||
|
||||
let mut rows = sqlx::query_as("SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?")
|
||||
.bind(contact_id as i64)
|
||||
.bind(self.job_id as i64)
|
||||
.fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (job_id, params): (i64, String) = row?;
|
||||
let params: Params = params.parse().unwrap_or_default();
|
||||
let msg_id = params.get_msg_id().unwrap_or_default();
|
||||
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(Message { rfc724_mid, .. }) => {
|
||||
job_ids.push(job_id as u32);
|
||||
rfc724_mids.push(rfc724_mid);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "failed to load mdn job message: {}", err);
|
||||
}
|
||||
for (job_id, msg_id) in res {
|
||||
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await {
|
||||
job_ids.push(job_id);
|
||||
rfc724_mids.push(rfc724_mid);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((job_ids, rfc724_mids))
|
||||
}
|
||||
|
||||
@@ -632,7 +631,9 @@ impl Job {
|
||||
}
|
||||
}
|
||||
if self.foreign_id & DC_EMPTY_INBOX > 0 {
|
||||
imap.empty_folder(context, "INBOX").await;
|
||||
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
|
||||
imap.empty_folder(context, &inbox_folder).await;
|
||||
}
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
@@ -646,7 +647,21 @@ impl Job {
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
|
||||
let folder = msg.server_folder.as_ref().unwrap();
|
||||
match imap.set_seen(context, folder, msg.server_uid).await {
|
||||
|
||||
let result = if msg.server_uid == 0 {
|
||||
// The message is moved or deleted by us.
|
||||
//
|
||||
// Do not call set_seen with zero UID, as it will return
|
||||
// ImapActionResult::RetryLater, but we do not want to
|
||||
// retry. If the message was moved, we will create another
|
||||
// job to mark the message as seen later. If it was
|
||||
// deleted, there is nothing to do.
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
imap.set_seen(context, folder, msg.server_uid).await
|
||||
};
|
||||
|
||||
match result {
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
|
||||
ImapActionResult::Success | ImapActionResult::Failed => {
|
||||
@@ -654,7 +669,18 @@ impl Job {
|
||||
// we want to send out an MDN anyway
|
||||
// The job will not be retried so locally
|
||||
// there is no risk of double-sending MDNs.
|
||||
//
|
||||
// Read receipts for system messages are never
|
||||
// sent. These messages have no place to display
|
||||
// received read receipt anyway. And since their text
|
||||
// is locally generated, quoting them is dangerous as
|
||||
// it may contain contact names. E.g., for original
|
||||
// message "Group left by me", a read receipt will
|
||||
// quote "Group left by <name>", and the name can be a
|
||||
// display name stored in address book rather than
|
||||
// the name sent in the From field by the user.
|
||||
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default()
|
||||
&& !msg.is_system_message()
|
||||
&& context.get_config_bool(Config::MdnsEnabled).await
|
||||
{
|
||||
if let Err(err) = send_mdn(context, &msg).await {
|
||||
@@ -672,27 +698,21 @@ impl Job {
|
||||
pub async fn kill_action(context: &Context, action: Action) -> bool {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE action=?;", paramsx![action])
|
||||
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Remove jobs with specified IDs.
|
||||
async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
|
||||
use sqlx::Arguments;
|
||||
|
||||
let mut args = sqlx::sqlite::SqliteArguments::default();
|
||||
for job_id in job_ids {
|
||||
args.add(*job_id as i32);
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
job_ids.iter().map(|_| "?").join(",")
|
||||
),
|
||||
args,
|
||||
job_ids.iter().map(|i| i as &dyn crate::ToSql).collect(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -701,7 +721,7 @@ async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
|
||||
pub async fn action_exists(context: &Context, action: Action) -> bool {
|
||||
context
|
||||
.sql
|
||||
.exists("SELECT id FROM jobs WHERE action=?;", paramsx![action])
|
||||
.exists("SELECT id FROM jobs WHERE action=?;", paramsv![action])
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -710,7 +730,11 @@ async fn set_delivered(context: &Context, msg_id: MsgId) {
|
||||
message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
|
||||
let chat_id: ChatId = context
|
||||
.sql
|
||||
.query_value("SELECT chat_id FROM msgs WHERE id=?", paramsx![])
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT chat_id FROM msgs WHERE id=?",
|
||||
paramsv![msg_id],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
context.emit_event(Event::MsgDelivered { chat_id, msg_id });
|
||||
@@ -838,25 +862,6 @@ pub(crate) enum Connection<'a> {
|
||||
Smtp(&'a mut Smtp),
|
||||
}
|
||||
|
||||
async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after().await {
|
||||
let threshold_timestamp = time() - delete_server_after;
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_value_optional(
|
||||
r#"
|
||||
SELECT id FROM msgs
|
||||
WHERE timestamp < ? AND server_uid != 0
|
||||
"#,
|
||||
paramsx![threshold_timestamp],
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
|
||||
let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? {
|
||||
Some(Job::new(
|
||||
@@ -980,22 +985,16 @@ async fn perform_job_action(
|
||||
location::job_maybe_send_locations_ended(context, job).await
|
||||
}
|
||||
Action::EmptyServer => job.empty_server(context, connection.inbox()).await,
|
||||
Action::OldDeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
|
||||
Action::Housekeeping => {
|
||||
if let Err(err) = sql::housekeeping(context).await {
|
||||
error!(context, "housekeeping failed: {}", err);
|
||||
}
|
||||
sql::housekeeping(context).await;
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Inbox finished immediate try {} of job {}", tries, job
|
||||
);
|
||||
info!(context, "Finished immediate try {} of job {}", tries, job);
|
||||
|
||||
try_res
|
||||
}
|
||||
@@ -1043,7 +1042,6 @@ pub async fn add(context: &Context, job: Job) {
|
||||
Action::Unknown => unreachable!(),
|
||||
Action::Housekeeping
|
||||
| Action::EmptyServer
|
||||
| Action::OldDeleteMsgOnImap
|
||||
| Action::DeleteMsgOnImap
|
||||
| Action::MarkseenMsgOnImap
|
||||
| Action::MoveMsg => {
|
||||
@@ -1079,8 +1077,9 @@ pub(crate) async fn load_next(
|
||||
info!(context, "loading job for {}-thread", thread);
|
||||
|
||||
let query;
|
||||
let params: Box<dyn Fn() -> sqlx::sqlite::SqliteArguments<'static> + 'static + Send>;
|
||||
let params;
|
||||
let t = time();
|
||||
let m;
|
||||
let thread_i = thread as i64;
|
||||
|
||||
if let Some(msg_id) = info.msg_id {
|
||||
@@ -1091,7 +1090,8 @@ WHERE thread=? AND foreign_id=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = Box::new(move || paramsx![thread_i, msg_id]);
|
||||
m = msg_id;
|
||||
params = paramsv![thread_i, m];
|
||||
} else if !info.probe_network {
|
||||
// processing for first-try and after backoff-timeouts:
|
||||
// process jobs in the order they were added.
|
||||
@@ -1102,7 +1102,7 @@ WHERE thread=? AND desired_timestamp<=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = Box::new(move || paramsx![thread_i, t]);
|
||||
params = paramsv![thread_i, t];
|
||||
} else {
|
||||
// processing after call to dc_maybe_network():
|
||||
// process _all_ pending jobs that failed before
|
||||
@@ -1114,12 +1114,27 @@ WHERE thread=? AND tries>0
|
||||
ORDER BY desired_timestamp, action DESC
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = Box::new(move || paramsx![thread_i]);
|
||||
params = paramsv![thread_i];
|
||||
};
|
||||
|
||||
let job: Option<Job> = loop {
|
||||
let p = params();
|
||||
let job_res = context.sql.query_row_optional(query, p).await;
|
||||
let job = loop {
|
||||
let job_res = context
|
||||
.sql
|
||||
.query_row_optional(query, params.clone(), |row| {
|
||||
let job = Job {
|
||||
job_id: row.get("id")?,
|
||||
action: row.get("action")?,
|
||||
foreign_id: row.get("foreign_id")?,
|
||||
desired_timestamp: row.get("desired_timestamp")?,
|
||||
added_timestamp: row.get("added_timestamp")?,
|
||||
tries: row.get("tries")?,
|
||||
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
|
||||
pending_error: None,
|
||||
};
|
||||
|
||||
Ok(job)
|
||||
})
|
||||
.await;
|
||||
|
||||
match job_res {
|
||||
Ok(job) => break job,
|
||||
@@ -1128,13 +1143,15 @@ LIMIT 1;
|
||||
info!(context, "cleaning up job, because of {}", err);
|
||||
|
||||
// TODO: improve by only doing a single query
|
||||
let p = params();
|
||||
let id: Result<i32, _> = context.sql.query_value(query, p).await;
|
||||
match id {
|
||||
match context
|
||||
.sql
|
||||
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
|
||||
.await
|
||||
{
|
||||
Ok(id) => {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsx![id])
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
@@ -1184,7 +1201,7 @@ mod tests {
|
||||
"INSERT INTO jobs
|
||||
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?);",
|
||||
paramsx![
|
||||
paramsv![
|
||||
now,
|
||||
Thread::from(Action::MoveMsg),
|
||||
Action::MoveMsg,
|
||||
|
||||
57
src/key.rs
57
src/key.rs
@@ -116,21 +116,22 @@ impl DcKey for SignedPublicKey {
|
||||
type KeyType = SignedPublicKey;
|
||||
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
let res: std::result::Result<Vec<u8>, _> = context
|
||||
match context
|
||||
.sql
|
||||
.query_value(
|
||||
.query_row(
|
||||
r#"
|
||||
SELECT public_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsx![],
|
||||
paramsv![],
|
||||
|row| row.get::<_, Vec<u8>>(0),
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
.await
|
||||
{
|
||||
Ok(bytes) => Self::from_slice(&bytes),
|
||||
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.public)
|
||||
}
|
||||
@@ -160,21 +161,22 @@ impl DcKey for SignedSecretKey {
|
||||
type KeyType = SignedSecretKey;
|
||||
|
||||
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||
let res: std::result::Result<Vec<u8>, _> = context
|
||||
match context
|
||||
.sql
|
||||
.query_value(
|
||||
.query_row(
|
||||
r#"
|
||||
SELECT private_key
|
||||
FROM keypairs
|
||||
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsx![],
|
||||
paramsv![],
|
||||
|row| row.get::<_, Vec<u8>>(0),
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
.await
|
||||
{
|
||||
Ok(bytes) => Self::from_slice(&bytes),
|
||||
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let keypair = generate_keypair(context).await?;
|
||||
Ok(keypair.secret)
|
||||
}
|
||||
@@ -225,26 +227,27 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
let _guard = context.generating_key_mutex.lock().await;
|
||||
|
||||
// Check if the key appeared while we were waiting on the lock.
|
||||
let res: std::result::Result<(Vec<u8>, Vec<u8>), _> = context
|
||||
match context
|
||||
.sql
|
||||
.query_row(
|
||||
r#"
|
||||
SELECT public_key, private_key
|
||||
FROM keypairs
|
||||
WHERE addr=?
|
||||
WHERE addr=?1
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsx![addr.to_string()],
|
||||
paramsv![addr],
|
||||
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
.await
|
||||
{
|
||||
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
|
||||
addr,
|
||||
public: SignedPublicKey::from_slice(&pub_bytes)?,
|
||||
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||
}),
|
||||
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
|
||||
let start = std::time::Instant::now();
|
||||
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
let start = std::time::SystemTime::now();
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
|
||||
.unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keytype);
|
||||
@@ -255,7 +258,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
info!(
|
||||
context,
|
||||
"Keypair generated in {:.3}s.",
|
||||
start.elapsed().as_secs()
|
||||
start.elapsed().unwrap_or_default().as_secs()
|
||||
);
|
||||
Ok(keypair)
|
||||
}
|
||||
@@ -318,14 +321,14 @@ pub async fn store_self_keypair(
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
|
||||
paramsx![&public_key, &secret_key],
|
||||
paramsv![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("UPDATE keypairs SET is_default=0;", paramsx![])
|
||||
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
|
||||
.await
|
||||
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
|
||||
}
|
||||
@@ -337,7 +340,7 @@ 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];
|
||||
let params = paramsv![addr, is_default, public_key, secret_key, t];
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -613,12 +616,10 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
|
||||
let ctx1 = ctx.clone();
|
||||
let nrows = || async {
|
||||
let val: i32 = ctx1
|
||||
.sql
|
||||
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
|
||||
ctx1.sql
|
||||
.query_get_value::<u32>(&ctx1, "SELECT COUNT(*) FROM keypairs;", paramsv![])
|
||||
.await
|
||||
.unwrap();
|
||||
val as usize
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(nrows().await, 0);
|
||||
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
|
||||
|
||||
16
src/lib.rs
16
src/lib.rs
@@ -1,15 +1,26 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
|
||||
#![allow(clippy::match_bool)]
|
||||
#![deny(
|
||||
clippy::correctness,
|
||||
missing_debug_implementations,
|
||||
clippy::all,
|
||||
clippy::indexing_slicing
|
||||
)]
|
||||
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate num_derive;
|
||||
#[macro_use]
|
||||
extern crate smallvec;
|
||||
#[macro_use]
|
||||
extern crate rusqlite;
|
||||
extern crate strum;
|
||||
#[macro_use]
|
||||
extern crate strum_macros;
|
||||
|
||||
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
|
||||
|
||||
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
|
||||
|
||||
#[macro_use]
|
||||
pub mod log;
|
||||
#[macro_use]
|
||||
@@ -37,6 +48,7 @@ pub mod constants;
|
||||
pub mod contact;
|
||||
pub mod context;
|
||||
mod e2ee;
|
||||
pub mod ephemeral;
|
||||
mod imap;
|
||||
pub mod imex;
|
||||
mod scheduler;
|
||||
|
||||
473
src/location.rs
473
src/location.rs
@@ -1,6 +1,5 @@
|
||||
//! Location handling
|
||||
|
||||
use async_std::prelude::*;
|
||||
use bitflags::bitflags;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
@@ -38,34 +37,6 @@ impl Location {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Location {
|
||||
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
|
||||
use sqlx::Row;
|
||||
|
||||
let msg_id = row.try_get::<i32, _>("msg_id")? as u32;
|
||||
let txt: String = row.try_get("txt")?;
|
||||
let marker = if msg_id != 0 && is_marker(&txt) {
|
||||
Some(txt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let loc = Location {
|
||||
location_id: row.try_get::<i32, _>("id")? as u32,
|
||||
latitude: row.try_get("latitude")?,
|
||||
longitude: row.try_get("longitude")?,
|
||||
accuracy: row.try_get("accuracy")?,
|
||||
timestamp: row.try_get("timestamp")?,
|
||||
independent: row.try_get::<i32, _>("independent")? as u32,
|
||||
msg_id,
|
||||
contact_id: row.try_get::<i32, _>("from_id")? as u32,
|
||||
chat_id: row.try_get("chat_id")?,
|
||||
marker,
|
||||
};
|
||||
|
||||
Ok(loc)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Kml {
|
||||
pub addr: Option<String>,
|
||||
@@ -226,16 +197,14 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
UPDATE chats
|
||||
SET locations_send_begin=?,
|
||||
locations_send_until=?
|
||||
WHERE id=?
|
||||
"#,
|
||||
paramsx![
|
||||
"UPDATE chats \
|
||||
SET locations_send_begin=?, \
|
||||
locations_send_until=? \
|
||||
WHERE id=?",
|
||||
paramsv![
|
||||
if 0 != seconds { now } else { 0 },
|
||||
if 0 != seconds { now + seconds } else { 0 },
|
||||
chat_id
|
||||
chat_id,
|
||||
],
|
||||
)
|
||||
.await
|
||||
@@ -291,60 +260,53 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) ->
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
|
||||
paramsx![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
|
||||
paramsv![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn set(
|
||||
context: &Context,
|
||||
latitude: f64,
|
||||
longitude: f64,
|
||||
accuracy: f64,
|
||||
) -> Result<bool, Error> {
|
||||
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
|
||||
if latitude == 0.0 && longitude == 0.0 {
|
||||
return Ok(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut continue_streaming = false;
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as("SELECT id FROM chats WHERE locations_send_until>?;")
|
||||
.bind(time())
|
||||
.fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (chat_id,): (i64,) = row?;
|
||||
|
||||
if let Err(err) = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
paramsx![
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
DC_CONTACT_ID_SELF as i32
|
||||
],
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(context, "failed to store location {:?}", err);
|
||||
} else {
|
||||
continue_streaming = true;
|
||||
if let Ok(chats) = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||
paramsv![time()],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await
|
||||
{
|
||||
for chat_id in chats {
|
||||
if let Err(err) = context.sql.execute(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
DC_CONTACT_ID_SELF,
|
||||
]
|
||||
).await {
|
||||
warn!(context, "failed to store location {:?}", err);
|
||||
} else {
|
||||
continue_streaming = true;
|
||||
}
|
||||
}
|
||||
if continue_streaming {
|
||||
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
};
|
||||
schedule_maybe_send_locations(context, false).await;
|
||||
}
|
||||
|
||||
if continue_streaming {
|
||||
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
};
|
||||
|
||||
schedule_maybe_send_locations(context, false).await;
|
||||
|
||||
Ok(continue_streaming)
|
||||
continue_streaming
|
||||
}
|
||||
|
||||
pub async fn get_range(
|
||||
@@ -357,21 +319,16 @@ pub async fn get_range(
|
||||
if timestamp_to == 0 {
|
||||
timestamp_to = time() + 10;
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_rows(
|
||||
r#"
|
||||
SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent,
|
||||
COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt
|
||||
FROM locations l
|
||||
LEFT JOIN msgs m ON l.id=m.location_id
|
||||
WHERE (? OR l.chat_id=?)
|
||||
AND (? OR l.from_id=?)
|
||||
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?))
|
||||
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;
|
||||
"#,
|
||||
paramsx![
|
||||
.query_map(
|
||||
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
|
||||
COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \
|
||||
FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
|
||||
AND (? OR l.from_id=?) \
|
||||
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
|
||||
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
|
||||
paramsv![
|
||||
if chat_id.is_unset() { 1 } else { 0 },
|
||||
chat_id,
|
||||
if contact_id == 0 { 1 } else { 0 },
|
||||
@@ -379,6 +336,36 @@ SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent,
|
||||
timestamp_from,
|
||||
timestamp_to,
|
||||
],
|
||||
|row| {
|
||||
let msg_id = row.get(6)?;
|
||||
let txt: String = row.get(9)?;
|
||||
let marker = if msg_id != 0 && is_marker(&txt) {
|
||||
Some(txt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let loc = Location {
|
||||
location_id: row.get(0)?,
|
||||
latitude: row.get(1)?,
|
||||
longitude: row.get(2)?,
|
||||
accuracy: row.get(3)?,
|
||||
timestamp: row.get(4)?,
|
||||
independent: row.get(5)?,
|
||||
msg_id,
|
||||
contact_id: row.get(7)?,
|
||||
chat_id: row.get(8)?,
|
||||
marker,
|
||||
};
|
||||
Ok(loc)
|
||||
},
|
||||
|locations| {
|
||||
let mut ret = Vec::new();
|
||||
|
||||
for location in locations {
|
||||
ret.push(location?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -392,7 +379,7 @@ fn is_marker(txt: &str) -> bool {
|
||||
pub async fn delete_all(context: &Context) -> Result<(), Error> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM locations;", paramsx![])
|
||||
.execute("DELETE FROM locations;", paramsv![])
|
||||
.await?;
|
||||
context.emit_event(Event::LocationChanged(None));
|
||||
Ok(())
|
||||
@@ -406,10 +393,16 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let (locations_send_begin, locations_send_until, locations_last_sent): (i64, i64, i64) = context.sql.query_row(
|
||||
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
|
||||
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
|
||||
paramsx![chat_id]
|
||||
).await?;
|
||||
paramsv![chat_id], |row| {
|
||||
let send_begin: i64 = row.get(0)?;
|
||||
let send_until: i64 = row.get(1)?;
|
||||
let last_sent: i64 = row.get(2)?;
|
||||
|
||||
Ok((send_begin, send_until, last_sent))
|
||||
})
|
||||
.await?;
|
||||
|
||||
let now = time();
|
||||
let mut location_count = 0;
|
||||
@@ -420,41 +413,40 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
self_addr,
|
||||
);
|
||||
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, latitude, longitude, accuracy, timestamp
|
||||
FROM locations
|
||||
WHERE from_id=?
|
||||
AND timestamp>=?
|
||||
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?))
|
||||
AND independent=0
|
||||
GROUP BY timestamp
|
||||
ORDER BY timestamp;
|
||||
"#,
|
||||
)
|
||||
.bind(DC_CONTACT_ID_SELF as i32)
|
||||
.bind(locations_send_begin)
|
||||
.bind(locations_last_sent)
|
||||
.bind(DC_CONTACT_ID_SELF as i32)
|
||||
.fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (location_id, latitude, longitude, accuracy, timestamp): (i32, f64, f64, f64, i64) =
|
||||
row?;
|
||||
let timestamp = get_kml_timestamp(timestamp);
|
||||
|
||||
ret += &format!(
|
||||
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
|
||||
timestamp,
|
||||
accuracy,
|
||||
longitude,
|
||||
latitude
|
||||
);
|
||||
location_count += 1;
|
||||
last_added_location_id = location_id as u32;
|
||||
}
|
||||
context.sql.query_map(
|
||||
"SELECT id, latitude, longitude, accuracy, timestamp \
|
||||
FROM locations WHERE from_id=? \
|
||||
AND timestamp>=? \
|
||||
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
|
||||
AND independent=0 \
|
||||
GROUP BY timestamp \
|
||||
ORDER BY timestamp;",
|
||||
paramsv![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF],
|
||||
|row| {
|
||||
let location_id: i32 = row.get(0)?;
|
||||
let latitude: f64 = row.get(1)?;
|
||||
let longitude: f64 = row.get(2)?;
|
||||
let accuracy: f64 = row.get(3)?;
|
||||
let timestamp = get_kml_timestamp(row.get(4)?);
|
||||
|
||||
Ok((location_id, latitude, longitude, accuracy, timestamp))
|
||||
},
|
||||
|rows| {
|
||||
for row in rows {
|
||||
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
|
||||
ret += &format!(
|
||||
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
|
||||
timestamp,
|
||||
accuracy,
|
||||
longitude,
|
||||
latitude
|
||||
);
|
||||
location_count += 1;
|
||||
last_added_location_id = location_id as u32;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
).await?;
|
||||
ret += "</Document>\n</kml>";
|
||||
}
|
||||
|
||||
@@ -496,7 +488,7 @@ pub async fn set_kml_sent_timestamp(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||
paramsx![timestamp, chat_id],
|
||||
paramsv![timestamp, chat_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -511,7 +503,7 @@ pub async fn set_msg_location_id(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET location_id=? WHERE id=?;",
|
||||
paramsx![location_id as i32, msg_id],
|
||||
paramsv![location_id, msg_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -538,51 +530,50 @@ pub async fn save(
|
||||
accuracy,
|
||||
..
|
||||
} = location;
|
||||
|
||||
let exists: Option<i32> = context
|
||||
let (loc_id, ts) = context
|
||||
.sql
|
||||
.query_value_optional(
|
||||
"SELECT id FROM locations WHERE timestamp=? AND from_id=?",
|
||||
paramsx![timestamp, contact_id as i32],
|
||||
)
|
||||
.await?;
|
||||
.with_conn(move |mut conn| {
|
||||
let mut stmt_test = conn
|
||||
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
|
||||
let mut stmt_insert = conn.prepare_cached(
|
||||
"INSERT INTO locations\
|
||||
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
|
||||
VALUES (?,?,?,?,?,?,?);",
|
||||
)?;
|
||||
|
||||
if independent || exists.is_none() {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
INSERT INTO locations
|
||||
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent)
|
||||
VALUES (?,?,?,?,?,?,?);
|
||||
"#,
|
||||
paramsx![
|
||||
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
|
||||
|
||||
if independent || !exists {
|
||||
stmt_insert.execute(paramsv![
|
||||
timestamp,
|
||||
contact_id as i32,
|
||||
chat_id,
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
independent
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
independent,
|
||||
])?;
|
||||
|
||||
if timestamp > newest_timestamp {
|
||||
// okay to drop, as we use cached prepared statements
|
||||
newest_timestamp = timestamp;
|
||||
newest_location_id = context
|
||||
.sql
|
||||
.get_rowid2(
|
||||
"locations",
|
||||
"timestamp",
|
||||
timestamp,
|
||||
"from_id",
|
||||
contact_id as i32,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
if timestamp > newest_timestamp {
|
||||
// okay to drop, as we use cached prepared statements
|
||||
drop(stmt_test);
|
||||
drop(stmt_insert);
|
||||
newest_timestamp = timestamp;
|
||||
newest_location_id = crate::sql::get_rowid2(
|
||||
&mut conn,
|
||||
"locations",
|
||||
"timestamp",
|
||||
timestamp,
|
||||
"from_id",
|
||||
contact_id as i32,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok((newest_location_id, newest_timestamp))
|
||||
})
|
||||
.await?;
|
||||
newest_timestamp = ts;
|
||||
newest_location_id = loc_id;
|
||||
}
|
||||
|
||||
Ok(newest_location_id)
|
||||
@@ -596,75 +587,85 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
|
||||
" ----------------- MAYBE_SEND_LOCATIONS -------------- ",
|
||||
);
|
||||
|
||||
let pool = match context.sql.get_pool().await {
|
||||
Ok(pool) => pool,
|
||||
Err(err) => {
|
||||
return job::Status::Finished(Err(err.into()));
|
||||
}
|
||||
};
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, locations_send_begin, locations_last_sent \
|
||||
FROM chats \
|
||||
WHERE locations_send_until>?;",
|
||||
paramsv![now],
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let locations_send_begin: i64 = row.get(1)?;
|
||||
let locations_last_sent: i64 = row.get(2)?;
|
||||
continue_streaming = true;
|
||||
|
||||
let mut rows = sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, locations_send_begin, locations_last_sent
|
||||
FROM chats
|
||||
WHERE locations_send_until>?;
|
||||
"#,
|
||||
)
|
||||
.bind(now)
|
||||
.fetch(&pool);
|
||||
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
||||
if now - locations_last_sent < (60 - 3) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
|
||||
}
|
||||
},
|
||||
|rows| {
|
||||
rows.filter_map(|v| v.transpose())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (chat_id, locations_send_begin, locations_last_sent): (ChatId, i64, i64) = match row {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
warn!(context, "invalid row: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
continue_streaming = true;
|
||||
|
||||
// be a bit tolerant as the timer may not align exactly with time(NULL)
|
||||
if now - locations_last_sent < (60 - 3) {
|
||||
continue;
|
||||
}
|
||||
let exists = context
|
||||
if rows.is_ok() {
|
||||
let msgs = context
|
||||
.sql
|
||||
.exists(
|
||||
r#"
|
||||
SELECT id
|
||||
FROM locations
|
||||
WHERE from_id=?
|
||||
AND timestamp>=?
|
||||
AND timestamp>?
|
||||
AND independent=0
|
||||
ORDER BY timestamp;",
|
||||
"#,
|
||||
paramsx![
|
||||
DC_CONTACT_ID_SELF as i32,
|
||||
locations_send_begin,
|
||||
locations_last_sent,
|
||||
],
|
||||
)
|
||||
.with_conn(move |conn| {
|
||||
let rows = rows.unwrap();
|
||||
|
||||
let mut stmt_locations = conn.prepare_cached(
|
||||
"SELECT id \
|
||||
FROM locations \
|
||||
WHERE from_id=? \
|
||||
AND timestamp>=? \
|
||||
AND timestamp>? \
|
||||
AND independent=0 \
|
||||
ORDER BY timestamp;",
|
||||
)?;
|
||||
|
||||
let mut msgs = Vec::new();
|
||||
for (chat_id, locations_send_begin, locations_last_sent) in &rows {
|
||||
if !stmt_locations
|
||||
.exists(paramsv![
|
||||
DC_CONTACT_ID_SELF,
|
||||
*locations_send_begin,
|
||||
*locations_last_sent,
|
||||
])
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// if there is no new location, there's nothing to send.
|
||||
// however, maybe we want to bypass this test eg. 15 minutes
|
||||
} else {
|
||||
// pending locations are attached automatically to every message,
|
||||
// so also to this empty text message.
|
||||
// DC_CMD_LOCATION is only needed to create a nicer subject.
|
||||
//
|
||||
// for optimisation and to avoid flooding the sending queue,
|
||||
// we could sending these messages only if we're really online.
|
||||
// the easiest way to determine this, is to check for an empty message queue.
|
||||
// (might not be 100%, however, as positions are sent combined later
|
||||
// and dc_set_location() is typically called periodically, this is ok)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.hidden = true;
|
||||
msg.param.set_cmd(SystemMessage::LocationOnly);
|
||||
msgs.push((*chat_id, msg));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(msgs)
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if !exists {
|
||||
// if there is no new location, there's nothing to send.
|
||||
// however, maybe we want to bypass this test eg. 15 minutes
|
||||
} else {
|
||||
// pending locations are attached automatically to every message,
|
||||
// so also to this empty text message.
|
||||
// DC_CMD_LOCATION is only needed to create a nicer subject.
|
||||
//
|
||||
// for optimisation and to avoid flooding the sending queue,
|
||||
// we could sending these messages only if we're really online.
|
||||
// the easiest way to determine this, is to check for an empty message queue.
|
||||
// (might not be 100%, however, as positions are sent combined later
|
||||
// and dc_set_location() is typically called periodically, this is ok)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.hidden = true;
|
||||
msg.param.set_cmd(SystemMessage::LocationOnly);
|
||||
|
||||
for (chat_id, mut msg) in msgs.into_iter() {
|
||||
// TODO: better error handling
|
||||
chat::send_msg(context, chat_id, &mut msg)
|
||||
.await
|
||||
@@ -675,7 +676,6 @@ SELECT id
|
||||
if continue_streaming {
|
||||
schedule_maybe_send_locations(context, true).await;
|
||||
}
|
||||
|
||||
job::Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
@@ -689,12 +689,13 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
|
||||
let chat_id = ChatId::new(job.foreign_id);
|
||||
|
||||
let (send_begin, send_until): (i64, i64) = job_try!(
|
||||
let (send_begin, send_until) = job_try!(
|
||||
context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
|
||||
paramsx![chat_id],
|
||||
paramsv![chat_id],
|
||||
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
|
||||
)
|
||||
.await
|
||||
);
|
||||
@@ -706,8 +707,8 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
if !(send_begin == 0 && send_until == 0) {
|
||||
// not streaming, device-message already sent
|
||||
job_try!(context.sql.execute(
|
||||
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
|
||||
paramsx![chat_id],
|
||||
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
|
||||
paramsv![chat_id],
|
||||
).await);
|
||||
|
||||
let stock_str = context
|
||||
|
||||
11
src/log.rs
11
src/log.rs
@@ -41,6 +41,17 @@ macro_rules! error {
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error_network {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
error_network!($ctx, $msg,)
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
emit_event!($ctx, $crate::Event::ErrorNetwork(formatted));
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! emit_event {
|
||||
($ctx:expr, $event:expr) => {
|
||||
|
||||
@@ -56,54 +56,63 @@ impl LoginParam {
|
||||
|
||||
let key = format!("{}addr", prefix);
|
||||
let addr = sql
|
||||
.get_raw_config(key)
|
||||
.get_raw_config(context, key)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let key = format!("{}mail_server", prefix);
|
||||
let mail_server = sql.get_raw_config(key).await.unwrap_or_default();
|
||||
let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_port", prefix);
|
||||
let mail_port = sql.get_raw_config_int(key).await.unwrap_or_default();
|
||||
let mail_port = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_user", prefix);
|
||||
let mail_user = sql.get_raw_config(key).await.unwrap_or_default();
|
||||
let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_pw", prefix);
|
||||
let mail_pw = sql.get_raw_config(key).await.unwrap_or_default();
|
||||
let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
|
||||
let key = format!("{}imap_certificate_checks", prefix);
|
||||
let imap_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(key).await {
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let key = format!("{}send_server", prefix);
|
||||
let send_server = sql.get_raw_config(key).await.unwrap_or_default();
|
||||
let send_server = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_port", prefix);
|
||||
let send_port = sql.get_raw_config_int(key).await.unwrap_or_default();
|
||||
let send_port = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_user", prefix);
|
||||
let send_user = sql.get_raw_config(key).await.unwrap_or_default();
|
||||
let send_user = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_pw", prefix);
|
||||
let send_pw = sql.get_raw_config(key).await.unwrap_or_default();
|
||||
let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
|
||||
let key = format!("{}smtp_certificate_checks", prefix);
|
||||
let smtp_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(key).await {
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let key = format!("{}server_flags", prefix);
|
||||
let server_flags = sql.get_raw_config_int(key).await.unwrap_or_default();
|
||||
let server_flags = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
LoginParam {
|
||||
addr,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use deltachat_derive::*;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
|
||||
use crate::key::Fingerprint;
|
||||
|
||||
@@ -22,7 +22,7 @@ pub struct Lot {
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||||
pub enum Meaning {
|
||||
None = 0,
|
||||
Text1Draft = 1,
|
||||
@@ -67,7 +67,7 @@ impl Lot {
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||||
pub enum LotState {
|
||||
// Default
|
||||
Undefined = 0,
|
||||
@@ -91,6 +91,9 @@ pub enum LotState {
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// id=contact
|
||||
QrAddr = 320,
|
||||
|
||||
|
||||
683
src/message.rs
683
src/message.rs
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
use async_std::prelude::*;
|
||||
use chrono::TimeZone;
|
||||
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
|
||||
|
||||
@@ -10,6 +9,7 @@ use crate::contact::*;
|
||||
use crate::context::{get_version_str, Context};
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee::*;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::location;
|
||||
use crate::message::{self, Message};
|
||||
@@ -88,33 +88,32 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
if chat.is_self_talk() {
|
||||
recipients.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
} else {
|
||||
let pool = context.sql.get_pool().await?;
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT c.authname, c.addr \
|
||||
FROM chats_contacts cc \
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id \
|
||||
WHERE cc.chat_id=? AND cc.contact_id>9;",
|
||||
paramsv![msg.chat_id],
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((authname, addr))
|
||||
},
|
||||
|rows| {
|
||||
for row in rows {
|
||||
let (authname, addr) = row?;
|
||||
if !recipients_contain_addr(&recipients, &addr) {
|
||||
recipients.push((authname, addr));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut rows = sqlx::query_as(
|
||||
r#"
|
||||
SELECT c.authname, c.addr
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id
|
||||
WHERE cc.chat_id=? AND cc.contact_id>9;
|
||||
"#,
|
||||
)
|
||||
.bind(msg.chat_id)
|
||||
.fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (authname, addr): (String, String) = row?;
|
||||
|
||||
if !recipients_contain_addr(&recipients, &addr) {
|
||||
recipients.push((authname, addr));
|
||||
}
|
||||
}
|
||||
|
||||
let command = msg.param.get_cmd();
|
||||
|
||||
if command != SystemMessage::AutocryptSetupMessage
|
||||
&& command != SystemMessage::SecurejoinMessage
|
||||
&& context.get_config_bool(Config::MdnsEnabled).await
|
||||
{
|
||||
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await {
|
||||
req_mdn = true;
|
||||
}
|
||||
}
|
||||
@@ -122,15 +121,18 @@ SELECT c.authname, c.addr
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
|
||||
paramsx![msg.id],
|
||||
paramsv![msg.id],
|
||||
|row| {
|
||||
let in_reply_to: String = row.get(0)?;
|
||||
let references: String = row.get(1)?;
|
||||
|
||||
Ok((
|
||||
render_rfc724_mid_list(&in_reply_to),
|
||||
render_rfc724_mid_list(&references),
|
||||
))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|(in_reply_to, references): (String, String)| {
|
||||
(
|
||||
render_rfc724_mid_list(&in_reply_to),
|
||||
render_rfc724_mid_list(&references),
|
||||
)
|
||||
})?;
|
||||
.await?;
|
||||
|
||||
let default_str = context
|
||||
.stock_str(StockMessage::StatusLine)
|
||||
@@ -205,7 +207,7 @@ SELECT c.authname, c.addr
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate>, &str)>, Error> {
|
||||
async fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate<'_>>, &str)>, Error> {
|
||||
let self_addr = self
|
||||
.context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
@@ -219,7 +221,7 @@ SELECT c.authname, c.addr
|
||||
.filter(|(_, addr)| addr != &self_addr)
|
||||
{
|
||||
res.push((
|
||||
Peerstate::from_addr(self.context, addr).await.ok(),
|
||||
Peerstate::from_addr(self.context, addr).await,
|
||||
addr.as_str(),
|
||||
));
|
||||
}
|
||||
@@ -492,7 +494,16 @@ SELECT c.authname, c.addr
|
||||
let e2ee_guaranteed = self.is_e2ee_guaranteed();
|
||||
let encrypt_helper = EncryptHelper::new(self.context).await?;
|
||||
|
||||
let subject = encode_words(&subject_str);
|
||||
let subject = if subject_str
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
|
||||
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
|
||||
// but we do not want to encode all subjects just because they contain a space.
|
||||
{
|
||||
subject_str
|
||||
} else {
|
||||
encode_words(&subject_str)
|
||||
};
|
||||
|
||||
let mut message = match self.loaded {
|
||||
Loaded::Message { .. } => {
|
||||
@@ -520,6 +531,14 @@ SELECT c.authname, c.addr
|
||||
Loaded::MDN { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
|
||||
};
|
||||
|
||||
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(self.context).await?;
|
||||
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
|
||||
protected_headers.push(Header::new(
|
||||
"Ephemeral-Timer".to_string(),
|
||||
duration.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// we could also store the message-id in the protected headers
|
||||
// which would probably help to survive providers like
|
||||
// Outlook.com or hotmail which mangle the Message-ID.
|
||||
@@ -770,6 +789,26 @@ SELECT c.authname, c.addr
|
||||
"location-streaming-enabled".into(),
|
||||
));
|
||||
}
|
||||
SystemMessage::EphemeralTimerChanged => {
|
||||
protected_headers.push(Header::new(
|
||||
"Chat-Content".to_string(),
|
||||
"ephemeral-timer-changed".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::LocationOnly => {
|
||||
// This should prevent automatic replies,
|
||||
// such as non-delivery reports.
|
||||
//
|
||||
// See https://tools.ietf.org/html/rfc3834
|
||||
//
|
||||
// Adding this header without encryption leaks some
|
||||
// information about the message contents, but it can
|
||||
// already be easily guessed from message timing and size.
|
||||
unprotected_headers.push(Header::new(
|
||||
"Auto-Submitted".to_string(),
|
||||
"auto-generated".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::AutocryptSetupMessage => {
|
||||
unprotected_headers
|
||||
.push(Header::new("Autocrypt-Setup-Message".into(), "v1".into()));
|
||||
@@ -831,6 +870,19 @@ SELECT c.authname, c.addr
|
||||
|
||||
if self.msg.viewtype == Viewtype::Sticker {
|
||||
protected_headers.push(Header::new("Chat-Content".into(), "sticker".into()));
|
||||
} else if self.msg.viewtype == Viewtype::VideochatInvitation {
|
||||
protected_headers.push(Header::new(
|
||||
"Chat-Content".into(),
|
||||
"videochat-invitation".into(),
|
||||
));
|
||||
protected_headers.push(Header::new(
|
||||
"Chat-Webrtc-Room".into(),
|
||||
self.msg
|
||||
.param
|
||||
.get(Param::WebrtcRoom)
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
if self.msg.viewtype == Viewtype::Voice
|
||||
@@ -1373,6 +1425,54 @@ mod tests {
|
||||
.as_bytes(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// 5. Receive an mdn (read receipt) and make sure the mdn's subject is not used
|
||||
let t = TestContext::new_alice().await;
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
b"From: alice@example.com\n\
|
||||
To: Charlie <charlie@example.com>\n\
|
||||
Subject: Hello, Charlie\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let new_msg = incoming_msg_to_reply_msg(b"From: charlie@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: message opened\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <Mr.12345678902@example.com>\n\
|
||||
Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\
|
||||
\n\
|
||||
\n\
|
||||
--SNIPP\n\
|
||||
Content-Type: text/plain; charset=utf-8\n\
|
||||
\n\
|
||||
Read receipts do not guarantee sth. was read.\n\
|
||||
\n\
|
||||
\n\
|
||||
--SNIPP\n\
|
||||
Content-Type: message/disposition-notification\n\
|
||||
\n\
|
||||
Reporting-UA: Delta Chat 1.28.0\n\
|
||||
Original-Recipient: rfc822;charlie@example.com\n\
|
||||
Final-Recipient: rfc822;charlie@example.com\n\
|
||||
Original-Message-ID: <2893@example.com>\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
\n", &t.ctx).await;
|
||||
let mf = MimeFactory::from_msg(&t.ctx, &new_msg, false)
|
||||
.await
|
||||
.unwrap();
|
||||
// The subject string should not be "Re: message opened"
|
||||
assert_eq!("Re: Hello, Charlie", mf.subject_str().await);
|
||||
}
|
||||
|
||||
async fn first_subject_str(t: TestContext) -> String {
|
||||
@@ -1409,7 +1509,7 @@ mod tests {
|
||||
mf.subject_str().await
|
||||
}
|
||||
|
||||
// Creates a mimefactory for a message that replies "Hi" to the incoming message in `imf_raw`.
|
||||
// Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`.
|
||||
async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message {
|
||||
context
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use deltachat_derive::*;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use lazy_static::lazy_static;
|
||||
use lettre_email::mime::{self, Mime};
|
||||
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||
@@ -46,7 +46,14 @@ pub struct MimeMessage {
|
||||
pub from: Vec<SingleInfo>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub decrypting_failed: bool,
|
||||
|
||||
/// Set of valid signature fingerprints if a message is an
|
||||
/// Autocrypt encrypted and signed message.
|
||||
///
|
||||
/// If a message is not encrypted or the signature is not valid,
|
||||
/// this set is empty.
|
||||
pub signatures: HashSet<Fingerprint>,
|
||||
|
||||
pub gossipped_addr: HashSet<String>,
|
||||
pub is_forwarded: bool,
|
||||
pub is_system_message: SystemMessage,
|
||||
@@ -64,7 +71,7 @@ pub(crate) enum AvatarAction {
|
||||
Change(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||||
#[repr(i32)]
|
||||
pub enum SystemMessage {
|
||||
Unknown = 0,
|
||||
@@ -76,6 +83,9 @@ pub enum SystemMessage {
|
||||
SecurejoinMessage = 7,
|
||||
LocationStreamingEnabled = 8,
|
||||
LocationOnly = 9,
|
||||
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged = 10,
|
||||
}
|
||||
|
||||
impl Default for SystemMessage {
|
||||
@@ -214,6 +224,8 @@ impl MimeMessage {
|
||||
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
|
||||
if value == "location-streaming-enabled" {
|
||||
self.is_system_message = SystemMessage::LocationStreamingEnabled;
|
||||
} else if value == "ephemeral-timer-changed" {
|
||||
self.is_system_message = SystemMessage::EphemeralTimerChanged;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -230,10 +242,24 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_videochat_headers(&mut self) {
|
||||
if let Some(value) = self.get(HeaderDef::ChatContent).cloned() {
|
||||
if value == "videochat-invitation" {
|
||||
let instance = self.get(HeaderDef::ChatWebrtcRoom).cloned();
|
||||
if let Some(part) = self.parts.first_mut() {
|
||||
part.typ = Viewtype::VideochatInvitation;
|
||||
part.param
|
||||
.set(Param::WebrtcRoom, instance.unwrap_or_default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Squashes mutlipart chat messages with attachment into single-part messages.
|
||||
///
|
||||
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
|
||||
/// containing an explanation. If such a message is detected, first part can be safely dropped.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn squash_attachment_parts(&mut self) {
|
||||
if let [textpart, filepart] = &self.parts[..] {
|
||||
let need_drop = {
|
||||
@@ -267,22 +293,21 @@ impl MimeMessage {
|
||||
fn parse_attachments(&mut self) {
|
||||
// Attachment messages should be squashed into a single part
|
||||
// before calling this function.
|
||||
if self.parts.len() == 1 {
|
||||
if self.parts[0].typ == Viewtype::Audio
|
||||
&& self.get(HeaderDef::ChatVoiceMessage).is_some()
|
||||
{
|
||||
let part_mut = &mut self.parts[0];
|
||||
part_mut.typ = Viewtype::Voice;
|
||||
if self.parts.len() != 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(mut part) = self.parts.pop() {
|
||||
if part.typ == Viewtype::Audio && self.get(HeaderDef::ChatVoiceMessage).is_some() {
|
||||
part.typ = Viewtype::Voice;
|
||||
}
|
||||
if self.parts[0].typ == Viewtype::Image {
|
||||
if part.typ == Viewtype::Image {
|
||||
if let Some(value) = self.get(HeaderDef::ChatContent) {
|
||||
if value == "sticker" {
|
||||
let part_mut = &mut self.parts[0];
|
||||
part_mut.typ = Viewtype::Sticker;
|
||||
part.typ = Viewtype::Sticker;
|
||||
}
|
||||
}
|
||||
}
|
||||
let part = &self.parts[0];
|
||||
if part.typ == Viewtype::Audio
|
||||
|| part.typ == Viewtype::Voice
|
||||
|| part.typ == Viewtype::Video
|
||||
@@ -290,17 +315,19 @@ impl MimeMessage {
|
||||
if let Some(field_0) = self.get(HeaderDef::ChatDuration) {
|
||||
let duration_ms = field_0.parse().unwrap_or_default();
|
||||
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
|
||||
let part_mut = &mut self.parts[0];
|
||||
part_mut.param.set_int(Param::Duration, duration_ms);
|
||||
part.param.set_int(Param::Duration, duration_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_headers(&mut self, context: &Context) -> Result<()> {
|
||||
self.parse_system_message_headers(context)?;
|
||||
self.parse_avatar_headers();
|
||||
self.parse_videochat_headers();
|
||||
self.squash_attachment_parts();
|
||||
|
||||
if let Some(ref subject) = self.get_subject() {
|
||||
@@ -316,12 +343,11 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
if prepend_subject {
|
||||
let subj = if let Some(n) = subject.find('[') {
|
||||
&subject[0..n]
|
||||
} else {
|
||||
subject
|
||||
}
|
||||
.trim();
|
||||
let subj = subject
|
||||
.find('[')
|
||||
.and_then(|n| subject.get(..n))
|
||||
.unwrap_or(subject)
|
||||
.trim();
|
||||
|
||||
if !subj.is_empty() {
|
||||
for part in self.parts.iter_mut() {
|
||||
@@ -379,8 +405,7 @@ impl MimeMessage {
|
||||
Some(AvatarAction::Delete)
|
||||
} else {
|
||||
let mut i = 0;
|
||||
while i != self.parts.len() {
|
||||
let part = &mut self.parts[i];
|
||||
while let Some(part) = self.parts.get_mut(i) {
|
||||
if let Some(part_filename) = &part.org_filename {
|
||||
if part_filename == &header_value {
|
||||
if let Some(blob) = part.param.get(Param::File) {
|
||||
@@ -397,6 +422,11 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the message was encrypted as defined in
|
||||
/// Autocrypt standard.
|
||||
///
|
||||
/// This means the message was both encrypted and signed with a
|
||||
/// valid signature.
|
||||
pub fn was_encrypted(&self) -> bool {
|
||||
!self.signatures.is_empty()
|
||||
}
|
||||
@@ -544,7 +574,7 @@ impl MimeMessage {
|
||||
contains exactly two body parts. The first body
|
||||
part is the body part over which the digital signature was created [...]
|
||||
The second body part contains the control information necessary to
|
||||
verify the digital signature." We simpliy take the first body part and
|
||||
verify the digital signature." We simply take the first body part and
|
||||
skip the rest. (see
|
||||
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
|
||||
for background information why we use encrypted+signed) */
|
||||
@@ -765,16 +795,11 @@ impl MimeMessage {
|
||||
}
|
||||
|
||||
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
|
||||
if self.parts.is_empty() {
|
||||
return;
|
||||
if let Some(part) = self.parts.first_mut() {
|
||||
part.typ = Viewtype::Text;
|
||||
part.msg = format!("[{}]", error_msg.as_ref());
|
||||
self.parts.truncate(1);
|
||||
}
|
||||
|
||||
let part = &mut self.parts[0];
|
||||
part.typ = Viewtype::Text;
|
||||
part.msg = format!("[{}]", error_msg.as_ref());
|
||||
self.parts.truncate(1);
|
||||
|
||||
assert_eq!(self.parts.len(), 1);
|
||||
}
|
||||
|
||||
pub fn get_rfc724_mid(&self) -> Option<String> {
|
||||
@@ -825,7 +850,11 @@ impl MimeMessage {
|
||||
report: &mailparse::ParsedMail<'_>,
|
||||
) -> Result<Option<Report>> {
|
||||
// parse as mailheaders
|
||||
let report_body = report.subparts[1].get_body_raw()?;
|
||||
let report_body = if let Some(subpart) = report.subparts.get(1) {
|
||||
subpart.get_body_raw()?
|
||||
} else {
|
||||
bail!("Report does not have second MIME part");
|
||||
};
|
||||
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
||||
|
||||
// must be present
|
||||
@@ -903,6 +932,7 @@ impl MimeMessage {
|
||||
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
|
||||
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
|
||||
/// Also you should add a test in dc_receive_imf.rs (there already are lots of test_parse_ndn_* tests).
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn heuristically_parse_ndn(&mut self, context: &Context) -> Option<()> {
|
||||
let maybe_ndn = if let Some(from) = self.get(HeaderDef::From_) {
|
||||
let from = from.to_ascii_lowercase();
|
||||
@@ -961,11 +991,10 @@ impl MimeMessage {
|
||||
if let Some(failure_report) = &self.failure_report {
|
||||
let error = parts.iter().find(|p| p.typ == Viewtype::Text).map(|p| {
|
||||
let msg = &p.msg;
|
||||
match msg.find("\n--- ") {
|
||||
Some(footer_start) => &msg[..footer_start],
|
||||
None => msg,
|
||||
}
|
||||
.trim()
|
||||
msg.find("\n--- ")
|
||||
.and_then(|footer_start| msg.get(..footer_start))
|
||||
.unwrap_or(msg)
|
||||
.trim()
|
||||
});
|
||||
message::handle_ndn(context, failure_report, error).await
|
||||
}
|
||||
@@ -989,12 +1018,12 @@ async fn update_gossip_peerstates(
|
||||
.iter()
|
||||
.any(|info| info.addr == header.addr.to_lowercase())
|
||||
{
|
||||
let mut peerstate = Peerstate::from_addr(context, &header.addr).await.ok();
|
||||
let mut peerstate = Peerstate::from_addr(context, &header.addr).await;
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate.apply_gossip(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
} else {
|
||||
let p = Peerstate::from_gossip(header, message_time);
|
||||
let p = Peerstate::from_gossip(context, header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
@@ -1031,6 +1060,7 @@ pub(crate) struct FailureReport {
|
||||
pub failed_recipient: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) fn parse_message_ids(ids: &str) -> Result<Vec<String>> {
|
||||
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
|
||||
let mut msgids = Vec::new();
|
||||
@@ -1433,6 +1463,28 @@ mod tests {
|
||||
assert!(mimeparser.group_avatar.unwrap().is_change());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_mimeparser_with_videochat() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let raw = include_bytes!("../test-data/message/videochat_invitation.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::VideochatInvitation);
|
||||
assert_eq!(
|
||||
mimeparser.parts[0]
|
||||
.param
|
||||
.get(Param::WebrtcRoom)
|
||||
.unwrap_or_default(),
|
||||
"https://example.org/p2p/?roomname=6HiduoAn4xN"
|
||||
);
|
||||
assert!(mimeparser.parts[0]
|
||||
.msg
|
||||
.contains("https://example.org/p2p/?roomname=6HiduoAn4xN"));
|
||||
assert_eq!(mimeparser.user_avatar, None);
|
||||
assert_eq!(mimeparser.group_avatar, None);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_mimeparser_message_kml() {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
@@ -95,7 +95,10 @@ pub async fn dc_get_oauth2_access_token(
|
||||
|
||||
// read generated token
|
||||
if !regenerate && !is_expired(context).await {
|
||||
let access_token = context.sql.get_raw_config("oauth2_access_token").await;
|
||||
let access_token = context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_access_token")
|
||||
.await;
|
||||
if access_token.is_some() {
|
||||
// success
|
||||
return access_token;
|
||||
@@ -103,10 +106,13 @@ pub async fn dc_get_oauth2_access_token(
|
||||
}
|
||||
|
||||
// generate new token: build & call auth url
|
||||
let refresh_token = context.sql.get_raw_config("oauth2_refresh_token").await;
|
||||
let refresh_token = context
|
||||
.sql
|
||||
.get_raw_config(context, "oauth2_refresh_token")
|
||||
.await;
|
||||
let refresh_token_for = context
|
||||
.sql
|
||||
.get_raw_config("oauth2_refresh_token_for")
|
||||
.get_raw_config(context, "oauth2_refresh_token_for")
|
||||
.await
|
||||
.unwrap_or_else(|| "unset".into());
|
||||
|
||||
@@ -116,7 +122,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
(
|
||||
context
|
||||
.sql
|
||||
.get_raw_config("oauth2_pending_redirect_uri")
|
||||
.get_raw_config(context, "oauth2_pending_redirect_uri")
|
||||
.await
|
||||
.unwrap_or_else(|| "unset".into()),
|
||||
oauth2.init_token,
|
||||
@@ -130,7 +136,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
(
|
||||
context
|
||||
.sql
|
||||
.get_raw_config("oauth2_redirect_uri")
|
||||
.get_raw_config(context, "oauth2_redirect_uri")
|
||||
.await
|
||||
.unwrap_or_else(|| "unset".into()),
|
||||
oauth2.refresh_token,
|
||||
@@ -354,7 +360,7 @@ impl Oauth2 {
|
||||
async fn is_expired(context: &Context) -> bool {
|
||||
let expire_timestamp = context
|
||||
.sql
|
||||
.get_raw_config_int64("oauth2_timestamp_expires")
|
||||
.get_raw_config_int64(context, "oauth2_timestamp_expires")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
@@ -68,6 +68,9 @@ pub enum Param {
|
||||
/// For Messages
|
||||
AttachGroupImage = b'A',
|
||||
|
||||
/// For Messages
|
||||
WebrtcRoom = b'V',
|
||||
|
||||
/// For Messages: space-separated list of messaged IDs of forwarded copies.
|
||||
///
|
||||
/// This is used when a [crate::message::Message] is in the
|
||||
@@ -171,7 +174,7 @@ impl str::FromStr for Params {
|
||||
let key = key.unwrap_or_default().trim();
|
||||
let value = value.unwrap_or_default().trim();
|
||||
|
||||
if let Some(key) = Param::from_u8(key.as_bytes()[0]) {
|
||||
if let Some(key) = key.as_bytes().first().and_then(|key| Param::from_u8(*key)) {
|
||||
inner.insert(key, value.to_string());
|
||||
} else {
|
||||
bail!("Unknown key: {}", key);
|
||||
|
||||
249
src/peerstate.rs
249
src/peerstate.rs
@@ -1,8 +1,8 @@
|
||||
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::*;
|
||||
use crate::context::Context;
|
||||
@@ -24,8 +24,8 @@ pub enum PeerstateVerifiedStatus {
|
||||
}
|
||||
|
||||
/// Peerstate represents the state of an Autocrypt peer.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Peerstate {
|
||||
pub struct Peerstate<'a> {
|
||||
pub context: &'a Context,
|
||||
pub addr: String,
|
||||
pub last_seen: i64,
|
||||
pub last_seen_autocrypt: i64,
|
||||
@@ -41,49 +41,43 @@ pub struct Peerstate {
|
||||
pub degrade_event: Option<DegradeEvent>,
|
||||
}
|
||||
|
||||
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Peerstate {
|
||||
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
|
||||
use sqlx::Row;
|
||||
impl<'a> PartialEq for Peerstate<'a> {
|
||||
fn eq(&self, other: &Peerstate) -> bool {
|
||||
self.addr == other.addr
|
||||
&& self.last_seen == other.last_seen
|
||||
&& self.last_seen_autocrypt == other.last_seen_autocrypt
|
||||
&& self.prefer_encrypt == other.prefer_encrypt
|
||||
&& self.public_key == other.public_key
|
||||
&& self.public_key_fingerprint == other.public_key_fingerprint
|
||||
&& self.gossip_key == other.gossip_key
|
||||
&& self.gossip_timestamp == other.gossip_timestamp
|
||||
&& self.gossip_key_fingerprint == other.gossip_key_fingerprint
|
||||
&& self.verified_key == other.verified_key
|
||||
&& self.verified_key_fingerprint == other.verified_key_fingerprint
|
||||
&& self.to_save == other.to_save
|
||||
&& self.degrade_event == other.degrade_event
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = Self::new(row.try_get("addr")?);
|
||||
impl<'a> Eq for Peerstate<'a> {}
|
||||
|
||||
res.last_seen = row.try_get_unchecked("last_seen")?;
|
||||
res.last_seen_autocrypt = row.try_get_unchecked("last_seen_autocrypt")?;
|
||||
res.prefer_encrypt = row.try_get("prefer_encrypted")?;
|
||||
res.gossip_timestamp = row.try_get_unchecked("gossip_timestamp")?;
|
||||
|
||||
res.public_key_fingerprint = row
|
||||
.try_get::<Option<String>, _>("public_key_fingerprint")?
|
||||
.map(|fp| fp.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
res.gossip_key_fingerprint = row
|
||||
.try_get::<Option<String>, _>("gossip_key_fingerprint")?
|
||||
.map(|fp| fp.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
res.verified_key_fingerprint = row
|
||||
.try_get::<Option<String>, _>("verified_key_fingerprint")?
|
||||
.map(|fp| fp.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
res.public_key = row
|
||||
.try_get::<Option<&[u8]>, _>("public_key")?
|
||||
.map(|blob| SignedPublicKey::from_slice(blob))
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
res.gossip_key = row
|
||||
.try_get::<Option<&[u8]>, _>("gossip_key")?
|
||||
.map(|blob| SignedPublicKey::from_slice(blob))
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
res.verified_key = row
|
||||
.try_get::<Option<&[u8]>, _>("verified_key")?
|
||||
.map(|blob| SignedPublicKey::from_slice(blob))
|
||||
.transpose()
|
||||
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
|
||||
|
||||
Ok(res)
|
||||
impl<'a> fmt::Debug for Peerstate<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("Peerstate")
|
||||
.field("addr", &self.addr)
|
||||
.field("last_seen", &self.last_seen)
|
||||
.field("last_seen_autocrypt", &self.last_seen_autocrypt)
|
||||
.field("prefer_encrypt", &self.prefer_encrypt)
|
||||
.field("public_key", &self.public_key)
|
||||
.field("public_key_fingerprint", &self.public_key_fingerprint)
|
||||
.field("gossip_key", &self.gossip_key)
|
||||
.field("gossip_timestamp", &self.gossip_timestamp)
|
||||
.field("gossip_key_fingerprint", &self.gossip_key_fingerprint)
|
||||
.field("verified_key", &self.verified_key)
|
||||
.field("verified_key_fingerprint", &self.verified_key_fingerprint)
|
||||
.field("to_save", &self.to_save)
|
||||
.field("degrade_event", &self.degrade_event)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,9 +98,10 @@ pub enum DegradeEvent {
|
||||
FingerprintChanged = 0x02,
|
||||
}
|
||||
|
||||
impl Peerstate {
|
||||
pub fn new(addr: String) -> Self {
|
||||
impl<'a> Peerstate<'a> {
|
||||
pub fn new(context: &'a Context, addr: String) -> Self {
|
||||
Peerstate {
|
||||
context,
|
||||
addr,
|
||||
last_seen: 0,
|
||||
last_seen_autocrypt: 0,
|
||||
@@ -123,8 +118,8 @@ impl Peerstate {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_header(header: &Aheader, message_time: i64) -> Self {
|
||||
let mut res = Self::new(header.addr.clone());
|
||||
pub fn from_header(context: &'a Context, header: &Aheader, message_time: i64) -> Self {
|
||||
let mut res = Self::new(context, header.addr.clone());
|
||||
|
||||
res.last_seen = message_time;
|
||||
res.last_seen_autocrypt = message_time;
|
||||
@@ -136,8 +131,8 @@ impl Peerstate {
|
||||
res
|
||||
}
|
||||
|
||||
pub fn from_gossip(gossip_header: &Aheader, message_time: i64) -> Self {
|
||||
let mut res = Self::new(gossip_header.addr.clone());
|
||||
pub fn from_gossip(context: &'a Context, gossip_header: &Aheader, message_time: i64) -> Self {
|
||||
let mut res = Self::new(context, gossip_header.addr.clone());
|
||||
|
||||
res.gossip_timestamp = message_time;
|
||||
res.to_save = Some(ToSave::All);
|
||||
@@ -147,53 +142,76 @@ impl Peerstate {
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn from_addr(context: &Context, addr: &str) -> Result<Peerstate> {
|
||||
let query = r#"
|
||||
SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key,
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint,
|
||||
verified_key, verified_key_fingerprint
|
||||
FROM acpeerstates
|
||||
WHERE addr=? COLLATE NOCASE;
|
||||
"#;
|
||||
Self::from_stmt(context, query, paramsx![addr]).await
|
||||
pub async fn from_addr(context: &'a Context, addr: &str) -> Option<Peerstate<'a>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, verified_key, verified_key_fingerprint FROM acpeerstates WHERE addr=? COLLATE NOCASE;";
|
||||
Self::from_stmt(context, query, paramsv![addr]).await
|
||||
}
|
||||
|
||||
pub async fn from_fingerprint(
|
||||
context: &Context,
|
||||
context: &'a Context,
|
||||
_sql: &Sql,
|
||||
fingerprint: &Fingerprint,
|
||||
) -> Result<Peerstate> {
|
||||
let query = r#"
|
||||
SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key,
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint,
|
||||
verified_key, verified_key_fingerprint
|
||||
FROM acpeerstates
|
||||
WHERE public_key_fingerprint=? COLLATE NOCASE
|
||||
OR gossip_key_fingerprint=? COLLATE NOCASE
|
||||
ORDER BY public_key_fingerprint=? DESC;
|
||||
"#;
|
||||
|
||||
let fingerprint = fingerprint.hex();
|
||||
Self::from_stmt(
|
||||
context,
|
||||
query,
|
||||
paramsx![&fingerprint, &fingerprint, &fingerprint],
|
||||
)
|
||||
.await
|
||||
) -> Option<Peerstate<'a>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
verified_key, verified_key_fingerprint \
|
||||
FROM acpeerstates \
|
||||
WHERE public_key_fingerprint=? COLLATE NOCASE \
|
||||
OR gossip_key_fingerprint=? COLLATE NOCASE \
|
||||
ORDER BY public_key_fingerprint=? DESC;";
|
||||
let fp = fingerprint.hex();
|
||||
Self::from_stmt(context, query, paramsv![fp, fp, fp]).await
|
||||
}
|
||||
|
||||
async fn from_stmt<'a, P: sqlx::IntoArguments<'a, sqlx::sqlite::Sqlite> + 'a>(
|
||||
context: &Context,
|
||||
query: &'a str,
|
||||
params: P,
|
||||
) -> Result<Peerstate> {
|
||||
/* all the above queries start with this: SELECT
|
||||
addr, last_seen, last_seen_autocrypt, prefer_encrypted,
|
||||
public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
|
||||
gossip_key_fingerprint, verified_key, verified_key_fingerprint
|
||||
*/
|
||||
let peerstate = context.sql.query_row(query, params).await?;
|
||||
async fn from_stmt(
|
||||
context: &'a Context,
|
||||
query: &str,
|
||||
params: Vec<&dyn crate::ToSql>,
|
||||
) -> Option<Peerstate<'a>> {
|
||||
context
|
||||
.sql
|
||||
.query_row(query, params, |row| {
|
||||
/* all the above queries start with this: SELECT
|
||||
addr, last_seen, last_seen_autocrypt, prefer_encrypted,
|
||||
public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
|
||||
gossip_key_fingerprint, verified_key, verified_key_fingerprint
|
||||
*/
|
||||
let mut res = Self::new(context, row.get(0)?);
|
||||
|
||||
Ok(peerstate)
|
||||
res.last_seen = row.get(1)?;
|
||||
res.last_seen_autocrypt = row.get(2)?;
|
||||
res.prefer_encrypt = EncryptPreference::from_i32(row.get(3)?).unwrap_or_default();
|
||||
res.gossip_timestamp = row.get(5)?;
|
||||
|
||||
res.public_key_fingerprint = row
|
||||
.get::<_, Option<String>>(7)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()?;
|
||||
res.gossip_key_fingerprint = row
|
||||
.get::<_, Option<String>>(8)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()?;
|
||||
res.verified_key_fingerprint = row
|
||||
.get::<_, Option<String>>(10)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()?;
|
||||
res.public_key = row
|
||||
.get(4)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
res.gossip_key = row
|
||||
.get(6)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
res.verified_key = row
|
||||
.get(9)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn recalc_fingerprint(&mut self) {
|
||||
@@ -385,21 +403,19 @@ SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key,
|
||||
if create {
|
||||
sql.execute(
|
||||
"INSERT INTO acpeerstates (addr) VALUES(?);",
|
||||
paramsx![&self.addr],
|
||||
paramsv![self.addr],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if self.to_save == Some(ToSave::All) || create {
|
||||
sql.execute(
|
||||
r#"
|
||||
UPDATE acpeerstates
|
||||
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?,
|
||||
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?,
|
||||
verified_key=?, verified_key_fingerprint=?
|
||||
WHERE addr=?;
|
||||
"#,
|
||||
paramsx![
|
||||
"UPDATE acpeerstates \
|
||||
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \
|
||||
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \
|
||||
verified_key=?, verified_key_fingerprint=? \
|
||||
WHERE addr=?;",
|
||||
paramsv![
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
self.prefer_encrypt as i64,
|
||||
@@ -410,17 +426,18 @@ UPDATE acpeerstates
|
||||
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
&self.addr
|
||||
self.addr,
|
||||
],
|
||||
).await?;
|
||||
} else if self.to_save == Some(ToSave::Timestamps) {
|
||||
sql.execute(
|
||||
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? WHERE addr=?;",
|
||||
paramsx![
|
||||
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
|
||||
WHERE addr=?;",
|
||||
paramsv![
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
self.gossip_timestamp,
|
||||
&self.addr
|
||||
self.addr
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
@@ -438,11 +455,18 @@ UPDATE acpeerstates
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::key::FingerprintError> for rusqlite::Error {
|
||||
fn from(_source: crate::key::FingerprintError) -> Self {
|
||||
Self::InvalidColumnType(0, "Invalid fingerprint".into(), rusqlite::types::Type::Text)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_save_to_db() {
|
||||
@@ -452,6 +476,7 @@ mod tests {
|
||||
let pub_key = alice_keypair().public;
|
||||
|
||||
let mut peerstate = Peerstate {
|
||||
context: &ctx.ctx,
|
||||
addr: addr.into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 11,
|
||||
@@ -479,9 +504,10 @@ mod tests {
|
||||
// clear to_save, as that is not persissted
|
||||
peerstate.to_save = None;
|
||||
assert_eq!(peerstate, peerstate_new);
|
||||
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
|
||||
.await
|
||||
.expect("failed to load peerstate from db");
|
||||
let peerstate_new2 =
|
||||
Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint())
|
||||
.await
|
||||
.expect("failed to load peerstate from db");
|
||||
assert_eq!(peerstate, peerstate_new2);
|
||||
}
|
||||
|
||||
@@ -492,6 +518,7 @@ mod tests {
|
||||
let pub_key = alice_keypair().public;
|
||||
|
||||
let peerstate = Peerstate {
|
||||
context: &ctx.ctx,
|
||||
addr: addr.into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 11,
|
||||
@@ -525,6 +552,7 @@ mod tests {
|
||||
let pub_key = alice_keypair().public;
|
||||
|
||||
let mut peerstate = Peerstate {
|
||||
context: &ctx.ctx,
|
||||
addr: addr.into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 11,
|
||||
@@ -553,4 +581,11 @@ mod tests {
|
||||
peerstate.to_save = None;
|
||||
assert_eq!(peerstate, peerstate_new);
|
||||
}
|
||||
|
||||
// TODO: don't copy this from stress.rs
|
||||
#[allow(dead_code)]
|
||||
struct TestContext {
|
||||
ctx: Context,
|
||||
dir: TempDir,
|
||||
}
|
||||
}
|
||||
|
||||
12
src/pgp.rs
12
src/pgp.rs
@@ -365,11 +365,13 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
|
||||
let decryptor = enc_msg.decrypt_with_password(|| passphrase)?;
|
||||
|
||||
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
|
||||
ensure!(!msgs.is_empty(), "No valid messages found");
|
||||
|
||||
match msgs[0].get_content()? {
|
||||
Some(content) => Ok(content),
|
||||
None => bail!("Decrypted message is empty"),
|
||||
if let Some(msg) = msgs.first() {
|
||||
match msg.get_content()? {
|
||||
Some(content) => Ok(content),
|
||||
None => bail!("Decrypted message is empty"),
|
||||
}
|
||||
} else {
|
||||
bail!("No valid messages found")
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
||||
119
src/qr.rs
119
src/qr.rs
@@ -12,11 +12,13 @@ use crate::context::Context;
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::key::Fingerprint;
|
||||
use crate::lot::{Lot, LotState};
|
||||
use crate::message::Message;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
|
||||
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
|
||||
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
|
||||
const DCWEBRTC_SCHEME: &str = "DCWEBRTC:";
|
||||
const MAILTO_SCHEME: &str = "mailto:";
|
||||
const MATMSG_SCHEME: &str = "MATMSG:";
|
||||
const VCARD_SCHEME: &str = "BEGIN:VCARD";
|
||||
@@ -51,6 +53,8 @@ pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
|
||||
decode_openpgp(context, qr).await
|
||||
} else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) {
|
||||
decode_account(context, qr)
|
||||
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
|
||||
decode_webrtc_instance(context, qr)
|
||||
} else if qr.starts_with(MAILTO_SCHEME) {
|
||||
decode_mailto(context, qr).await
|
||||
} else if qr.starts_with(SMTP_SCHEME) {
|
||||
@@ -68,6 +72,7 @@ pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
|
||||
|
||||
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
|
||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
|
||||
|
||||
@@ -138,10 +143,10 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
let mut lot = Lot::new();
|
||||
|
||||
// retrieve known state for this fingerprint
|
||||
let peerstate = Peerstate::from_fingerprint(context, &fingerprint).await;
|
||||
let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint).await;
|
||||
|
||||
if invitenumber.is_none() || auth.is_none() {
|
||||
if let Ok(peerstate) = peerstate {
|
||||
if let Some(peerstate) = peerstate {
|
||||
lot.state = LotState::QrFprOk;
|
||||
|
||||
lot.id = Contact::add_or_lookup(
|
||||
@@ -187,13 +192,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
}
|
||||
|
||||
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn decode_account(_context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[DCACCOUNT_SCHEME.len()..];
|
||||
|
||||
let mut lot = Lot::new();
|
||||
|
||||
if let Ok(url) = url::Url::parse(payload) {
|
||||
if url.scheme() == "https" {
|
||||
if url.scheme() == "http" || url.scheme() == "https" {
|
||||
lot.state = LotState::QrAccount;
|
||||
lot.text1 = url.host_str().map(|x| x.to_string());
|
||||
} else {
|
||||
@@ -208,6 +214,31 @@ fn decode_account(_context: &Context, qr: &str) -> Lot {
|
||||
lot
|
||||
}
|
||||
|
||||
/// scheme: `DCWEBRTC:https://meet.jit.si/$ROOM`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[DCWEBRTC_SCHEME.len()..];
|
||||
|
||||
let mut lot = Lot::new();
|
||||
|
||||
let (_type, url) = Message::parse_webrtc_instance(payload);
|
||||
if let Ok(url) = url::Url::parse(&url) {
|
||||
if url.scheme() == "http" || url.scheme() == "https" {
|
||||
lot.state = LotState::QrWebrtcInstance;
|
||||
lot.text1 = url.host_str().map(|x| x.to_string());
|
||||
lot.text2 = Some(payload.to_string())
|
||||
} else {
|
||||
lot.state = LotState::QrError;
|
||||
lot.text1 = Some(format!("Bad scheme for webrtc instance: {}", payload));
|
||||
}
|
||||
} else {
|
||||
lot.state = LotState::QrError;
|
||||
lot.text1 = Some(format!("Invalid webrtc instance: {}", payload));
|
||||
}
|
||||
|
||||
lot
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CreateAccountResponse {
|
||||
email: String,
|
||||
@@ -217,7 +248,8 @@ struct CreateAccountResponse {
|
||||
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
|
||||
/// download additional information from the contained url and set the parameters.
|
||||
/// on success, a configure::configure() should be able to log in to the account
|
||||
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
|
||||
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
|
||||
|
||||
let response: Result<CreateAccountResponse, surf::Error> =
|
||||
@@ -237,9 +269,24 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
|
||||
match check_qr(context, &qr).await.state {
|
||||
LotState::QrAccount => set_account_from_qr(context, qr).await,
|
||||
LotState::QrWebrtcInstance => {
|
||||
let val = decode_webrtc_instance(context, qr).text2;
|
||||
context
|
||||
.set_config(Config::WebrtcInstance, val.as_deref())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
_ => bail!("qr code does not contain config: {}", qr),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract address for the mailto scheme.
|
||||
///
|
||||
/// Scheme: `mailto:addr...?subject=...&body=..`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_mailto(context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[MAILTO_SCHEME.len()..];
|
||||
|
||||
@@ -261,6 +308,7 @@ async fn decode_mailto(context: &Context, qr: &str) -> Lot {
|
||||
/// Extract address for the smtp scheme.
|
||||
///
|
||||
/// Scheme: `SMTP:addr...:subject...:body...`
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_smtp(context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[SMTP_SCHEME.len()..];
|
||||
|
||||
@@ -283,6 +331,7 @@ async fn decode_smtp(context: &Context, qr: &str) -> Lot {
|
||||
/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
|
||||
///
|
||||
/// There may or may not be linebreaks after the fields.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_matmsg(context: &Context, qr: &str) -> Lot {
|
||||
// Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
|
||||
// we ignore this case.
|
||||
@@ -316,14 +365,15 @@ lazy_static! {
|
||||
/// Extract address for the matmsg scheme.
|
||||
///
|
||||
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn decode_vcard(context: &Context, qr: &str) -> Lot {
|
||||
let name = VCARD_NAME_RE
|
||||
.captures(qr)
|
||||
.map(|caps| {
|
||||
let last_name = &caps[1];
|
||||
let first_name = &caps[2];
|
||||
.and_then(|caps| {
|
||||
let last_name = caps.get(1)?.as_str().trim();
|
||||
let first_name = caps.get(2)?.as_str().trim();
|
||||
|
||||
format!("{} {}", first_name.trim(), last_name.trim())
|
||||
Some(format!("{} {}", first_name, last_name))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -611,12 +661,31 @@ mod tests {
|
||||
assert_eq!(res.get_text1().unwrap(), "example.org");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_webrtc_instance() {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let res = check_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://basicurl.com/$ROOM").await;
|
||||
assert_eq!(res.get_state(), LotState::QrWebrtcInstance);
|
||||
assert_eq!(res.get_text1().unwrap(), "basicurl.com");
|
||||
assert_eq!(
|
||||
res.get_text2().unwrap(),
|
||||
"basicwebrtc:https://basicurl.com/$ROOM"
|
||||
);
|
||||
|
||||
// Test it again with mixcased "dcWebRTC:" uri scheme
|
||||
let res = check_qr(&ctx.ctx, "dcWebRTC:https://example.org/").await;
|
||||
assert_eq!(res.get_state(), LotState::QrWebrtcInstance);
|
||||
assert_eq!(res.get_text1().unwrap(), "example.org");
|
||||
assert_eq!(res.get_text2().unwrap(), "https://example.org/");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decode_account_bad_scheme() {
|
||||
let ctx = TestContext::new().await;
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
"DCACCOUNT:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.get_state(), LotState::QrError);
|
||||
@@ -625,10 +694,40 @@ mod tests {
|
||||
// Test it again with lowercased "dcaccount:" uri scheme
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"dcaccount:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
"dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.get_state(), LotState::QrError);
|
||||
assert!(res.get_text1().is_some());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_set_config_from_qr() {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
|
||||
assert!(!res.is_ok());
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "https://no.qr").await;
|
||||
assert!(!res.is_ok());
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(),
|
||||
"https://example.org/"
|
||||
);
|
||||
|
||||
let res =
|
||||
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(),
|
||||
"basicwebrtc:https://foo.bar/?$ROOM&test"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![warn(clippy::indexing_slicing)]
|
||||
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{channel, Receiver, Sender};
|
||||
use async_std::task;
|
||||
@@ -76,6 +74,13 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
}
|
||||
None => {
|
||||
jobs_loaded = 0;
|
||||
|
||||
// Expunge folder if needed, e.g. if some jobs have
|
||||
// deleted messages on the server.
|
||||
if let Err(err) = connection.maybe_close_folder(&ctx).await {
|
||||
warn!(ctx, "failed to close folder: {:?}", err);
|
||||
}
|
||||
|
||||
info = if ctx.get_config_bool(Config::InboxWatch).await {
|
||||
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
|
||||
} else {
|
||||
@@ -100,7 +105,7 @@ async fn fetch(ctx: &Context, connection: &mut Imap) {
|
||||
match ctx.get_config(Config::ConfiguredInboxFolder).await {
|
||||
Some(watch_folder) => {
|
||||
if let Err(err) = connection.connect_configured(&ctx).await {
|
||||
error!(ctx, "{}", err);
|
||||
error_network!(ctx, "{}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,7 +127,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
Some(watch_folder) => {
|
||||
// connect and fake idle if unable to connect
|
||||
if let Err(err) = connection.connect_configured(&ctx).await {
|
||||
error!(ctx, "imap connection failed: {}", err);
|
||||
warn!(ctx, "imap connection failed: {}", err);
|
||||
return connection.fake_idle(&ctx, None).await;
|
||||
}
|
||||
|
||||
|
||||
@@ -152,69 +152,81 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
ongoing_allocated: bool,
|
||||
join_vg: bool,
|
||||
) -> ChatId {
|
||||
let mut bob = context.bob.write().await;
|
||||
bob.expects = 0;
|
||||
let ret_chat_id: ChatId = if bob.status == DC_BOB_SUCCESS {
|
||||
if join_vg {
|
||||
chat::get_chat_id_by_grpid(
|
||||
context,
|
||||
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or((ChatId::new(0), false, Blocked::Not))
|
||||
.0
|
||||
} else {
|
||||
contact_chat_id
|
||||
}
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
bob.qr_scan = None;
|
||||
|
||||
if ongoing_allocated {
|
||||
context.free_ongoing().await;
|
||||
}
|
||||
ret_chat_id
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group handshake.
|
||||
/// See the ffi-documentation for more details.
|
||||
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
if let Err(err) = context.alloc_ongoing().await {
|
||||
error!(context, "SecureJoin Error: {}", err);
|
||||
return ChatId::new(0);
|
||||
if context.alloc_ongoing().await.is_err() {
|
||||
return cleanup(&context, ChatId::new(0), false, false).await;
|
||||
}
|
||||
|
||||
match securejoin(context, qr).await {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
error!(context, "SecureJoin Error: {}", err);
|
||||
ChatId::new(0)
|
||||
}
|
||||
}
|
||||
securejoin(context, qr).await
|
||||
}
|
||||
|
||||
/// Bob - the joiner's side
|
||||
/// Step 2 in "Setup verified contact" protocol
|
||||
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, anyhow::Error> {
|
||||
struct DropGuard<'a> {
|
||||
context: &'a Context,
|
||||
}
|
||||
async fn securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
/*========================================================
|
||||
==== Bob - the joiner's side =====
|
||||
==== Step 2 in "Setup verified contact" protocol =====
|
||||
========================================================*/
|
||||
|
||||
impl Drop for DropGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
async_std::task::block_on(async {
|
||||
let mut bob = self.context.bob.write().await;
|
||||
bob.expects = 0;
|
||||
bob.qr_scan = None;
|
||||
self.context.free_ongoing().await;
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut contact_chat_id = ChatId::new(0);
|
||||
let mut join_vg: bool = false;
|
||||
|
||||
let _guard = DropGuard { context: &context };
|
||||
|
||||
info!(context, "Requesting secure-join ...");
|
||||
info!(context, "Requesting secure-join ...",);
|
||||
ensure_secret_key_exists(context).await.ok();
|
||||
let qr_scan = check_qr(context, &qr).await;
|
||||
if qr_scan.state != LotState::QrAskVerifyContact && qr_scan.state != LotState::QrAskVerifyGroup
|
||||
{
|
||||
bail!("Unknown QR code.");
|
||||
error!(context, "Unknown QR code.",);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
let contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id).await {
|
||||
contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id).await {
|
||||
Ok(chat_id) => chat_id,
|
||||
Err(_) => {
|
||||
bail!("Unknown contact.");
|
||||
error!(context, "Unknown contact.");
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
};
|
||||
|
||||
if context.shall_stop_ongoing().await {
|
||||
bail!("Interrupted");
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
|
||||
let join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
|
||||
join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
|
||||
{
|
||||
let mut bob = context.bob.write().await;
|
||||
bob.status = 0;
|
||||
bob.qr_scan = Some(qr_scan);
|
||||
}
|
||||
|
||||
let fp_equals_sender = fingerprint_equals_sender(
|
||||
if fingerprint_equals_sender(
|
||||
context,
|
||||
context
|
||||
.bob
|
||||
@@ -228,22 +240,21 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, anyhow::Error
|
||||
.unwrap(),
|
||||
contact_chat_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if fp_equals_sender {
|
||||
.await
|
||||
{
|
||||
// the scanned fingerprint matches Alice's key,
|
||||
// we can proceed to step 4b) directly and save two mails
|
||||
info!(context, "Taking protocol shortcut.");
|
||||
context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM;
|
||||
joiner_progress!(
|
||||
context,
|
||||
chat_id_2_contact_id(context, contact_chat_id).await?,
|
||||
chat_id_2_contact_id(context, contact_chat_id).await,
|
||||
400
|
||||
);
|
||||
let own_fingerprint = get_self_fingerprint(context).await;
|
||||
|
||||
// Bob -> Alice
|
||||
send_handshake_msg(
|
||||
if let Err(err) = send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
if join_vg {
|
||||
@@ -259,12 +270,16 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, anyhow::Error
|
||||
"".to_string()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
error!(context, "failed to send handshake message: {}", err);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
} else {
|
||||
context.bob.write().await.expects = DC_VC_AUTH_REQUIRED;
|
||||
|
||||
// Bob -> Alice
|
||||
send_handshake_msg(
|
||||
if let Err(err) = send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
if join_vg { "vg-request" } else { "vc-request" },
|
||||
@@ -272,7 +287,11 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, anyhow::Error
|
||||
None,
|
||||
"",
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
error!(context, "failed to send handshake message: {}", err);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
}
|
||||
|
||||
if join_vg {
|
||||
@@ -280,23 +299,12 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, anyhow::Error
|
||||
while !context.shall_stop_ongoing().await {
|
||||
async_std::task::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
let bob = context.bob.read().await;
|
||||
if bob.status == DC_BOB_SUCCESS {
|
||||
let id = chat::get_chat_id_by_grpid(
|
||||
context,
|
||||
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
|
||||
)
|
||||
.await?
|
||||
.0;
|
||||
Ok(id)
|
||||
} else {
|
||||
bail!("Failed to join");
|
||||
}
|
||||
cleanup(&context, contact_chat_id, true, join_vg).await
|
||||
} else {
|
||||
// for a one-to-one-chat, the chat is already known, return the chat-id,
|
||||
// the verification runs in background
|
||||
context.free_ongoing().await;
|
||||
Ok(contact_chat_id)
|
||||
contact_chat_id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,12 +351,11 @@ async fn send_handshake_msg(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result<u32, Error> {
|
||||
let contacts = chat::get_chat_contacts(context, contact_chat_id).await?;
|
||||
if contacts.len() == 1 {
|
||||
Ok(contacts[0])
|
||||
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
|
||||
contact_id
|
||||
} else {
|
||||
Ok(0)
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,24 +363,20 @@ async fn fingerprint_equals_sender(
|
||||
context: &Context,
|
||||
fingerprint: &Fingerprint,
|
||||
contact_chat_id: ChatId,
|
||||
) -> Result<bool, Error> {
|
||||
let contacts = chat::get_chat_contacts(context, contact_chat_id).await?;
|
||||
|
||||
if contacts.len() == 1 {
|
||||
if let Ok(contact) = Contact::load_from_db(context, contacts[0]).await {
|
||||
if let Ok(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
) -> bool {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
if let Some(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
if peerstate.public_key_fingerprint.is_some()
|
||||
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
|
||||
{
|
||||
return Ok(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum HandshakeError {
|
||||
#[error("Can not be called with special contact ID")]
|
||||
@@ -394,8 +397,6 @@ pub(crate) enum HandshakeError {
|
||||
MsgSendFailed(#[source] Error),
|
||||
#[error("Failed to parse fingerprint")]
|
||||
BadFingerprint(#[from] crate::key::FingerprintError),
|
||||
#[error("{0}")]
|
||||
Other(#[from] Error),
|
||||
}
|
||||
|
||||
/// What to do with a Secure-Join handshake message after it was handled.
|
||||
@@ -422,6 +423,7 @@ pub(crate) enum HandshakeMessage {
|
||||
/// When handle_securejoin_handshake() is called,
|
||||
/// the message is not yet filed in the database;
|
||||
/// this is done by receive_imf() later on as needed.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) async fn handle_securejoin_handshake(
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
@@ -529,20 +531,20 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
"Not encrypted."
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
context.bob.write().await.status = 0; // secure-join failed
|
||||
context.stop_ongoing().await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id)
|
||||
.await?
|
||||
.await
|
||||
{
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
context.bob.write().await.status = 0; // secure-join failed
|
||||
context.stop_ongoing().await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
@@ -585,7 +587,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint not provided.",
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
@@ -595,16 +597,16 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Auth not encrypted.",
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? {
|
||||
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
info!(context, "Fingerprint verified.",);
|
||||
@@ -617,13 +619,13 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Auth not provided.",
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
if !token::exists(context, token::Namespace::Auth, &auth_0).await {
|
||||
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.")
|
||||
.await?;
|
||||
.await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
|
||||
@@ -632,12 +634,12 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await;
|
||||
info!(context, "Auth verified.",);
|
||||
secure_connection_established(context, contact_chat_id).await?;
|
||||
secure_connection_established(context, contact_chat_id).await;
|
||||
emit_event!(context, Event::ContactsChanged(Some(contact_id)));
|
||||
inviter_progress!(context, contact_id, 600);
|
||||
if join_vg {
|
||||
@@ -740,7 +742,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Contact confirm message not encrypted.",
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
context.bob.write().await.status = 0;
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
@@ -754,7 +756,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined).await;
|
||||
@@ -772,7 +774,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
secure_connection_established(context, contact_chat_id).await?;
|
||||
secure_connection_established(context, contact_chat_id).await;
|
||||
context.bob.write().await.expects = 0;
|
||||
|
||||
// Bob -> Alice
|
||||
@@ -897,7 +899,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
contact_chat_id,
|
||||
"Message not encrypted correctly.",
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint)
|
||||
@@ -909,7 +911,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
contact_chat_id,
|
||||
"Fingerprint not provided, please update Delta Chat on all your devices.",
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
@@ -919,7 +921,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
contact_chat_id,
|
||||
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
Ok(if step.as_str() == "vg-member-added" {
|
||||
@@ -932,11 +934,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
}
|
||||
}
|
||||
|
||||
async fn secure_connection_established(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
) -> Result<(), HandshakeError> {
|
||||
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id).await?;
|
||||
async fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
|
||||
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id).await;
|
||||
let contact = Contact::get_by_id(context, contact_id).await;
|
||||
|
||||
let addr = if let Ok(ref contact) = contact {
|
||||
@@ -949,16 +948,14 @@ async fn secure_connection_established(
|
||||
.await;
|
||||
chat::add_info_msg(context, contact_chat_id, msg).await;
|
||||
emit_event!(context, Event::ChatModified(contact_chat_id));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn could_not_establish_secure_connection(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
details: &str,
|
||||
) -> Result<(), HandshakeError> {
|
||||
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
|
||||
) {
|
||||
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await;
|
||||
let contact = Contact::get_by_id(context, contact_id).await;
|
||||
let msg = context
|
||||
.stock_string_repl_str(
|
||||
@@ -973,11 +970,12 @@ async fn could_not_establish_secure_connection(
|
||||
|
||||
chat::add_info_msg(context, contact_chat_id, &msg).await;
|
||||
error!(context, "{} ({})", &msg, details);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> {
|
||||
if let Ok(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await {
|
||||
if let Some(ref mut peerstate) =
|
||||
Peerstate::from_fingerprint(context, &context.sql, fingerprint).await
|
||||
{
|
||||
if peerstate.set_verified(
|
||||
PeerstateKeyType::PublicKey,
|
||||
fingerprint,
|
||||
@@ -1010,42 +1008,47 @@ fn encrypted_and_signed(
|
||||
if !mimeparser.was_encrypted() {
|
||||
warn!(context, "Message not encrypted.",);
|
||||
false
|
||||
} else if mimeparser.signatures.is_empty() {
|
||||
warn!(context, "Message not signed.",);
|
||||
false
|
||||
} else if expected_fingerprint.is_none() {
|
||||
} else if let Some(expected_fingerprint) = expected_fingerprint {
|
||||
if !mimeparser.signatures.contains(expected_fingerprint) {
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.", expected_fingerprint,
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Fingerprint for comparison missing.");
|
||||
false
|
||||
} else if !mimeparser
|
||||
.signatures
|
||||
.contains(expected_fingerprint.unwrap())
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.",
|
||||
expected_fingerprint.unwrap(),
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_degrade_event(context: &Context, peerstate: &Peerstate) -> Result<(), Error> {
|
||||
pub async fn handle_degrade_event(
|
||||
context: &Context,
|
||||
peerstate: &Peerstate<'_>,
|
||||
) -> Result<(), Error> {
|
||||
// - we do not issue an warning for DC_DE_ENCRYPTION_PAUSED as this is quite normal
|
||||
// - currently, we do not issue an extra warning for DC_DE_VERIFICATION_LOST - this always comes
|
||||
// together with DC_DE_FINGERPRINT_CHANGED which is logged, the idea is not to bother
|
||||
// with things they cannot fix, so the user is just kicked from the verified group
|
||||
// (and he will know this and can fix this)
|
||||
if Some(DegradeEvent::FingerprintChanged) == peerstate.degrade_event {
|
||||
let contact_id: i32 = context
|
||||
let contact_id: i32 = match context
|
||||
.sql
|
||||
.query_value(
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT id FROM contacts WHERE addr=?;",
|
||||
paramsx![&peerstate.addr],
|
||||
paramsv![peerstate.addr],
|
||||
)
|
||||
.await?;
|
||||
|
||||
.await
|
||||
{
|
||||
None => bail!(
|
||||
"contact with peerstate.addr {:?} not found",
|
||||
&peerstate.addr
|
||||
),
|
||||
Some(contact_id) => contact_id,
|
||||
};
|
||||
if contact_id > 0 {
|
||||
let (contact_chat_id, _) =
|
||||
chat::create_or_lookup_by_contact_id(context, contact_id as u32, Blocked::Deaddrop)
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
// but for non-delta-compatibility, that seems to be better.
|
||||
// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
|
||||
pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
if text.starts_with("--") {
|
||||
"-\u{200B}-".to_string() + &text[2..].replace("\n--", "\n-\u{200B}-")
|
||||
if let Some(text) = text.strip_prefix("--") {
|
||||
"-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-")
|
||||
} else {
|
||||
text.replace("\n--", "\n-\u{200B}-")
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove standard (RFC 3676, §4.3) footer if it is found.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
|
||||
let mut nearly_standard_footer = None;
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
@@ -41,6 +42,7 @@ fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
|
||||
|
||||
/// Remove nonstandard footer and a boolean indicating whether such
|
||||
/// footer was removed.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
if line == "--"
|
||||
@@ -107,6 +109,7 @@ fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
let mut last_quoted_line = None;
|
||||
for (l, line) in lines.iter().enumerate().rev() {
|
||||
@@ -132,6 +135,7 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
let mut last_quoted_line = None;
|
||||
let mut has_quoted_headline = false;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
//! # SMTP transport module
|
||||
|
||||
#![forbid(clippy::indexing_slicing)]
|
||||
|
||||
pub mod send;
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use async_smtp::smtp::client::net::*;
|
||||
use async_smtp::*;
|
||||
@@ -57,7 +55,7 @@ pub(crate) struct Smtp {
|
||||
/// Timestamp of last successful send/receive network interaction
|
||||
/// (eg connect or send succeeded). On initialization and disconnect
|
||||
/// it is set to None.
|
||||
last_success: Option<Instant>,
|
||||
last_success: Option<SystemTime>,
|
||||
}
|
||||
|
||||
impl Smtp {
|
||||
@@ -78,7 +76,11 @@ impl Smtp {
|
||||
/// have been successfully used the last 60 seconds
|
||||
pub async fn has_maybe_stale_connection(&self) -> bool {
|
||||
if let Some(last_success) = self.last_success {
|
||||
Instant::now().duration_since(last_success).as_secs() > 60
|
||||
SystemTime::now()
|
||||
.duration_since(last_success)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
> 60
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -181,7 +183,7 @@ impl Smtp {
|
||||
.stock_string_repl_str2(
|
||||
StockMessage::ServerResponse,
|
||||
format!("SMTP {}:{}", domain, port),
|
||||
format!("{}, ({:?})", err.to_string(), err),
|
||||
err.to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -190,7 +192,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
self.transport = Some(trans);
|
||||
self.last_success = Some(Instant::now());
|
||||
self.last_success = Some(SystemTime::now());
|
||||
|
||||
context.emit_event(Event::SmtpConnected(format!(
|
||||
"SMTP-LOGIN as {} ok",
|
||||
|
||||
@@ -5,6 +5,7 @@ use async_smtp::*;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
use std::time::Duration;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -30,7 +31,7 @@ impl Smtp {
|
||||
message: Vec<u8>,
|
||||
job_id: u32,
|
||||
) -> Result<()> {
|
||||
let message_len = message.len();
|
||||
let message_len_bytes = message.len();
|
||||
|
||||
let recipients_display = recipients
|
||||
.iter()
|
||||
@@ -47,13 +48,18 @@ impl Smtp {
|
||||
);
|
||||
|
||||
if let Some(ref mut transport) = self.transport {
|
||||
transport.send(mail).await.map_err(Error::SendError)?;
|
||||
// The timeout is 1min + 3min per MB.
|
||||
let timeout = 60 + (180 * message_len_bytes / 1_000_000) as u64;
|
||||
transport
|
||||
.send_with_timeout(mail, Some(&Duration::from_secs(timeout)))
|
||||
.await
|
||||
.map_err(Error::SendError)?;
|
||||
|
||||
context.emit_event(Event::SmtpMessageSent(format!(
|
||||
"Message len={} was smtp-sent to {}",
|
||||
message_len, recipients_display
|
||||
message_len_bytes, recipients_display
|
||||
)));
|
||||
self.last_success = Some(std::time::Instant::now());
|
||||
self.last_success = Some(std::time::SystemTime::now());
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
|
||||
1367
src/sql.rs
Normal file
1367
src/sql.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
#[macro_export]
|
||||
macro_rules! paramsx {
|
||||
() => {
|
||||
sqlx::sqlite::SqliteArguments::default()
|
||||
};
|
||||
($($param:expr),+ $(,)?) => {{
|
||||
use sqlx::Arguments;
|
||||
|
||||
let mut args = sqlx::sqlite::SqliteArguments::default();
|
||||
$(args.add($param);)+
|
||||
args
|
||||
}};
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
use super::{Error, Result, Sql};
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
|
||||
/// Executes all migrations required to get from the passed in `dbversion` to the latest.
|
||||
pub async fn run(
|
||||
context: &Context,
|
||||
sql: &Sql,
|
||||
dbversion: i32,
|
||||
exists_before_update: bool,
|
||||
) -> Result<()> {
|
||||
let migrate = |version: i32, stmt: &'static str| async move {
|
||||
if dbversion < version {
|
||||
info!(context, "[migration] v{}", version);
|
||||
|
||||
sql.execute_batch(stmt).await?;
|
||||
sql.set_raw_config_int(context, "dbversion", version)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(())
|
||||
};
|
||||
|
||||
migrate(
|
||||
0,
|
||||
r#"
|
||||
CREATE TABLE config (id INTEGER PRIMARY KEY, keyname TEXT, value TEXT);
|
||||
CREATE INDEX config_index1 ON config (keyname);
|
||||
CREATE TABLE contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT DEFAULT '',
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
origin INTEGER DEFAULT 0,
|
||||
blocked INTEGER DEFAULT 0,
|
||||
last_seen INTEGER DEFAULT 0,
|
||||
param TEXT DEFAULT '');
|
||||
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
|
||||
CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE);
|
||||
INSERT INTO contacts (id,name,origin) VALUES
|
||||
(1,'self',262144), (2,'info',262144), (3,'rsvd',262144),
|
||||
(4,'rsvd',262144), (5,'device',262144), (6,'rsvd',262144),
|
||||
(7,'rsvd',262144), (8,'rsvd',262144), (9,'rsvd',262144);
|
||||
CREATE TABLE chats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type INTEGER DEFAULT 0,
|
||||
name TEXT DEFAULT '',
|
||||
draft_timestamp INTEGER DEFAULT 0,
|
||||
draft_txt TEXT DEFAULT '',
|
||||
blocked INTEGER DEFAULT 0,
|
||||
grpid TEXT DEFAULT '',
|
||||
param TEXT DEFAULT '');
|
||||
CREATE INDEX chats_index1 ON chats (grpid);
|
||||
CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER);
|
||||
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);
|
||||
INSERT INTO chats (id,type,name) VALUES
|
||||
(1,120,'deaddrop'), (2,120,'rsvd'), (3,120,'trash'),
|
||||
(4,120,'msgs_in_creation'), (5,120,'starred'), (6,120,'archivedlink'),
|
||||
(7,100,'rsvd'), (8,100,'rsvd'), (9,100,'rsvd');
|
||||
CREATE TABLE msgs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc724_mid TEXT DEFAULT '',
|
||||
server_folder TEXT DEFAULT '',
|
||||
server_uid INTEGER DEFAULT 0,
|
||||
chat_id INTEGER DEFAULT 0,
|
||||
from_id INTEGER DEFAULT 0,
|
||||
to_id INTEGER DEFAULT 0,
|
||||
timestamp INTEGER DEFAULT 0,
|
||||
type INTEGER DEFAULT 0,
|
||||
state INTEGER DEFAULT 0,
|
||||
msgrmsg INTEGER DEFAULT 1,
|
||||
bytes INTEGER DEFAULT 0,
|
||||
txt TEXT DEFAULT '',
|
||||
txt_raw TEXT DEFAULT '',
|
||||
param TEXT DEFAULT '');
|
||||
CREATE INDEX msgs_index1 ON msgs (rfc724_mid);
|
||||
CREATE INDEX msgs_index2 ON msgs (chat_id);
|
||||
CREATE INDEX msgs_index3 ON msgs (timestamp);
|
||||
CREATE INDEX msgs_index4 ON msgs (state);
|
||||
INSERT INTO msgs (id,msgrmsg,txt) VALUES
|
||||
(1,0,'marker1'), (2,0,'rsvd'), (3,0,'rsvd'),
|
||||
(4,0,'rsvd'), (5,0,'rsvd'), (6,0,'rsvd'), (7,0,'rsvd'),
|
||||
(8,0,'rsvd'), (9,0,'daymarker');
|
||||
CREATE TABLE jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
added_timestamp INTEGER,
|
||||
desired_timestamp INTEGER DEFAULT 0,
|
||||
action INTEGER,
|
||||
foreign_id INTEGER,
|
||||
param TEXT DEFAULT '');
|
||||
CREATE INDEX jobs_index1 ON jobs (desired_timestamp);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
1,
|
||||
r#"
|
||||
CREATE TABLE leftgrps (
|
||||
id INTEGER PRIMARY KEY,
|
||||
grpid TEXT DEFAULT '');
|
||||
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
2,
|
||||
r#"
|
||||
ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
7,
|
||||
r#"
|
||||
CREATE TABLE keypairs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
is_default INTEGER DEFAULT 0,
|
||||
private_key,
|
||||
public_key,
|
||||
created INTEGER DEFAULT 0);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
10,
|
||||
r#"
|
||||
CREATE TABLE acpeerstates (
|
||||
id INTEGER PRIMARY KEY,
|
||||
addr TEXT DEFAULT '' COLLATE NOCASE,
|
||||
last_seen INTEGER DEFAULT 0,
|
||||
last_seen_autocrypt INTEGER DEFAULT 0,
|
||||
public_key,
|
||||
prefer_encrypted INTEGER DEFAULT 0);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
12,
|
||||
r#"
|
||||
CREATE TABLE msgs_mdns (
|
||||
msg_id INTEGER,
|
||||
contact_id INTEGER);
|
||||
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
17,
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;
|
||||
CREATE INDEX chats_index2 ON chats (archived);
|
||||
ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;
|
||||
CREATE INDEX msgs_index5 ON msgs (starred);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
18,
|
||||
r#"
|
||||
ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;
|
||||
ALTER TABLE acpeerstates ADD COLUMN gossip_key;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// chat.id=1 and chat.id=2 are the old deaddrops,
|
||||
// the current ones are defined by chats.blocked=2
|
||||
migrate(
|
||||
27,
|
||||
r#"
|
||||
DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;
|
||||
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
|
||||
ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
|
||||
ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
34,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0;
|
||||
ALTER TABLE msgs_mdns ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
|
||||
ALTER TABLE acpeerstates ADD COLUMN public_key_fingerprint TEXT DEFAULT '';
|
||||
ALTER TABLE acpeerstates ADD COLUMN gossip_key_fingerprint TEXT DEFAULT '';
|
||||
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
|
||||
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
39,
|
||||
r#"
|
||||
CREATE TABLE tokens (
|
||||
id INTEGER PRIMARY KEY,
|
||||
namespc INTEGER DEFAULT 0,
|
||||
foreign_id INTEGER DEFAULT 0,
|
||||
token TEXT DEFAULT '',
|
||||
timestamp INTEGER DEFAULT 0);
|
||||
ALTER TABLE acpeerstates ADD COLUMN verified_key;
|
||||
ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';
|
||||
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
40,
|
||||
r#"
|
||||
ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
44,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN mime_headers TEXT;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
46,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;
|
||||
ALTER TABLE msgs ADD COLUMN mime_references TEXT;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
47,
|
||||
r#"
|
||||
ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// NOTE: move_state is not used anymore
|
||||
migrate(
|
||||
48,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
49,
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if dbversion < 50 {
|
||||
info!(context, "[migration] v50");
|
||||
// installations <= 0.100.1 used DC_SHOW_EMAILS_ALL implicitly;
|
||||
// keep this default and use DC_SHOW_EMAILS_NO
|
||||
// only for new installations
|
||||
if exists_before_update {
|
||||
sql.set_raw_config_int(context, "show_emails", ShowEmails::All as i32)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sql.set_raw_config_int(context, "dbversion", 50).await?;
|
||||
}
|
||||
|
||||
// the messages containing _only_ locations
|
||||
// are also added to the database as _hidden_.
|
||||
migrate(
|
||||
53,
|
||||
r#"
|
||||
CREATE TABLE locations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
latitude REAL DEFAULT 0.0,
|
||||
longitude REAL DEFAULT 0.0,
|
||||
accuracy REAL DEFAULT 0.0,
|
||||
timestamp INTEGER DEFAULT 0,
|
||||
chat_id INTEGER DEFAULT 0,
|
||||
from_id INTEGER DEFAULT 0);
|
||||
CREATE INDEX locations_index1 ON locations (from_id);
|
||||
CREATE INDEX locations_index2 ON locations (timestamp);
|
||||
ALTER TABLE chats ADD COLUMN locations_send_begin INTEGER DEFAULT 0;
|
||||
ALTER TABLE chats ADD COLUMN locations_send_until INTEGER DEFAULT 0;
|
||||
ALTER TABLE chats ADD COLUMN locations_last_sent INTEGER DEFAULT 0;
|
||||
CREATE INDEX chats_index3 ON chats (locations_send_until);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
54,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;
|
||||
CREATE INDEX msgs_index6 ON msgs (location_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
55,
|
||||
r#"
|
||||
ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
59,
|
||||
r#"
|
||||
CREATE TABLE devmsglabels (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label TEXT,
|
||||
msg_id INTEGER DEFAULT 0);
|
||||
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// records in the devmsglabels are kept when the message is deleted.
|
||||
// so, msg_id may or may not exist.
|
||||
if dbversion < 59 && exists_before_update && sql.get_raw_config_int("bcc_self").await.is_none()
|
||||
{
|
||||
sql.set_raw_config_int(context, "bcc_self", 1).await?;
|
||||
}
|
||||
|
||||
migrate(
|
||||
60,
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
61,
|
||||
r#"
|
||||
ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
62,
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
63,
|
||||
r#"
|
||||
UPDATE chats SET grpid='' WHERE type=100;
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
migrate(
|
||||
64,
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
706
src/sql/mod.rs
706
src/sql/mod.rs
@@ -1,706 +0,0 @@
|
||||
//! # SQLite wrapper
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::RwLock;
|
||||
use sqlx::sqlite::*;
|
||||
|
||||
use crate::chat::{update_device_icon, update_saved_messages_icon};
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod migrations;
|
||||
|
||||
pub use macros::*;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Sqlite: Connection closed")]
|
||||
SqlNoConnection,
|
||||
#[error("Sqlite: Already open")]
|
||||
SqlAlreadyOpen,
|
||||
#[error("Sqlite: Failed to open")]
|
||||
SqlFailedToOpen,
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("{0:?}")]
|
||||
BlobError(#[from] crate::blob::BlobError),
|
||||
#[error("{0}")]
|
||||
Other(#[from] crate::error::Error),
|
||||
#[error("{0}")]
|
||||
Sqlx(#[from] sqlx::Error),
|
||||
#[error("{0}: {1}")]
|
||||
SqlxWithContext(String, #[source] sqlx::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A wrapper around the underlying Sqlite3 object.
|
||||
#[derive(Debug)]
|
||||
pub struct Sql {
|
||||
xpool: RwLock<Option<sqlx::SqlitePool>>,
|
||||
}
|
||||
|
||||
impl Default for Sql {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
xpool: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sql {
|
||||
pub fn new() -> Sql {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Returns `true` if there is a working sqlite connection, `false` otherwise.
|
||||
pub async fn is_open(&self) -> bool {
|
||||
let pool = self.xpool.read().await;
|
||||
pool.is_some() && !pool.as_ref().unwrap().is_closed()
|
||||
}
|
||||
|
||||
/// Shuts down all sqlite connections.
|
||||
pub async fn close(&self) {
|
||||
if let Some(pool) = self.xpool.write().await.take() {
|
||||
pool.close().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn open<T: AsRef<Path>>(
|
||||
&self,
|
||||
context: &Context,
|
||||
dbfile: T,
|
||||
readonly: bool,
|
||||
) -> crate::error::Result<()> {
|
||||
if let Err(err) = open(context, self, dbfile, readonly).await {
|
||||
return match err.downcast_ref::<Error>() {
|
||||
Some(Error::SqlAlreadyOpen) => Err(err),
|
||||
_ => {
|
||||
self.close().await;
|
||||
Err(err)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a single query.
|
||||
pub async fn execute<'a, P>(&self, statement: &'a str, params: P) -> Result<usize>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
|
||||
let count = sqlx::query_with(statement, params).execute(xpool).await?;
|
||||
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
/// Execute a list of statements, without any bindings
|
||||
pub async fn execute_batch(&self, statement: &str) -> Result<()> {
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
|
||||
sqlx::query(statement.as_ref()).execute(xpool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_pool(&self) -> Result<SqlitePool> {
|
||||
let lock = self.xpool.read().await;
|
||||
lock.as_ref().cloned().ok_or_else(|| Error::SqlNoConnection)
|
||||
}
|
||||
|
||||
/// Starts a new transaction.
|
||||
pub async fn begin(
|
||||
&self,
|
||||
) -> Result<sqlx::Transaction<'static, Sqlite, sqlx::pool::PoolConnection<Sqlite>>> {
|
||||
let lock = self.xpool.read().await;
|
||||
let pool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
|
||||
let tx = pool.begin().await?;
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
/// Execute a query which is expected to return zero or more rows.
|
||||
pub async fn query_rows<'a, T, P>(&self, statement: &'a str, params: P) -> Result<Vec<T>>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::FromRow<'b, SqliteRow> + Unpin + Send,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
let rows = sqlx::query_with(statement.as_ref(), params)
|
||||
.try_map(|row: SqliteRow| sqlx::FromRow::from_row(&row))
|
||||
.fetch_all(xpool)
|
||||
.await?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Execute a query which is expected to return zero or more rows.
|
||||
pub async fn query_values<'a, T, P>(&self, statement: &'a str, params: P) -> Result<Vec<T>>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::decode::Decode<'b, Sqlite>,
|
||||
T: sqlx::Type<Sqlite>,
|
||||
T: 'static + Unpin + Send,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
let rows = sqlx::query_with(statement.as_ref(), params)
|
||||
.try_map(|row: SqliteRow| {
|
||||
let (val,): (T,) = sqlx::FromRow::from_row(&row)?;
|
||||
Ok(val)
|
||||
})
|
||||
.fetch_all(xpool)
|
||||
.await?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Return `true` if a query in the SQL statement it executes returns one or more
|
||||
/// rows and false if the SQL returns an empty set.
|
||||
pub async fn exists<'a, P>(&self, statement: &'a str, params: P) -> Result<bool>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
|
||||
let mut rows = sqlx::query_with(statement, params).fetch(xpool);
|
||||
|
||||
match rows.next().await {
|
||||
Some(Ok(_)) => Ok(true),
|
||||
None => Ok(false),
|
||||
Some(Err(sqlx::Error::RowNotFound)) => Ok(false),
|
||||
Some(Err(err)) => Err(Error::SqlxWithContext(
|
||||
format!("exists: '{}'", statement),
|
||||
err,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a query which is expected to return one row.
|
||||
pub async fn query_row<'a, T, P>(&self, statement: &'a str, params: P) -> Result<T>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::FromRow<'b, SqliteRow> + Unpin + Send,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
let row = sqlx::query_with(statement.as_ref(), params)
|
||||
.try_map(|row: SqliteRow| sqlx::FromRow::from_row(&row))
|
||||
.fetch_one(xpool)
|
||||
.await?;
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
/// Execute a query which is expected to return zero or one row.
|
||||
pub async fn query_row_optional<'a, T, P>(
|
||||
&self,
|
||||
statement: &'a str,
|
||||
params: P,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::FromRow<'b, SqliteRow> + Unpin + Send,
|
||||
{
|
||||
let lock = self.xpool.read().await;
|
||||
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
|
||||
let row = sqlx::query_with(statement.as_ref(), params)
|
||||
.try_map(|row: SqliteRow| sqlx::FromRow::from_row(&row))
|
||||
.fetch_optional(xpool)
|
||||
.await?;
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn query_value_optional<'a, T, P>(
|
||||
&self,
|
||||
statement: &'a str,
|
||||
params: P,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::decode::Decode<'b, Sqlite>,
|
||||
T: sqlx::Type<Sqlite>,
|
||||
T: 'static + Unpin + Send,
|
||||
{
|
||||
match self.query_row_optional(statement, params).await? {
|
||||
Some((val,)) => Ok(Some(val)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_value<'a, T, P>(&self, statement: &'a str, params: P) -> Result<T>
|
||||
where
|
||||
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
|
||||
T: for<'b> sqlx::decode::Decode<'b, Sqlite>,
|
||||
T: sqlx::Type<Sqlite>,
|
||||
T: 'static + Unpin + Send,
|
||||
{
|
||||
let (val,): (T,) = self.query_row(statement, params).await?;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
pub async fn table_exists(&self, name: impl AsRef<str>) -> Result<bool> {
|
||||
self.exists(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'table' AND name=?",
|
||||
paramsx![name.as_ref()],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Set private configuration options.
|
||||
///
|
||||
/// Setting `None` deletes the value. On failure an error message
|
||||
/// will already have been logged.
|
||||
pub async fn set_raw_config(
|
||||
&self,
|
||||
_context: &Context,
|
||||
key: impl AsRef<str>,
|
||||
value: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let key = key.as_ref();
|
||||
|
||||
if let Some(ref value) = value {
|
||||
let exists = self
|
||||
.exists("SELECT value FROM config WHERE keyname=?;", paramsx![key])
|
||||
.await?;
|
||||
if exists {
|
||||
self.execute(
|
||||
"UPDATE config SET value=? WHERE keyname=?;",
|
||||
paramsx![value, key],
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
self.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||
paramsx![key, value],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
self.execute("DELETE FROM config WHERE keyname=?;", paramsx![key])
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get configuration options from the database.
|
||||
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Option<String> {
|
||||
if !self.is_open().await || key.as_ref().is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.query_row(
|
||||
"SELECT value FROM config WHERE keyname=?;",
|
||||
paramsx![key.as_ref().to_string()],
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.map(|(res,)| res)
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_int(
|
||||
&self,
|
||||
context: &Context,
|
||||
key: impl AsRef<str>,
|
||||
value: i32,
|
||||
) -> Result<()> {
|
||||
self.set_raw_config(context, key, Some(&format!("{}", value)))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_int(&self, key: impl AsRef<str>) -> Option<i32> {
|
||||
self.get_raw_config(key).await.and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_bool(&self, key: impl AsRef<str>) -> bool {
|
||||
// Not the most obvious way to encode bool as string, but it is matter
|
||||
// of backward compatibility.
|
||||
let res = self.get_raw_config_int(key).await;
|
||||
res.unwrap_or_default() > 0
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_bool<T>(&self, context: &Context, key: T, value: bool) -> Result<()>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
let value = if value { Some("1") } else { None };
|
||||
self.set_raw_config(context, key, value).await
|
||||
}
|
||||
|
||||
pub async fn set_raw_config_int64(
|
||||
&self,
|
||||
context: &Context,
|
||||
key: impl AsRef<str>,
|
||||
value: i64,
|
||||
) -> Result<()> {
|
||||
self.set_raw_config(context, key, Some(&format!("{}", value)))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_raw_config_int64(&self, key: impl AsRef<str>) -> Option<i64> {
|
||||
self.get_raw_config(key).await.and_then(|r| r.parse().ok())
|
||||
}
|
||||
|
||||
/// Alternative to sqlite3_last_insert_rowid() which MUST NOT be used due to race conditions, see comment above.
|
||||
/// the ORDER BY ensures, this function always returns the most recent id,
|
||||
/// eg. if a Message-ID is split into different messages.
|
||||
pub async fn get_rowid(
|
||||
&self,
|
||||
table: impl AsRef<str>,
|
||||
field: impl AsRef<str>,
|
||||
value: impl AsRef<str>,
|
||||
) -> Result<u32> {
|
||||
// alternative to sqlite3_last_insert_rowid() which MUST NOT be used due to race conditions, see comment above.
|
||||
// the ORDER BY ensures, this function always returns the most recent id,
|
||||
// eg. if a Message-ID is split into different messages.
|
||||
let query = format!(
|
||||
"SELECT id FROM {} WHERE {}=? ORDER BY id DESC",
|
||||
table.as_ref(),
|
||||
field.as_ref(),
|
||||
);
|
||||
|
||||
let res: i32 = self.query_value(&query, paramsx![value.as_ref()]).await?;
|
||||
|
||||
Ok(res as u32)
|
||||
}
|
||||
|
||||
pub async fn get_rowid2(
|
||||
&self,
|
||||
table: impl AsRef<str>,
|
||||
field: impl AsRef<str>,
|
||||
value: i64,
|
||||
field2: impl AsRef<str>,
|
||||
value2: i32,
|
||||
) -> Result<u32> {
|
||||
let query = format!(
|
||||
"SELECT id FROM {} WHERE {}=? AND {}=? ORDER BY id DESC",
|
||||
table.as_ref(),
|
||||
field.as_ref(),
|
||||
field2.as_ref(),
|
||||
);
|
||||
|
||||
let res: i32 = self
|
||||
.query_value(query.as_ref(), paramsx![value, value2])
|
||||
.await?;
|
||||
|
||||
Ok(res as u32)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
let mut files_in_use = HashSet::new();
|
||||
let mut unreferenced_count = 0usize;
|
||||
|
||||
info!(context, "Start housekeeping...");
|
||||
maybe_add_from_param(
|
||||
context,
|
||||
&mut files_in_use,
|
||||
"SELECT param FROM msgs WHERE chat_id!=3 AND type!=10;",
|
||||
Param::File,
|
||||
)
|
||||
.await?;
|
||||
maybe_add_from_param(
|
||||
context,
|
||||
&mut files_in_use,
|
||||
"SELECT param FROM jobs;",
|
||||
Param::File,
|
||||
)
|
||||
.await?;
|
||||
maybe_add_from_param(
|
||||
context,
|
||||
&mut files_in_use,
|
||||
"SELECT param FROM chats;",
|
||||
Param::ProfileImage,
|
||||
)
|
||||
.await?;
|
||||
maybe_add_from_param(
|
||||
context,
|
||||
&mut files_in_use,
|
||||
"SELECT param FROM contacts;",
|
||||
Param::ProfileImage,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as("SELECT value FROM config;").fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (row,): (String,) = row?;
|
||||
maybe_add_file(&mut files_in_use, row);
|
||||
}
|
||||
|
||||
info!(context, "{} files in use.", files_in_use.len(),);
|
||||
/* go through directory and delete unused files */
|
||||
let p = context.get_blobdir();
|
||||
match async_std::fs::read_dir(p).await {
|
||||
Ok(mut dir_handle) => {
|
||||
/* avoid deletion of files that are just created to build a message object */
|
||||
let diff = std::time::Duration::from_secs(60 * 60);
|
||||
let keep_files_newer_than = std::time::SystemTime::now().checked_sub(diff).unwrap();
|
||||
|
||||
while let Some(entry) = dir_handle.next().await {
|
||||
if entry.is_err() {
|
||||
break;
|
||||
}
|
||||
let entry = entry.unwrap();
|
||||
let name_f = entry.file_name();
|
||||
let name_s = name_f.to_string_lossy();
|
||||
|
||||
if is_file_in_use(&files_in_use, None, &name_s)
|
||||
|| is_file_in_use(&files_in_use, Some(".increation"), &name_s)
|
||||
|| is_file_in_use(&files_in_use, Some(".waveform"), &name_s)
|
||||
|| is_file_in_use(&files_in_use, Some("-preview.jpg"), &name_s)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
unreferenced_count += 1;
|
||||
|
||||
if let Ok(stats) = async_std::fs::metadata(entry.path()).await {
|
||||
let recently_created =
|
||||
stats.created().is_ok() && stats.created().unwrap() > keep_files_newer_than;
|
||||
let recently_modified = stats.modified().is_ok()
|
||||
&& stats.modified().unwrap() > keep_files_newer_than;
|
||||
let recently_accessed = stats.accessed().is_ok()
|
||||
&& stats.accessed().unwrap() > keep_files_newer_than;
|
||||
|
||||
if recently_created || recently_modified || recently_accessed {
|
||||
info!(
|
||||
context,
|
||||
"Housekeeping: Keeping new unreferenced file #{}: {:?}",
|
||||
unreferenced_count,
|
||||
entry.file_name(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Housekeeping: Deleting unreferenced file #{}: {:?}",
|
||||
unreferenced_count,
|
||||
entry.file_name()
|
||||
);
|
||||
let path = entry.path();
|
||||
dc_delete_file(context, path).await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Housekeeping: Cannot open {}. ({})",
|
||||
context.get_blobdir().display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = prune_tombstones(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Houskeeping: Cannot prune message tombstones: {}", err
|
||||
);
|
||||
}
|
||||
|
||||
info!(context, "Housekeeping done.",);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, name: &str) -> bool {
|
||||
let name_to_check = if let Some(namespc) = namespc_opt {
|
||||
let name_len = name.len();
|
||||
let namespc_len = namespc.len();
|
||||
if name_len <= namespc_len || !name.ends_with(namespc) {
|
||||
return false;
|
||||
}
|
||||
&name[..name_len - namespc_len]
|
||||
} else {
|
||||
name
|
||||
};
|
||||
files_in_use.contains(name_to_check)
|
||||
}
|
||||
|
||||
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: impl AsRef<str>) {
|
||||
if !file.as_ref().starts_with("$BLOBDIR/") {
|
||||
return;
|
||||
}
|
||||
|
||||
files_in_use.insert(file.as_ref()[9..].into());
|
||||
}
|
||||
|
||||
async fn maybe_add_from_param(
|
||||
context: &Context,
|
||||
files_in_use: &mut HashSet<String>,
|
||||
query: &str,
|
||||
param_id: Param,
|
||||
) -> Result<()> {
|
||||
info!(context, "maybe_add_from_param: {}", query);
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as(query).fetch(&pool);
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let (row,): (String,) = row?;
|
||||
info!(context, "param: {}", &row);
|
||||
let param: Params = row.parse().unwrap_or_default();
|
||||
|
||||
if let Some(file) = param.get(param_id) {
|
||||
info!(context, "got file: {:?} {}", param_id, file);
|
||||
maybe_add_file(files_in_use, file);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn open(
|
||||
context: &Context,
|
||||
sql: &Sql,
|
||||
dbfile: impl AsRef<Path>,
|
||||
readonly: bool,
|
||||
) -> crate::error::Result<()> {
|
||||
if sql.is_open().await {
|
||||
error!(
|
||||
context,
|
||||
"Cannot open, database \"{:?}\" already opened.",
|
||||
dbfile.as_ref(),
|
||||
);
|
||||
return Err(Error::SqlAlreadyOpen.into());
|
||||
}
|
||||
|
||||
if readonly {
|
||||
// TODO: readonly mode
|
||||
}
|
||||
|
||||
let xpool = sqlx::SqlitePool::builder()
|
||||
.min_size(1)
|
||||
.max_size(4)
|
||||
.build(&format!("sqlite://{}", dbfile.as_ref().to_string_lossy()))
|
||||
.await?;
|
||||
|
||||
{
|
||||
*sql.xpool.write().await = Some(xpool)
|
||||
}
|
||||
|
||||
if !readonly {
|
||||
let mut exists_before_update = false;
|
||||
let mut dbversion_before_update: i32 = -1;
|
||||
|
||||
if sql.table_exists("config").await? {
|
||||
exists_before_update = true;
|
||||
if let Some(version) = sql.get_raw_config_int("dbversion").await {
|
||||
dbversion_before_update = version;
|
||||
}
|
||||
}
|
||||
|
||||
// (1) update low-level database structure.
|
||||
// this should be done before updates that use high-level objects that
|
||||
// rely themselves on the low-level structure.
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
migrations::run(context, &sql, dbversion_before_update, exists_before_update).await?;
|
||||
|
||||
// general updates
|
||||
// (2) updates that require high-level objects
|
||||
// (the structure is complete now and all objects are usable)
|
||||
// --------------------------------------------------------------------
|
||||
let mut recalc_fingerprints = false;
|
||||
let mut update_icons = false;
|
||||
|
||||
if dbversion_before_update < 34 {
|
||||
recalc_fingerprints = true;
|
||||
}
|
||||
|
||||
if dbversion_before_update < 61 {
|
||||
update_icons = true;
|
||||
}
|
||||
|
||||
if recalc_fingerprints {
|
||||
info!(context, "[migration] recalc fingerprints");
|
||||
let pool = context.sql.get_pool().await?;
|
||||
let mut rows = sqlx::query_as("SELECT addr FROM acpeerstates;").fetch(&pool);
|
||||
|
||||
while let Some(addr) = rows.next().await {
|
||||
let (addr,): (String,) = addr?;
|
||||
|
||||
if let Ok(ref mut peerstate) = Peerstate::from_addr(context, &addr).await {
|
||||
peerstate.recalc_fingerprint();
|
||||
peerstate.save_to_db(sql, false).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if update_icons {
|
||||
update_saved_messages_icon(context).await?;
|
||||
update_device_icon(context).await?;
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Opened {:?}.", dbfile.as_ref(),);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes from the database locally deleted messages that also don't
|
||||
/// have a server UID.
|
||||
async fn prune_tombstones(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
r#"
|
||||
DELETE FROM msgs
|
||||
WHERE (chat_id = ? OR hidden)
|
||||
AND server_uid = 0
|
||||
"#,
|
||||
paramsx![DC_CHAT_ID_TRASH as i32],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_maybe_add_file() {
|
||||
let mut files = Default::default();
|
||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||
maybe_add_file(&mut files, "world2.txt");
|
||||
maybe_add_file(&mut files, "$BLOBDIR");
|
||||
|
||||
assert!(files.contains("hello"));
|
||||
assert!(files.contains("world.txt"));
|
||||
assert!(!files.contains("world2.txt"));
|
||||
assert!(!files.contains("$BLOBDIR"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_file_in_use() {
|
||||
let mut files = Default::default();
|
||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||
maybe_add_file(&mut files, "world2.txt");
|
||||
|
||||
assert!(is_file_in_use(&files, None, "hello"));
|
||||
assert!(!is_file_in_use(&files, Some(".txt"), "hello"));
|
||||
assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix"));
|
||||
}
|
||||
}
|
||||
41
src/stock.rs
41
src/stock.rs
@@ -130,7 +130,9 @@ pub enum StockMessage {
|
||||
))]
|
||||
AcSetupMsgBody = 43,
|
||||
|
||||
#[strum(props(fallback = "Cannot login as %1$s."))]
|
||||
#[strum(props(
|
||||
fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
|
||||
))]
|
||||
CannotLogin = 60,
|
||||
|
||||
#[strum(props(fallback = "Could not connect to %1$s: %2$s"))]
|
||||
@@ -177,7 +179,7 @@ pub enum StockMessage {
|
||||
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
|
||||
WelcomeMessage = 71,
|
||||
|
||||
#[strum(props(fallback = "Unknown Sender for this chat. See 'info' for more details."))]
|
||||
#[strum(props(fallback = "Unknown sender for this chat. See 'info' for more details."))]
|
||||
UnknownSenderForChat = 72,
|
||||
|
||||
#[strum(props(fallback = "Message from %1$s"))]
|
||||
@@ -185,6 +187,35 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "Failed to send message to %1$s."))]
|
||||
FailedSendingTo = 74,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is disabled."))]
|
||||
MsgEphemeralTimerDisabled = 75,
|
||||
|
||||
// A fallback message for unknown timer values.
|
||||
// "s" stands for "second" SI unit here.
|
||||
#[strum(props(fallback = "Message deletion timer is set to %1$s s."))]
|
||||
MsgEphemeralTimerEnabled = 76,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 minute."))]
|
||||
MsgEphemeralTimerMinute = 77,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 hour."))]
|
||||
MsgEphemeralTimerHour = 78,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 day."))]
|
||||
MsgEphemeralTimerDay = 79,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 week."))]
|
||||
MsgEphemeralTimerWeek = 80,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 4 weeks."))]
|
||||
MsgEphemeralTimerFourWeeks = 81,
|
||||
|
||||
#[strum(props(fallback = "Video chat invitation"))]
|
||||
VideochatInvitation = 82,
|
||||
|
||||
#[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))]
|
||||
VideochatInviteMsgBody = 83,
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -334,10 +365,10 @@ impl Context {
|
||||
let action1 = action.trim_end_matches('.');
|
||||
match from_id {
|
||||
0 => action,
|
||||
1 => {
|
||||
DC_CONTACT_ID_SELF => {
|
||||
self.stock_string_repl_str(StockMessage::MsgActionByMe, action1)
|
||||
.await
|
||||
} // DC_CONTACT_ID_SELF
|
||||
}
|
||||
_ => {
|
||||
let displayname = Contact::get_by_id(self, from_id)
|
||||
.await
|
||||
@@ -360,7 +391,7 @@ impl Context {
|
||||
|
||||
// create saved-messages chat;
|
||||
// we do this only once, if the user has deleted the chat, he can recreate it manually.
|
||||
if !self.sql.get_raw_config_bool("self-chat-added").await {
|
||||
if !self.sql.get_raw_config_bool(&self, "self-chat-added").await {
|
||||
self.sql
|
||||
.set_raw_config_bool(&self, "self-chat-added", true)
|
||||
.await?;
|
||||
|
||||
@@ -27,20 +27,10 @@ impl TestContext {
|
||||
///
|
||||
/// [Context]: crate::context::Context
|
||||
pub async fn new() -> Self {
|
||||
pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let ctx = Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
|
||||
let events = ctx.get_event_emitter();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
log::info!("{:?}", event);
|
||||
}
|
||||
});
|
||||
|
||||
TestContext { ctx, dir }
|
||||
Self { ctx, dir }
|
||||
}
|
||||
|
||||
/// Create a new configured [TestContext].
|
||||
|
||||
12
src/token.rs
12
src/token.rs
@@ -11,7 +11,7 @@ use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
|
||||
/// Token namespace
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||||
#[repr(i32)]
|
||||
pub enum Namespace {
|
||||
Unknown = 0,
|
||||
@@ -34,7 +34,7 @@ pub async fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
|
||||
paramsx![namespace, foreign_id, &token, time()],
|
||||
paramsv![namespace, foreign_id, token, time()],
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
@@ -44,12 +44,12 @@ pub async fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -
|
||||
pub async fn lookup(context: &Context, namespace: Namespace, foreign_id: ChatId) -> Option<String> {
|
||||
context
|
||||
.sql
|
||||
.query_value(
|
||||
.query_get_value::<String>(
|
||||
context,
|
||||
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
|
||||
paramsx![namespace, foreign_id],
|
||||
paramsv![namespace, foreign_id],
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String {
|
||||
@@ -65,7 +65,7 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> boo
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM tokens WHERE namespc=? AND token=?;",
|
||||
paramsx![namespace, token],
|
||||
paramsv![namespace, token],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
|
||||
15
test-data/message/videochat_invitation.eml
Normal file
15
test-data/message/videochat_invitation.eml
Normal file
@@ -0,0 +1,15 @@
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Subject: Message from user
|
||||
Message-ID: <Mr.f1O61111evx.ikocf333353@example.org>
|
||||
Date: Mon, 20 Jul 2020 14:28:30 +0000
|
||||
X-Mailer: Delta Chat Core 1.40.0/CLI
|
||||
Chat-Version: 1.0
|
||||
Chat-Content: videochat-invitation
|
||||
Chat-Webrtc-Room: https://example.org/p2p/?roomname=6HiduoAn4xN
|
||||
To: <tunis3@example.org>
|
||||
From: "=?utf-8?q??=" <tunis4@example.org>
|
||||
|
||||
You are invited to an videochat, click https://example.org/p2p/?roomname=6HiduoAn4xN to join.
|
||||
|
||||
--
|
||||
Sent with my Delta Chat Messenger: https://delta.chat
|
||||
Reference in New Issue
Block a user