Compare commits

..

6 Commits

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

View File

@@ -138,12 +138,6 @@ jobs:
- py-docs
- wheelhouse
remote_tests_rust:
machine: true
steps:
- checkout
- run: ci_scripts/remote_tests_rust.sh
remote_tests_python:
machine: true
steps:
@@ -178,17 +172,14 @@ workflows:
jobs:
# - cargo_fetch
- remote_tests_rust:
filters:
tags:
only: /.*/
- remote_tests_python:
filters:
tags:
only: /.*/
- remote_python_packaging:
requires:
- remote_tests_python
filters:
branches:
only: master

View File

@@ -18,7 +18,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.45.0
toolchain: 1.43.1
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
@@ -32,36 +32,21 @@ jobs:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.45.0
toolchain: 1.43.1
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --examples
build_and_test:
name: Build and test
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
# macOS disabled due to random failures related to caching
#os: [ubuntu-latest, windows-latest, macOS-latest]
os: [ubuntu-latest, windows-latest]
rust: [1.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
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [nightly, 1.43.1]
steps:
- uses: actions/checkout@master
@@ -94,10 +79,11 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests --features repl
args: --workspace --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all
args: --workspace

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

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

View File

@@ -1,173 +1,5 @@
# Changelog
## Unreleased
- breaking change: `dc_update_device_chats()` was removed. This is now done automatically during configure.
- Added a `bot` config. Currently, it only prevents filling the device chats automatically.
## 1.46.0
- breaking change: `dc_configure()` report errors in
`DC_EVENT_CONFIGURE_PROGRESS`: capturing error events is no longer working
#1886 #1905
- breaking change: removed `DC_LP_{IMAP|SMTP}_SOCKET*` from `server_flags`;
added `mail_security` and `send_security` using `DC_SOCKET` enum #1835
- parse multiple servers in Mozilla autoconfig #1860
- try multiple servers for each protocol #1871
- do IMAP and SMTP configuration in parallel #1891
- configuration cleanup and speedup #1858 #1875 #1889 #1904 #1906
- secure-join cleanup, testing, fixing #1876 #1877 #1887 #1888 #1896 #1899 #1900
- do not reset peerstate on encrypted messages,
ignore reordered autocrypt headers #1885 #1890
- always sort message replies after parent message #1852
- add an index to significantly speed up `get_fresh_msg_cnt()` #1881
- improve mimetype guessing for PDF and many other formats #1857 #1861
- improve accepting invalid html #1851
- improve tests, cleanup and ci #1850 #1856 #1859 #1861 #1884 #1894 #1895
- tweak HELO command #1908
- make `dc_accounts_get_all()` return accounts sorted #1909
- fix KML coordinates precision used for location streaming #1872
- fix cancelling import/export #1855
## 1.45.0
- add `dc_accounts_t` account manager object and related api functions #1784
- add capability to import backups as .tar files,
which will become the default in a subsequent release #1749
- try various server domains on configuration #1780 #1838
- recognize .tgs files as stickers #1826
- remove X-Mailer debug header #1819
- improve guessing message types from extension #1818
- fix showing unprotected subjects in encrypted messages #1822
- fix threading in interaction with non-delta-clients #1843
- fix handling if encryption degrades #1829
- fix webrtc-servers names set by the user #1831
- update provider database #1828
- update async-imap to fix Oauth2 #1837
- optimize jpeg assets with trimage #1840
- add tests and documentations #1809 #1820
## 1.44.0
- fix peerstate issues #1800 #1805
- fix a crash related to muted chats #1803
- fix incorrect dimensions sometimes reported for images #1806
- fixed `dc_chat_get_remaining_mute_duration` function #1807
- handle empty tags (e.g. `<br/>`) in HTML mails #1810
- always translate the message about disappearing messages timer change #1813
- improve footer detection in plain text email #1812
- update device chat icon to fix warnings in iOS logs #1802
- fix deletion of multiple messages #1795
## 1.43.0
- improve using own jitsi-servers #1785
- fix smtp-timeout tweaks for larger mails #1797
- more bug fixes and updates #1794 #1792 #1789 #1787
## 1.42.0
- new qr-code type `DC_QR_WEBRTC` #1779
- new `dc_chatlist_get_summary2()` api #1771
- tweak smtp-timeout for larger mails #1782
- optimize read-receipts #1765
- Allow http scheme for DCACCOUNT URLs #1770
- improve tests #1769
- bug fixes #1766 #1772 #1773 #1775 #1776 #1777
## 1.41.0
- new apis to initiate video chats #1718 #1735
- new apis `dc_msg_get_ephemeral_timer()`
and `dc_msg_get_ephemeral_timestamp()`
- new api `dc_chatlist_get_summary2()` #1771
- improve IMAP handling #1703 #1704
- improve ephemeral messages #1696 #1705
- mark location-messages as auto-generated #1715
- multi-device avatar-sync #1716 #1717
- improve python bindings #1732 #1733 #1738 #1769
- Allow http scheme for DCACCOUNT urls #1770
- more fixes #1702 #1706 #1707 #1710 #1719 #1721
#1723 #1734 #1740 #1744 #1748 #1760 #1766 #1773 #1765
- refactorings #1712 #1714 #1757
- update toolchains and dependencies #1726 #1736 #1737 #1742 #1743 #1746
## 1.40.0
- introduce ephemeral messages #1540 #1680 #1683 #1684 #1691 #1692
- `DC_MSG_ID_DAYMARKER` gets timestamp attached #1677 #1685
- improve idle #1690 #1688
- fix message processing issues by sequential processing #1694
- refactorings #1670 #1673
## 1.39.0
- fix handling of `mvbox_watch`, `sentbox_watch`, `inbox_watch` #1654 #1658

1872
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.46.0"
version = "1.39.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -12,7 +12,7 @@ lto = true
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.7.0", default-features = false }
pgp = { version = "0.6.0", default-features = false }
hex = "0.4.0"
sha2 = "0.9.0"
rand = "0.7.0"
@@ -20,12 +20,12 @@ 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 = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
async-smtp = { version = "0.3" }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
async-imap = "0.4.0"
async-imap = "0.3.1"
async-native-tls = { version = "0.3.3" }
async-std = { version = "1.6.4", features = ["unstable"] }
async-std = { version = "1.6.1", features = ["unstable"] }
base64 = "0.12"
charset = "0.1"
percent-encoding = "2.0"
@@ -34,22 +34,20 @@ serde_json = "1.0"
chrono = "0.4.6"
indexmap = "1.3.0"
kamadak-exif = "0.5"
once_cell = "1.4.1"
lazy_static = "1.4.0"
regex = "1.1.6"
rusqlite = { version = "0.24", features = ["bundled"] }
r2d2_sqlite = "0.17.0"
r2d2 = "0.8.5"
strum = "0.19.0"
strum_macros = "0.19.0"
strum = "0.18.0"
strum_macros = "0.18.0"
backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.9.0"
itertools = "0.8.0"
image-meta = "0.1.0"
quick-xml = "0.18.1"
escaper = "0.1.0"
bitflags = "1.1.0"
sanitize-filename = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.13.0"
mailparse = "0.12.1"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
@@ -59,25 +57,22 @@ anyhow = "1.0.28"
async-trait = "0.1.31"
url = "2.1.1"
async-std-resolver = "0.19.5"
async-tar = "0.3.0"
uuid = { version = "0.8", features = ["serde", "v4"] }
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 }
dirs = { version = "3.0.1", optional=true }
toml = "0.5.6"
[dev-dependencies]
tempfile = "3.0"
pretty_assertions = "0.6.1"
pretty_env_logger = "0.4.0"
proptest = "0.10"
async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
futures-lite = "1.7.0"
criterion = "0.3"
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
smol = "0.1.11"
log = "0.4.8"
[workspace]
members = [
@@ -96,14 +91,9 @@ path = "examples/repl/main.rs"
required-features = ["repl"]
[[bench]]
name = "create_account"
harness = false
[features]
default = []
internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term"]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
nightly = ["pgp/nightly"]

View File

@@ -95,8 +95,7 @@ $ cargo build -p deltachat_ffi --release
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
- `RUST_LOG=info,async_imap=trace,async_smtp=trace`: enable IMAP and
SMTP tracing in addition to info messages.
### Expensive tests

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

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

View File

@@ -16,6 +16,6 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK 1
ADD deps/build_python.sh /builder/build_python.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
# Install Rust
# Install Rust nightly
ADD deps/build_rust.sh /builder/build_rust.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1

View File

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

View File

@@ -4,6 +4,8 @@ export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
@@ -28,7 +30,6 @@ set +x
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
set +x -e
shopt -s huponexit
cd $BUILDDIR
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL

View File

@@ -4,6 +4,8 @@ export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
@@ -22,12 +24,10 @@ echo "--- Running $CIRCLE_JOB remotely"
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
# let's share the target dir with our last run on this branch/job-type
# cargo will make sure to block/unblock us properly
export CARGO_TARGET_DIR=\`pwd\`/../target
export TARGET=release
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL

View File

@@ -4,6 +4,8 @@ export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
set -e
@@ -18,10 +20,10 @@ echo "--- Running $CIRCLE_JOB remotely"
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
# let's share the target dir with our last run on this branch/job-type
# cargo will make sure to block/unblock us properly
export CARGO_TARGET_DIR=\`pwd\`/../target
export TARGET=x86_64-unknown-linux-gnu
export RUSTC_WRAPPER=sccache

View File

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

View File

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

View File

@@ -15,7 +15,6 @@ cargo build --release -p deltachat_ffi
# Statically link against libdeltachat.a.
export DCC_RS_DEV=$(pwd)
export DCC_RS_TARGET=release
# Configure access to a base python and to several python interpreters
# needed by tox below.

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -105,9 +105,8 @@ impl<T: AsRef<std::ffi::OsStr>> OsStrExt for T {
#[cfg(not(target_os = "windows"))]
fn to_c_string(&self) -> Result<CString, CStringError> {
use std::os::unix::ffi::OsStrExt;
CString::new(self.as_ref().as_bytes()).map_err(|err| {
let std::ffi::NulError { .. } = err;
CStringError::InteriorNullByte
CString::new(self.as_ref().as_bytes()).map_err(|err| match err {
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
})
}
@@ -123,9 +122,8 @@ fn os_str_to_c_string_unicode(
os_str: &dyn AsRef<std::ffi::OsStr>,
) -> Result<CString, CStringError> {
match os_str.as_ref().to_str() {
Some(val) => CString::new(val.as_bytes()).map_err(|err| {
let std::ffi::NulError { .. } = err;
CStringError::InteriorNullByte
Some(val) => CString::new(val.as_bytes()).map_err(|err| match err {
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
}),
None => Err(CStringError::NotUnicode),
}

View File

@@ -8,36 +8,33 @@ 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(ToSql)]
pub fn to_sql_derive(input: TokenStream) -> TokenStream {
#[proc_macro_derive(Sqlx)]
pub fn sqlx_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
impl 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<'q> sqlx::encode::Encode<'q, sqlx::sqlite::Sqlite> for #name {
fn encode_by_ref(&self, buf: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>) -> sqlx::encode::IsNull {
num_traits::ToPrimitive::to_i32(self).expect("invalid type").encode(buf)
}
}
impl<'de> sqlx::decode::Decode<'de, sqlx::sqlite::Sqlite> for #name {
fn decode(value: sqlx::sqlite::SqliteValueRef) -> std::result::Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
let raw: i32 = sqlx::decode::Decode::decode(value)?;
Ok(num_traits::FromPrimitive::from_i32(raw).unwrap_or_default())
}
}
impl sqlx::types::Type<sqlx::sqlite::Sqlite> for #name {
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
<i32 as sqlx::types::Type<_>>::type_info()
}
}
};
gen.into()
}

View File

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

View File

@@ -1,10 +1,8 @@
extern crate dirs;
use std::str::FromStr;
use anyhow::{bail, ensure};
use async_std::path::Path;
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, ProtectionStatus};
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
@@ -19,7 +17,7 @@ use deltachat::message::{self, Message, MessageState, MsgId};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::EventType;
use deltachat::Event;
use deltachat::{config, provider};
/// Reset database tables.
@@ -30,7 +28,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 1 {
context
.sql()
.execute("DELETE FROM jobs;", paramsv![])
.execute("DELETE FROM jobs;", paramsx![])
.await
.unwrap();
println!("(1) Jobs reset.");
@@ -38,7 +36,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 2 {
context
.sql()
.execute("DELETE FROM acpeerstates;", paramsv![])
.execute("DELETE FROM acpeerstates;", paramsx![])
.await
.unwrap();
println!("(2) Peerstates reset.");
@@ -46,7 +44,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 4 {
context
.sql()
.execute("DELETE FROM keypairs;", paramsv![])
.execute("DELETE FROM keypairs;", paramsx![])
.await
.unwrap();
println!("(4) Private keypairs reset.");
@@ -54,41 +52,41 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 8 {
context
.sql()
.execute("DELETE FROM contacts WHERE id>9;", paramsv![])
.execute("DELETE FROM contacts WHERE id>9;", paramsx![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
.execute("DELETE FROM chats WHERE id>9;", paramsx![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats_contacts;", paramsv![])
.execute("DELETE FROM chats_contacts;", paramsx![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
.execute("DELETE FROM msgs WHERE id>9;", paramsx![])
.await
.unwrap();
context
.sql()
.execute(
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
paramsv![],
paramsx![],
)
.await
.unwrap();
context
.sql()
.execute("DELETE FROM leftgrps;", paramsv![])
.execute("DELETE FROM leftgrps;", paramsx![])
.await
.unwrap();
println!("(8) Rest but server config reset.");
}
context.emit_event(EventType::MsgsChanged {
context.emit_event(Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
@@ -120,7 +118,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
.await
.unwrap();
} else {
let rs = context.sql().get_raw_config(context, "import_spec").await;
let rs = context.sql().get_raw_config("import_spec").await;
if rs.is_none() {
error!(context, "Import: No file or folder given.");
return false;
@@ -159,7 +157,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
if read_cnt > 0 {
context.emit_event(EventType::MsgsChanged {
context.emit_event(Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
@@ -193,6 +191,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
&contact_name,
contact_id,
msgtext.unwrap_or_default(),
if msg.is_starred() { "" } else { "" },
if msg.get_from_id() == 1 as libc::c_uint {
""
} else if msg.get_state() == MessageState::InSeen {
@@ -203,15 +202,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
msg.get_videochat_url().unwrap_or_default(),
msg.get_videochat_type().unwrap_or_default()
)
} else {
"".to_string()
},
if msg.is_forwarded() {
"[FORWARDED]"
} else {
@@ -225,7 +215,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
let mut lines_out = 0;
for &msg_id in msglist {
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
if msg_id.is_daymarker() {
println!(
"--------------------------------------------------------------------------------"
);
@@ -285,10 +275,8 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
"addr unset"
}
);
let peerstate = Peerstate::from_addr(context, &addr)
.await
.expect("peerstate error");
if peerstate.is_some() && contact_id != 1 as libc::c_uint {
let peerstate = Peerstate::from_addr(context, &addr).await;
if peerstate.is_ok() && contact_id != 1 as libc::c_uint {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
@@ -357,7 +345,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
createchat <contact-id>\n\
createchatbymsg <msg-id>\n\
creategroup <name>\n\
createprotected <name>\n\
createverified <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
groupname <name>\n\
@@ -371,7 +359,6 @@ 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\
@@ -379,8 +366,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unarchive <chat-id>\n\
pin <chat-id>\n\
unpin <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
@@ -388,6 +373,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
listfresh\n\
forward <msg-id> <chat-id>\n\
markseen <msg-id>\n\
star <msg-id>\n\
unstar <msg-id>\n\
delmsg <msg-id>\n\
===========================Contact commands==\n\
listcontacts [<query>]\n\
@@ -404,6 +391,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
event <event-id to test>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
clear -- clear screen\n\
exit or quit\n\
============================================="
@@ -442,21 +430,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
has_backup(&context, blobdir).await?;
}
"export-backup" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportBackup, Some(&dir)).await?;
println!("Exported to {}.", dir.to_string_lossy());
imex(&context, ImexMode::ExportBackup, Some(blobdir)).await?;
}
"import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(&context, ImexMode::ImportBackup, Some(arg1)).await?;
}
"export-keys" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportSelfKeys, Some(&dir)).await?;
println!("Exported to {}.", dir.to_string_lossy());
imex(&context, ImexMode::ExportSelfKeys, Some(blobdir)).await?;
}
"import-keys" => {
imex(&context, ImexMode::ImportSelfKeys, Some(arg1)).await?;
imex(&context, ImexMode::ImportSelfKeys, Some(blobdir)).await?;
}
"export-setup" => {
let setup_code = create_setup_code(&context);
@@ -500,7 +484,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
context.maybe_network().await;
}
"housekeeping" => {
sql::housekeeping(&context).await;
sql::housekeeping(&context).await?;
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
@@ -525,7 +509,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
println!(
"{}#{}: {} [{} fresh] {}{}",
"{}#{}: {} [{} fresh] {}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
@@ -535,7 +519,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
if chat.is_protected() { "🛡️" } else { "" },
);
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
let statestr = if chat.visibility == ChatVisibility::Archived {
@@ -590,17 +573,8 @@ 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 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 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 subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string()
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
@@ -610,7 +584,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
format!("{} member(s)", members.len())
};
println!(
"{}#{}: {} [{}]{}{} {}",
"{}#{}: {} [{}]{}{}",
chat_prefix(sel_chat),
sel_chat.get_id(),
sel_chat.get_name(),
@@ -620,18 +594,13 @@ 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(),
},
_ => "".to_string(),
},
if sel_chat.is_protected() {
"🛡️"
} else {
""
},
);
log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
@@ -662,16 +631,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?;
println!("Group#{} created successfully.", chat_id);
}
"createprotected" => {
"createverified" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?;
println!("Group#{} created and protected successfully.", chat_id);
println!("VerifiedGroup#{} created successfully.", chat_id);
}
"addmember" => {
ensure!(sel_chat.is_some(), "No chat selected");
@@ -723,7 +691,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;
@@ -794,7 +762,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let latitude = arg1.parse()?;
let longitude = arg2.parse()?;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
if continue_streaming {
println!("Success, streaming should be continued.");
} else {
@@ -831,10 +799,6 @@ 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.");
@@ -881,6 +845,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"updatedevicechats" => {
context.update_device_chats().await?;
}
"listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -891,7 +858,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 {
@@ -912,21 +879,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id
.set_protection(
&context,
match arg0 {
"protect" => ProtectionStatus::Protected,
"unprotect" => ProtectionStatus::Unprotected,
_ => unreachable!("arg0={:?}", arg0),
_ => panic!("Unexpected command (This should never happen)"),
},
)
.await?;
@@ -939,7 +892,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" => {
@@ -965,6 +918,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
msg_ids[0] = MsgId::new(arg1.parse()?);
message::markseen_msgs(&context, msg_ids).await;
}
"star" | "unstar" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = vec![MsgId::new(0); 1];
msg_ids[0] = MsgId::new(arg1.parse()?);
message::star_msgs(&context, msg_ids, arg0 == "star").await;
}
"delmsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut ids = [MsgId::new(0); 1];
@@ -1075,7 +1034,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
// let event = arg1.parse()?;
// let event = EventType::from_u32(event).ok_or(format_err!("EventType::from_u32({})", event))?;
// let event = Event::from_u32(event).ok_or(format_err!("Event::from_u32({})", event))?;
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
// println!(
// "Sending event {:?}({}), received value {}.",
@@ -1102,6 +1061,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
seconds, device_cnt, server_cnt
);
}
"emptyserver" => {
ensure!(!arg1.is_empty(), "Argument <flags> missing");
message::dc_empty_server(&context, arg1.parse()?).await;
}
"" => (),
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
}

View File

@@ -19,7 +19,7 @@ use deltachat::config;
use deltachat::context::*;
use deltachat::oauth2::*;
use deltachat::securejoin::*;
use deltachat::EventType;
use deltachat::Event;
use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType;
@@ -34,35 +34,35 @@ mod cmdline;
use self::cmdline::*;
/// Event Handler
fn receive_event(event: EventType) {
fn receive_event(event: Event) {
let yellow = Color::Yellow.normal();
match event {
EventType::Info(msg) => {
Event::Info(msg) => {
/* do not show the event as this would fill the screen */
info!("{}", msg);
}
EventType::SmtpConnected(msg) => {
Event::SmtpConnected(msg) => {
info!("[SMTP_CONNECTED] {}", msg);
}
EventType::ImapConnected(msg) => {
Event::ImapConnected(msg) => {
info!("[IMAP_CONNECTED] {}", msg);
}
EventType::SmtpMessageSent(msg) => {
Event::SmtpMessageSent(msg) => {
info!("[SMTP_MESSAGE_SENT] {}", msg);
}
EventType::Warning(msg) => {
Event::Warning(msg) => {
warn!("{}", msg);
}
EventType::Error(msg) => {
Event::Error(msg) => {
error!("{}", msg);
}
EventType::ErrorNetwork(msg) => {
Event::ErrorNetwork(msg) => {
error!("[NETWORK] msg={}", msg);
}
EventType::ErrorSelfNotInGroup(msg) => {
Event::ErrorSelfNotInGroup(msg) => {
error!("[SELF_NOT_IN_GROUP] {}", msg);
}
EventType::MsgsChanged { chat_id, msg_id } => {
Event::MsgsChanged { chat_id, msg_id } => {
info!(
"{}",
yellow.paint(format!(
@@ -71,44 +71,34 @@ fn receive_event(event: EventType) {
))
);
}
EventType::ContactsChanged(_) => {
Event::ContactsChanged(_) => {
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
}
EventType::LocationChanged(contact) => {
Event::LocationChanged(contact) => {
info!(
"{}",
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
);
}
EventType::ConfigureProgress { progress, comment } => {
if let Some(comment) = comment {
info!(
"{}",
yellow.paint(format!(
"Received CONFIGURE_PROGRESS({} ‰, {})",
progress, comment
))
);
} else {
info!(
"{}",
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
);
}
Event::ConfigureProgress(progress) => {
info!(
"{}",
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
);
}
EventType::ImexProgress(progress) => {
Event::ImexProgress(progress) => {
info!(
"{}",
yellow.paint(format!("Received IMEX_PROGRESS({} ‰)", progress))
);
}
EventType::ImexFileWritten(file) => {
Event::ImexFileWritten(file) => {
info!(
"{}",
yellow.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display()))
);
}
EventType::ChatModified(chat) => {
Event::ChatModified(chat) => {
info!(
"{}",
yellow.paint(format!("Received CHAT_MODIFIED({})", chat))
@@ -168,7 +158,7 @@ const DB_COMMANDS: [&str; 9] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 27] = [
const CHAT_COMMANDS: [&str; 26] = [
"listchats",
"listarchived",
"chat",
@@ -188,7 +178,6 @@ const CHAT_COMMANDS: [&str; 27] = [
"send",
"sendimage",
"sendfile",
"videochat",
"draft",
"listmedia",
"archive",
@@ -282,12 +271,12 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf(), 0).await?;
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf()).await?;
let events = context.get_event_emitter();
async_std::task::spawn(async move {
while let Some(event) = events.recv().await {
receive_event(event.typ);
receive_event(event);
}
});
@@ -420,7 +409,7 @@ async fn handle_cmd(
"joinqr" => {
ctx.start_io().await;
if !arg0.is_empty() {
dc_join_securejoin(&ctx, arg1).await?;
dc_join_securejoin(&ctx, arg1).await;
}
}
"exit" | "quit" => return Ok(ExitResult::Exit),

View File

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

View File

@@ -1,22 +1,6 @@
1.44.0
------
- fix Chat.get_mute_duration()
1.40.1
0.900.0 (DRAFT)
---------------
- emit "ac_member_removed" event (with 'actor' being the removed contact)
for when a user leaves a group.
- fix create_contact(addr) when addr is the self-contact.
1.40.0
---------------
- uses latest 1.40+ Delta Chat core
- refactored internals to use plugin-approach
- introduced PerAccount and Global hooks that plugins can implement
@@ -26,7 +10,6 @@
- introduced two documented examples for an echo and a group-membership
tracking plugin.
0.800.0
-------

View File

@@ -7,14 +7,76 @@ which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
a low-level Chat/Contact/Message API to user interfaces and bots.
Installing bindings from source (Updated: 20-Jan-2020)
=========================================================
Install Rust and Cargo first. Deltachat needs a specific nightly
version, the easiest is probably to first install Rust stable from
rustup and then use this to install the correct nightly version.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then GIT clone the deltachat-core-rust repo and get the actual
rust- and cargo-toolchain needed by deltachat::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
rustup show
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
Ensure you are in the deltachat-core-rust/python directory, create the
virtual environment and activate it in your shell::
cd python
python3 -m venv venv # or: virtualenv venv
source venv/bin/activate
You should now be able to build the python bindings using the supplied script::
./install_python_bindings.py
The installation might take a while, depending on your machine.
The bindings will be installed in release mode but with debug symbols.
The release mode is currently necessary because some tests generate RSA keys
which is prohibitively slow in non-release mode.
After successful binding installation you can install a few more
Python packages before running the tests::
python -m pip install pytest pytest-timeout pytest-rerunfailures requests
pytest -v tests
running "live" tests with temporary accounts
---------------------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_NEW_TMP_EMAIL`` to:
- a particular https-url that you can ask for from the delta
chat devs. This is implemented on the server side via
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
- or the path of a file that contains two lines, each describing
via "addr=... mail_pw=..." a test account login that will
be used for the live tests.
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
e-mail accounts and run through all functional "liveconfig" tests.
Installing pre-built packages (Linux-only)
========================================================
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps. Otherwise you need to `compile the Delta Chat bindings
yourself <sourceinstall>`_.
without any "build-from-source" steps.
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
then create a fresh Python virtual environment and activate it in your shell::
virtualenv venv # or: python -m venv
@@ -41,78 +103,6 @@ To verify it worked::
`in contact with us <https://delta.chat/en/contribute>`_.
Running tests
=============
After successful binding installation you can install a few more
Python packages before running the tests::
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
pytest -v tests
This will run all "offline" tests and skip all functional
end-to-end tests that require accounts on real e-mail servers.
.. _livetests:
running "live" tests with temporary accounts
---------------------------------------------
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL``::
export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=1h_4w4r8h7y9nmcdsy
With this, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
These accounts exists for one 1hour and then are removed completely.
One hour is enough to invoke pytest and run all offline and online tests:
pytest
# or if you have installed pytest-xdist for parallel test execution
pytest -n6
Each test run creates new accounts.
.. _sourceinstall:
Installing bindings from source (Updated: July 2020)
=========================================================
Install Rust and Cargo first.
The easiest is probably to use `rustup <https://rustup.rs/>`_.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then clone the deltachat-core-rust repo::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
Ensure you are in the deltachat-core-rust/python directory, create the
virtual environment and activate it in your shell::
cd python
python3 -m venv venv # or: virtualenv venv
source venv/bin/activate
You should now be able to build the python bindings using the supplied script::
python install_python_bindings.py
The core compilation and bindings building might take a while,
depending on the speed of your machine.
The bindings will be installed in release mode but with debug symbols.
The release mode is currently necessary because some tests generate RSA keys
which is prohibitively slow in non-release mode.
Code examples
=============

View File

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

View File

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

View File

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

View File

@@ -17,12 +17,8 @@ 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)

View File

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

View File

@@ -179,6 +179,17 @@ class Account(object):
if not self.is_configured():
raise ValueError("need to configure first")
def empty_server_folders(self, inbox=False, mvbox=False):
""" empty server folders. """
flags = 0
if inbox:
flags |= const.DC_EMPTY_INBOX
if mvbox:
flags |= const.DC_EMPTY_MVBOX
if not flags:
raise ValueError("no flags set")
lib.dc_empty_server(self._dc_context, flags)
def get_latest_backupfile(self, backupdir):
""" return the latest backup file in a given directory.
"""
@@ -205,7 +216,7 @@ class Account(object):
def create_contact(self, obj, name=None):
""" create a (new) Contact or return an existing one.
Calling this method will always result in the same
Calling this method will always resulut in the same
underlying contact id. If there already is a Contact
with that e-mail address, it is unblocked and its display
`name` is updated if specified.
@@ -235,6 +246,7 @@ 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):
@@ -336,9 +348,6 @@ class Account(object):
def get_deaddrop_chat(self):
return Chat(self, const.DC_CHAT_ID_DEADDROP)
def get_device_chat(self):
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
def get_message_by_id(self, msg_id):
""" return Message instance.
:param msg_id: integer id of this message.
@@ -549,9 +558,6 @@ class Account(object):
You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the
account is started.
If you are using this from a test, you may want to call
wait_all_initial_fetches() afterwards.
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
:raises ConfigureFailed: if the account could not be configured.
@@ -601,24 +607,12 @@ class Account(object):
self.stop_io()
self.log("remove dc_context references")
# if _dc_context is unref'ed the event thread should quickly
# receive the termination signal. However, some python code might
# still hold a reference and so we use a secondary signal
# to make sure the even thread terminates if it receives any new
# event, indepedently from waiting for the core to send NULL to
# get_next_event().
self._event_thread.mark_shutdown()
# the dc_context_unref triggers get_next_event to return ffi.NULL
# which in turns makes the event thread finish execution
self._dc_context = None
self.log("wait for event thread to finish")
try:
self._event_thread.wait(timeout=2)
except RuntimeError as e:
self.log("Waiting for event thread failed: {}".format(e))
if self._event_thread.is_alive():
self.log("WARN: event thread did not terminate yet, ignoring.")
self._event_thread.wait()
self._shutdown_event.set()

View File

@@ -57,7 +57,10 @@ class Chat(object):
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
"""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
return lib.dc_chat_get_type(self._dc_chat) in (
const.DC_CHAT_TYPE_GROUP,
const.DC_CHAT_TYPE_VERIFIED_GROUP
)
def is_deaddrop(self):
""" return true if this chat is a deaddrop chat.
@@ -82,20 +85,12 @@ class Chat(object):
"""
return not lib.dc_chat_is_unpromoted(self._dc_chat)
def can_send(self):
"""Check if messages can be sent to a give chat.
This is not true eg. for the deaddrop or for the device-talk
def is_verified(self):
""" return True if this chat is a verified group.
:returns: True if the chat is writable, False otherwise
:returns: True if chat is verified, False otherwise.
"""
return lib.dc_chat_can_send(self._dc_chat)
def is_protected(self):
""" return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise.
"""
return lib.dc_chat_is_protected(self._dc_chat)
return lib.dc_chat_is_verified(self._dc_chat)
def get_name(self):
""" return name of this chat.
@@ -142,23 +137,7 @@ class Chat(object):
:param duration:
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
"""
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
def get_ephemeral_timer(self):
""" get ephemeral timer.
:returns: ephemeral timer value in seconds
"""
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
def set_ephemeral_timer(self, timer):
""" set ephemeral timer.
:param: timer value in seconds
:returns: None
"""
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
def get_type(self):
""" (deprecated) return type of this chat.

View File

@@ -51,10 +51,6 @@ class Contact(object):
""" Return True if the contact is blocked. """
return lib.dc_contact_is_blocked(self._dc_contact)
def set_blocked(self, block=True):
""" Block or unblock a contact. """
return lib.dc_block_contact(self.account._dc_context, self.id, block)
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)

View File

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

View File

@@ -24,13 +24,12 @@ def dc_account_extra_configure(account):
""" Reset the account (we reuse accounts across tests)
and make 'account.direct_imap' available for direct IMAP ops.
"""
if not hasattr(account, "direct_imap"):
imap = DirectImap(account)
if imap.select_config_folder("mvbox"):
imap.delete(ALL, expunge=True)
assert imap.select_config_folder("inbox")
imap = DirectImap(account)
if imap.select_config_folder("mvbox"):
imap.delete(ALL, expunge=True)
setattr(account, "direct_imap", imap)
assert imap.select_config_folder("inbox")
imap.delete(ALL, expunge=True)
setattr(account, "direct_imap", imap)
@deltachat.global_hookimpl
@@ -109,15 +108,6 @@ class DirectImap:
def get_all_messages(self):
assert not self._idling
# Flush unsolicited responses. IMAPClient has problems
# dealing with them: https://github.com/mjs/imapclient/issues/334
# When this NOOP was introduced, next FETCH returned empty
# result instead of a single message, even though IMAP server
# can only return more untagged responses than required, not
# less.
self.conn.noop()
return self.conn.fetch(ALL, [FLAGS])
def get_unread_messages(self):
@@ -169,13 +159,9 @@ class DirectImap:
log("---------", imapfolder, len(messages), "messages ---------")
# get message content without auto-marking it as seen
# fetching 'RFC822' would mark it as seen.
requested = [b'BODY.PEEK[]', FLAGS]
requested = [b'BODY.PEEK[HEADER]', FLAGS]
for uid, data in self.conn.fetch(messages, requested).items():
body_bytes = data[b'BODY[]']
if not body_bytes:
log("Message", uid, "has empty body")
continue
body_bytes = data[b'BODY[HEADER]']
flags = data[FLAGS]
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
path.mkdir(parents=True, exist_ok=True)
@@ -206,7 +192,6 @@ class DirectImap:
raise TimeoutError
if terminate:
self.idle_done()
self.account.log("imap-direct: idle_check returned {!r}".format(res))
return res
def idle_wait_for_seen(self):

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
""" The Message object. """
import os
import re
from . import props
from .cutil import from_dc_charpointer, as_dc_charpointer
from .capi import lib, ffi
@@ -155,47 +154,6 @@ class Message(object):
if ts:
return datetime.utcfromtimestamp(ts)
@props.with_doc
def ephemeral_timer(self):
"""Ephemeral timer in seconds
:returns: timer in seconds or None if there is no timer
"""
timer = lib.dc_msg_get_ephemeral_timer(self._dc_msg)
if timer:
return timer
@props.with_doc
def ephemeral_timestamp(self):
"""UTC time when the message will be deleted.
:returns: naive datetime.datetime() object or None if the timer is not started.
"""
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
if ts:
return datetime.utcfromtimestamp(ts)
@property
def quoted_text(self):
"""Text inside the quote
:returns: Quoted text"""
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
@property
def quote(self):
"""Quote getter
:returns: Quoted message, if found in the database"""
msg = lib.dc_msg_get_quoted_msg(self._dc_msg)
if msg:
return Message(self.account, ffi.gc(msg, lib.dc_msg_unref))
@quote.setter
def quote(self, quoted_message):
"""Quote setter"""
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
def get_mime_headers(self):
""" return mime-header object for an incoming message.
@@ -378,43 +336,20 @@ def get_viewtype_code_from_name(view_type_name):
def map_system_message(msg):
if msg.is_system_message():
res = parse_system_add_remove(msg.text)
if not res:
return
action, affected, actor = res
affected = msg.account.get_contact_by_addr(affected)
if actor == "me":
actor = None
else:
actor = msg.account.get_contact_by_addr(actor)
d = dict(chat=msg.chat, contact=affected, actor=actor, message=msg)
return "ac_member_" + res[0], d
def extract_addr(text):
m = re.match(r'.*\((.+@.+)\)', text)
if m:
text = m.group(1)
text = text.rstrip(".")
return text.strip()
if res:
contact = msg.account.get_contact_by_addr(res[1])
if contact:
d = dict(chat=msg.chat, contact=contact, message=msg)
return "ac_member_" + res[0], d
def parse_system_add_remove(text):
""" return add/remove info from parsing the given system message text.
returns a (action, affected, actor) triple """
# Member Me (x@y) removed by a@b.
# Member x@y added by a@b
# Member With space (tmp1@x.org) removed by tmp2@x.org.
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
# Group left by some one (tmp1@x.org).
# Group left by tmp1@x.org.
# Member x@y removed by a@b
text = text.lower()
m = re.match(r'member (.+) (removed|added) by (.+)', text)
if m:
affected, action, actor = m.groups()
return action, extract_addr(affected), extract_addr(actor)
if text.startswith("group left by "):
addr = extract_addr(text[13:])
if addr:
return "removed", addr, addr
parts = text.split()
if parts[0] == "member":
if parts[2] in ("removed", "added"):
return parts[2], parts[1]
if parts[3] in ("removed", "added"):
return parts[3], parts[2].strip("()")

View File

@@ -32,10 +32,6 @@ def pytest_addoption(parser):
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
parser.addoption(
"--strict-tls", action="store_true",
help="Never accept invalid TLS certificates for test accounts",
)
def pytest_configure(config):
@@ -156,7 +152,7 @@ class SessionLiveConfigFromURL:
assert index == len(self.configlist), index
res = requests.post(self.url)
if res.status_code != 200:
pytest.skip("creating newtmpuser failed with code {}: '{}'".format(res.status_code, res.text))
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
@@ -235,13 +231,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac._evtracker.set_timeout(30)
ac.addr = ac.get_self_contact().addr
ac.set_config("displayname", logid)
if not quiet:
logger = FFIEventLogger(ac)
logger.init_time = self.init_time
ac.add_account_plugin(logger)
ac.add_account_plugin(FFIEventLogger(ac))
self._accounts.append(ac)
return ac
@@ -251,7 +244,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def get_unconfigured_account(self):
self.offline_count += 1
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
return self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(2)
return ac
def _preconfigure_key(self, account, addr):
# Only set a key if we haven't used it yet for another account.
@@ -286,15 +282,16 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
if pytestconfig.getoption("--strict-tls"):
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
@@ -335,6 +332,8 @@ 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"),
@@ -342,29 +341,19 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
mvbox_move=account.get_config("mvbox_move"),
sentbox_watch=account.get_config("sentbox_watch"),
))
if hasattr(account, "direct_imap"):
# Attach the existing direct_imap. If we did not do this, a new one would be created and
# delete existing messages (see dc_account_extra_configure(configure))
ac.direct_imap = account.direct_imap
ac._configtracker = ac.configure()
return ac
def wait_configure_and_start_io(self):
started_accounts = []
for acc in self._accounts:
if hasattr(acc, "_configtracker"):
acc._configtracker.wait_finish()
acc._evtracker.consume_events()
acc.get_device_chat().mark_noticed()
del acc._configtracker
acc.set_config("bcc_self", "0")
if acc.is_configured() and not acc.is_started():
acc.start_io()
started_accounts.append(acc)
print("{}: {} account was successfully setup".format(
acc.get_config("displayname"), acc.get_config("addr")))
for acc in started_accounts:
acc._evtracker.wait_all_initial_fetches()
def run_bot_process(self, module, ffi=True):
fn = module.__file__

View File

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

View File

@@ -6,27 +6,13 @@ import queue
import time
from deltachat import const, Account
from deltachat.message import Message
from deltachat.tracker import ImexTracker
from deltachat.hookspec import account_hookimpl
from datetime import datetime, timedelta
@pytest.mark.parametrize("msgtext,res", [
("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),
("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")),
])
def test_parse_system_add_remove(msgtext, res):
from deltachat.message import parse_system_add_remove
@@ -130,11 +116,6 @@ 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")
@@ -167,16 +148,6 @@ class TestOfflineContact:
with pytest.raises(ValueError):
ac1.create_chat(ac3)
def test_contact_rename(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
contact = ac1.create_contact("some1@example.com", name="some1")
chat = ac1.create_chat(contact)
assert chat.get_name() == "some1"
ac1.create_contact("some1@example.com", name="renamed")
ev = ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
assert ev.data1 == chat.id
assert chat.get_name() == "renamed"
class TestOfflineChat:
@pytest.fixture
@@ -287,23 +258,15 @@ class TestOfflineChat:
def test_mute(self, ac1):
chat = ac1.create_group_chat(name="title1")
assert not chat.is_muted()
assert chat.get_mute_duration() == 0
chat.mute()
assert chat.is_muted()
assert chat.get_mute_duration() == -1
chat.unmute()
assert not chat.is_muted()
chat.mute(50)
assert chat.is_muted()
assert chat.get_mute_duration() <= 50
with pytest.raises(ValueError):
chat.mute(-51)
# Regression test, this caused Rust panic previously
chat.mute(2**63 - 1)
assert chat.is_muted()
assert chat.get_mute_duration() == -1
def test_delete_and_send_fails(self, ac1, chat1):
chat1.delete()
ac1._evtracker.wait_next_messages_changed()
@@ -477,21 +440,6 @@ class TestOfflineChat:
assert not res.is_ask_verifygroup()
assert res.contact_id == 10
def test_quote(self, chat1):
"""Offline quoting test"""
msg = Message.new_empty(chat1.account, "text")
msg.set_text("Multi\nline\nmessage")
assert msg.quoted_text is None
# Prepare message to assign it a Message-Id.
# Messages without Message-Id cannot be quoted.
msg = chat1.prepare_message(msg)
reply_msg = Message.new_empty(chat1.account, "text")
reply_msg.set_text("reply")
reply_msg.quote = msg
assert reply_msg.quoted_text == "Multi\nline\nmessage"
def test_group_chat_many_members_add_remove(self, ac1, lp):
lp.sec("ac1: creating group chat with 10 other members")
chat = ac1.create_group_chat(name="title1")
@@ -504,12 +452,12 @@ class TestOfflineChat:
class InPlugin:
@account_hookimpl
def ac_member_added(self, chat, contact, actor):
in_list.append(("added", chat, contact, actor))
def ac_member_added(self, chat, contact):
in_list.append(("added", chat, contact))
@account_hookimpl
def ac_member_removed(self, chat, contact, actor):
in_list.append(("removed", chat, contact, actor))
def ac_member_removed(self, chat, contact):
in_list.append(("removed", chat, contact))
ac1.add_account_plugin(InPlugin())
@@ -538,11 +486,10 @@ class TestOfflineChat:
assert len(in_list) == 10
chat_contacts = chat.get_contacts()
for in_cmd, in_chat, in_contact, in_actor in in_list:
for in_cmd, in_chat, in_contact 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
@@ -570,7 +517,7 @@ def test_basic_imap_api(acfactory, tmpdir):
imap2 = ac2.direct_imap
imap2.idle_start()
ac2.direct_imap.idle_start()
chat12.send_text("hello")
ac2._evtracker.wait_next_incoming_message()
@@ -845,12 +792,18 @@ class TestOnlineAccount:
assert msg_in.text == "message2"
assert msg_in.is_forwarded()
def test_send_self_message(self, acfactory, lp):
def test_send_self_message_and_empty_folder(self, acfactory, lp):
ac1 = acfactory.get_one_online_account(mvbox=True, move=True)
lp.sec("ac1: create self chat")
chat = ac1.get_self_contact().create_chat()
chat.send_text("hello")
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
ac1.empty_server_folders(inbox=True, mvbox=True)
ev1 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED")
ev2 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED")
boxes = [ev1.data2, ev2.data2]
boxes.remove("INBOX")
assert len(boxes) == 1 and boxes[0].endswith("DeltaChat")
def test_send_and_receive_message_markseen(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -898,13 +851,7 @@ class TestOnlineAccount:
lp.sec("mark messages as seen on ac2, wait for changes on ac1")
ac2.direct_imap.idle_start()
ac1.direct_imap.idle_start()
ac2.mark_seen_messages([msg2, msg4])
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert msg2.chat.id == msg4.chat.id
assert ev.data1 == msg2.chat.id
assert ev.data2 == 0
ac2.direct_imap.idle_check(terminate=True)
lp.step("1")
for i in range(2):
@@ -1012,10 +959,7 @@ class TestOnlineAccount:
chat = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("sending multi-line non-unicode message from ac1 to ac2")
text1 = (
"hello\nworld\nthis is a very long message that should be"
+ " wrapped using format=flowed and unwrapped on the receiver"
)
text1 = "hello\nworld"
msg_out = chat.send_text(text1)
assert not msg_out.is_encrypted()
@@ -1033,68 +977,7 @@ class TestOnlineAccount:
assert msg_in.text == text2
assert ac1.get_config("addr") in [x.addr for x in msg_in.chat.get_contacts()]
def test_no_draft_if_cant_send(self, acfactory):
"""Tests that no quote can be set if the user can't send to this chat"""
ac1 = acfactory.get_one_online_account()
device_chat = ac1.get_device_chat()
msg = Message.new_empty(ac1, "text")
device_chat.set_draft(msg)
assert not device_chat.can_send()
assert device_chat.get_draft() is None
def test_prefer_encrypt(self, acfactory, lp):
"""Test quorum rule for encryption preference in 1:1 and group chat."""
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
ac1.set_config("e2ee_enabled", "0")
ac2.set_config("e2ee_enabled", "1")
ac3.set_config("e2ee_enabled", "0")
# Make sure we do not send a copy to ourselves. This is to
# test that we count own preference even when we are not in
# the recipient list.
ac1.set_config("bcc_self", "0")
ac2.set_config("bcc_self", "0")
ac3.set_config("bcc_self", "0")
acfactory.introduce_each_other([ac1, ac2, ac3])
lp.sec("ac1: sending message to ac2")
chat1 = ac1.create_chat(ac2)
msg1 = chat1.send_text("message1")
assert not msg1.is_encrypted()
ac2._evtracker.wait_next_incoming_message()
lp.sec("ac2: sending message to ac1")
chat2 = ac2.create_chat(ac1)
msg2 = chat2.send_text("message2")
assert not msg2.is_encrypted()
ac1._evtracker.wait_next_incoming_message()
lp.sec("ac1: sending message to group chat with ac2 and ac3")
group = ac1.create_group_chat("hello")
group.add_contact(ac2)
group.add_contact(ac3)
msg3 = group.send_text("message3")
assert not msg3.is_encrypted()
ac2._evtracker.wait_next_incoming_message()
ac3._evtracker.wait_next_incoming_message()
lp.sec("ac3: start preferring encryption and inform ac1")
ac3.set_config("e2ee_enabled", "1")
chat3 = ac3.create_chat(ac1)
msg4 = chat3.send_text("message4")
# ac1 still does not prefer encryption
assert not msg4.is_encrypted()
ac1._evtracker.wait_next_incoming_message()
lp.sec("ac1: sending another message to group chat with ac2 and ac3")
msg5 = group.send_text("message5")
# Majority prefers encryption now
assert msg5.is_encrypted()
def test_reply_encrypted(self, acfactory, lp):
"""Test that replies to encrypted messages are encrypted."""
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")
@@ -1122,26 +1005,26 @@ class TestOnlineAccount:
print("ac2: e2ee_enabled={}".format(ac2.get_config("e2ee_enabled")))
ac1.set_config("e2ee_enabled", "0")
for quoted_msg in msg1, msg3:
# Save the draft with a quote.
# It should be encrypted if quoted message is encrypted.
msg_draft = Message.new_empty(ac1, "text")
msg_draft.set_text("message reply")
msg_draft.quote = quoted_msg
chat.set_draft(msg_draft)
# Set unprepared and unencrypted draft to test that it is not
# taken into account when determining whether last message is
# encrypted.
msg_draft = Message.new_empty(ac1, "text")
msg_draft.set_text("message2 -- should be encrypted")
chat.set_draft(msg_draft)
# Get the draft, prepare and send it.
msg_draft = chat.get_draft()
msg_out = chat.prepare_message(msg_draft)
chat.send_prepared(msg_out)
# Get the draft, prepare and send it.
msg_draft = chat.get_draft()
msg_out = chat.prepare_message(msg_draft)
chat.send_prepared(msg_out)
chat.set_draft(None)
assert chat.get_draft() is None
chat.set_draft(None)
assert chat.get_draft() is None
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message reply"
assert msg_in.quoted_text == quoted_msg.text
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
lp.sec("wait for ac2 to receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.text == "message2 -- should be encrypted"
assert msg_in.is_encrypted()
def test_saved_mime_on_received_message(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1246,16 +1129,8 @@ class TestOnlineAccount:
backupdir = tmpdir.mkdir("backup")
lp.sec("export all to {}".format(backupdir))
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
path = ac1.export_all(backupdir.strpath)
assert os.path.exists(path)
# check progress events for export
imex_tracker.wait_progress(250)
imex_tracker.wait_progress(500)
imex_tracker.wait_progress(750)
imex_tracker.wait_progress(1000)
# return mex_tracker.wait_finish()
path = ac1.export_all(backupdir.strpath)
assert os.path.exists(path)
t = time.time()
lp.sec("get fresh empty account")
@@ -1266,15 +1141,7 @@ class TestOnlineAccount:
assert path2 == path
lp.sec("import backup and check it's proper")
with ac2.temp_plugin(ImexTracker()) as imex_tracker:
ac2.import_all(path)
# check progress events for import
imex_tracker.wait_progress(250)
imex_tracker.wait_progress(500)
imex_tracker.wait_progress(750)
imex_tracker.wait_progress(1000)
ac2.import_all(path)
contacts = ac2.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
@@ -1370,7 +1237,7 @@ class TestOnlineAccount:
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
assert chat1.is_verified()
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -1389,7 +1256,7 @@ class TestOnlineAccount:
lp.sec("ac2: read message and check it's verified chat")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert msg.chat.is_protected()
assert msg.chat.is_verified()
assert msg.is_encrypted()
lp.sec("ac2: send message and let ac1 read it")
@@ -1508,48 +1375,6 @@ class TestOnlineAccount:
assert ev.action == "removed"
assert ev.message.get_sender_contact().addr == ac1_addr
def test_system_group_msg_from_blocked_user(self, acfactory, lp):
"""
Tests that a blocked user removes you from a group.
The message has to be fetched even though the user is blocked
to avoid inconsistent group state.
Also tests blocking in general.
"""
lp.sec("Create a group chat with ac1 and ac2")
(ac1, ac2) = acfactory.get_two_online_accounts()
acfactory.introduce_each_other((ac1, ac2))
chat_on_ac1 = ac1.create_group_chat("title", contacts=[ac2])
chat_on_ac1.send_text("First group message")
chat_on_ac2 = ac2._evtracker.wait_next_incoming_message().chat
lp.sec("ac1 blocks ac2")
contact = ac1.create_contact(ac2)
contact.set_blocked()
assert contact.is_blocked()
ev = ac1._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
assert ev.data1 == contact.id
lp.sec("ac2 sends a message to ac1 that does not arrive because it is blocked")
ac2.create_chat(ac1).send_text("This will not arrive!")
lp.sec("ac2 sends a group message to ac1 that arrives")
# Groups would be hardly usable otherwise: If you have blocked some
# users, they write messages and you only see replies to them without context
chat_on_ac2.send_text("This will arrive")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "This will arrive"
message_texts = [m.text for m in chat_on_ac1.get_messages()]
assert len(message_texts) == 2
assert "First group message" in message_texts
assert "This will arrive" in message_texts
lp.sec("ac2 removes ac1 from their group")
assert ac1.get_self_contact() in chat_on_ac1.get_contacts()
assert contact.is_blocked()
chat_on_ac2.remove_contact(ac1)
ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
assert not ac1.get_self_contact() in chat_on_ac1.get_contacts()
def test_set_get_group_image(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1671,7 +1496,6 @@ class TestOnlineAccount:
chat41 = ac4.create_chat(ac1)
chat42 = ac4.create_chat(ac2)
ac4.start_io()
ac4._evtracker.wait_all_initial_fetches()
lp.sec("ac1: creating group chat with 2 other members")
chat = ac1.create_group_chat("title", contacts=[ac2, ac3])
@@ -1718,276 +1542,6 @@ 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
def test_delete_multiple_messages(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat12 = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: sending seven messages")
texts = ["first", "second", "third", "fourth", "fifth", "sixth", "seventh"]
for text in texts:
chat12.send_text(text)
lp.sec("ac2: waiting for all messages on the other side")
to_delete = []
for text in texts:
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text in texts
if text != "third":
to_delete.append(msg)
lp.sec("ac2: deleting all messages except third")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
for msg in to_delete:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
lp.sec("imap2: test that only one message is left")
imap2 = ac2.direct_imap
assert len(imap2.get_all_messages()) == 1
def test_configure_error_msgs(self, acfactory):
ac1, configdict = acfactory.get_online_config()
ac1.update_config(configdict)
ac1.set_config("mail_pw", "abc") # Wrong mail pw
ac1.configure()
while True:
ev = ac1._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if ev.data1 == 0:
break
# Password is wrong so it definitely has to say something about "password"
assert "password" in ev.data2
ac2, configdict = acfactory.get_online_config()
ac2.update_config(configdict)
ac2.set_config("addr", "abc@def.invalid") # mail server can't be reached
ac2.configure()
while True:
ev = ac2._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if ev.data1 == 0:
break
# Can't connect so it probably should say something about "internet"
# again, should not repeat itself
# If this fails then probably `e.msg.to_lowercase().contains("could not resolve")`
# in configure/mod.rs returned false because the error message was changed
# (i.e. did not contain "could not resolve" anymore)
assert (ev.data2.count("internet") + ev.data2.count("network")) == 1
# Should mention that it can't connect:
assert ev.data2.count("connect") == 1
# The users do not know what "configuration" is
assert "configuration" not in ev.data2.lower()
def test_name_changes(self, acfactory):
ac1, ac2 = acfactory.get_two_online_accounts()
ac1.set_config("displayname", "Account 1")
chat12 = acfactory.get_accepted_chat(ac1, ac2)
contact = None
def update_name():
"""Send a message from ac1 to ac2 to update the name"""
nonlocal contact
chat12.send_text("Hello")
msg = ac2._evtracker.wait_next_incoming_message()
contact = msg.get_sender_contact()
return contact.name
assert update_name() == "Account 1"
ac1.set_config("displayname", "Account 1 revision 2")
assert update_name() == "Account 1 revision 2"
# Explicitly rename contact on ac2 to "Renamed"
ac2.create_contact(contact, name="Renamed")
assert contact.name == "Renamed"
ev = ac2._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
assert ev.data1 == contact.id
# ac1 also renames itself into "Renamed"
assert update_name() == "Renamed"
ac1.set_config("displayname", "Renamed")
assert update_name() == "Renamed"
# Contact name was set to "Renamed" explicitly before,
# so it should not be changed.
ac1.set_config("displayname", "Renamed again")
updated_name = update_name()
if updated_name == "Renamed again":
# Known bug, mark as XFAIL
pytest.xfail("Contact was renamed after explicit rename")
else:
# No renames should happen after explicit rename
assert updated_name == "Renamed"
def test_group_quote(self, acfactory, lp):
"""Test quoting in a group with a new member who have not seen the quoted message."""
ac1, ac2, ac3 = accounts = acfactory.get_many_online_accounts(3)
acfactory.introduce_each_other(accounts)
chat = ac1.create_group_chat(name="quote group")
chat.add_contact(ac2)
lp.sec("ac1: sending message")
out_msg = chat.send_text("hello")
lp.sec("ac2: receiving message")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
chat.add_contact(ac3)
ac2._evtracker.wait_next_incoming_message()
ac3._evtracker.wait_next_incoming_message()
lp.sec("ac2: sending reply with a quote")
reply_msg = Message.new_empty(msg.chat.account, "text")
reply_msg.set_text("reply")
reply_msg.quote = msg
reply_msg = msg.chat.prepare_message(reply_msg)
assert reply_msg.quoted_text == "hello"
msg.chat.send_prepared(reply_msg)
lp.sec("ac3: receiving reply")
received_reply = ac3._evtracker.wait_next_incoming_message()
assert received_reply.text == "reply"
assert received_reply.quoted_text == "hello"
# ac3 was not in the group and has not received quoted message
assert received_reply.quote is None
lp.sec("ac1: receiving reply")
received_reply = ac1._evtracker.wait_next_incoming_message()
assert received_reply.text == "reply"
assert received_reply.quoted_text == "hello"
assert received_reply.quote.id == out_msg.id
@pytest.mark.parametrize("mvbox_move", [False, True])
def test_add_all_recipients_as_contacts(self, acfactory, lp, mvbox_move):
"""Delta Chat reads the recipients from old emails sent by the user and adds them as contacts.
This way, we can already offer them some email addresses they can write to.
Also test that existing emails are fetched during onboarding.
Lastly, tests that bcc_self messages moved to the mvbox are marked as read."""
ac1 = acfactory.get_online_configuring_account(mvbox=mvbox_move, move=mvbox_move)
ac2 = acfactory.get_online_configuring_account()
acfactory.wait_configure_and_start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("send out message with bcc to ourselves")
if mvbox_move:
ac1.direct_imap.select_config_folder("mvbox")
ac1.direct_imap.idle_start()
ac1.set_config("bcc_self", "1")
chat.send_text("message text")
# now wait until the bcc_self message arrives
# Also test that bcc_self messages moved to the mvbox are marked as read.
assert ac1.direct_imap.idle_wait_for_seen()
ac1_clone = acfactory.clone_online_account(ac1)
ac1_clone._configtracker.wait_finish()
ac1_clone.start_io()
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
ac2_addr = ac2.get_config("addr")
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())
msg = ac1_clone._evtracker.wait_next_messages_changed()
assert msg.text == "message text"
class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):
@@ -2113,7 +1667,8 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "cannot login" in ev.data2.lower()
def test_invalid_user(self, acfactory):
ac1, configdict = acfactory.get_online_config()
@@ -2121,7 +1676,8 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "cannot login" in ev.data2.lower()
def test_invalid_domain(self, acfactory):
ac1, configdict = acfactory.get_online_config()
@@ -2129,4 +1685,5 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "could not connect" in ev.data2.lower()

View File

@@ -7,7 +7,7 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS

View File

@@ -1 +1 @@
1.45.0
1.43.1

View File

@@ -5,27 +5,18 @@ import sys
import re
import pathlib
import subprocess
from argparse import ArgumentParser
rex = re.compile(r'version = "(\S+)"')
def regex_matches(relpath, regex=rex):
def read_toml_version(relpath):
p = pathlib.Path(relpath)
assert p.exists()
for line in open(str(p)):
m = regex.match(line)
m = rex.match(line)
if m is not None:
return m
def read_toml_version(relpath):
res = regex_matches(relpath, rex)
if res is not None:
return res.group(1)
return m.group(1)
raise ValueError("no version found in {}".format(relpath))
def replace_toml_version(relpath, newversion):
p = pathlib.Path(relpath)
assert p.exists()
@@ -34,28 +25,18 @@ def replace_toml_version(relpath, newversion):
for line in open(str(p)):
m = rex.match(line)
if m is not None:
print("{}: set version={}".format(relpath, newversion))
f.write('version = "{}"\n'.format(newversion))
else:
f.write(line)
os.rename(tmp_path, str(p))
if __name__ == "__main__":
def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml"]
try:
opts = parser.parse_args()
except SystemExit:
print()
for x in toml_list:
if len(sys.argv) < 2:
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
print("{}: {}".format(x, read_toml_version(x)))
print()
raise SystemExit("need argument: new version, example: 1.25.0")
newversion = opts.newversion
newversion = sys.argv[1]
if newversion.count(".") < 2:
raise SystemExit("need at least two dots in version")
@@ -74,10 +55,7 @@ def 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"])
@@ -85,8 +63,3 @@ def main():
print("")
print(" git tag {}".format(newversion))
print("")
if __name__ == "__main__":
main()

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::error::Error;
use crate::events::EventType;
use crate::events::Event;
use crate::message;
/// Represents a file in the blob directory.
@@ -67,7 +67,7 @@ impl<'a> BlobObject<'a> {
blobdir,
name: format!("$BLOBDIR/{}", name),
};
context.emit_event(EventType::NewBlobFile(blob.as_name().to_string()));
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
@@ -155,7 +155,7 @@ impl<'a> BlobObject<'a> {
blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{}", name),
};
context.emit_event(EventType::NewBlobFile(blob.as_name().to_string()));
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
@@ -168,9 +168,6 @@ impl<'a> BlobObject<'a> {
/// subdirectory is used and [BlobObject::sanitise_name] does not
/// modify the filename.
///
/// Paths into the blob directory may be either defined by an absolute path
/// or by the relative prefix `$BLOBDIR`.
///
/// # Errors
///
/// This merely delegates to the [BlobObject::create_and_copy] and
@@ -182,11 +179,6 @@ impl<'a> BlobObject<'a> {
) -> std::result::Result<BlobObject<'_>, BlobError> {
if src.as_ref().starts_with(context.get_blobdir()) {
BlobObject::from_path(context, src)
} else if src.as_ref().starts_with("$BLOBDIR/") {
BlobObject::from_name(
context,
src.as_ref().to_str().unwrap_or_default().to_string(),
)
} else {
BlobObject::create_and_copy(context, src).await
}
@@ -685,7 +677,7 @@ mod tests {
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
assert!(stem.contains("foo"));
assert!(!stem.contains('?'));
assert!(!stem.contains("?"));
assert_eq!(ext, ".bar");
let (stem, ext) = BlobObject::sanitise_name("no-extension");
@@ -698,10 +690,10 @@ mod tests {
assert!(!stem.contains("ignored"));
assert!(stem.contains("this"));
assert!(stem.contains("forbidden"));
assert!(!stem.contains('/'));
assert!(!stem.contains('\\'));
assert!(!stem.contains(':'));
assert!(!stem.contains('*'));
assert!(!stem.contains('?'));
assert!(!stem.contains("/"));
assert!(!stem.contains("\\"));
assert!(!stem.contains(":"));
assert!(!stem.contains("*"));
assert!(!stem.contains("?"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ 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};
@@ -77,7 +76,7 @@ impl Chatlist {
/// chats
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
/// and hides the device-chat,
/// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
@@ -100,23 +99,12 @@ impl Chatlist {
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
// messages get deleted to avoid reloading the same chatlist.
if let Err(err) = delete_expired_messages(context).await {
if let Err(err) = delete_device_expired_messages(context).await {
warn!(context, "Failed to hide expired messages: {}", err);
}
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
@@ -143,7 +131,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_map(
context.sql.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -152,16 +140,14 @@ impl Chatlist {
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1)
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
process_row,
process_rows,
ORDER BY c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsx![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
).await?
} else if flag_archived_only {
// show archived chats
@@ -170,7 +156,7 @@ impl Chatlist {
// and adapting the number requires larger refactorings and seems not to be worth the effort)
context
.sql
.query_map(
.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -186,9 +172,7 @@ impl Chatlist {
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft],
process_row,
process_rows,
paramsx![MessageState::OutDraft],
)
.await?
} else if let Some(query) = query {
@@ -204,7 +188,7 @@ impl Chatlist {
let str_like_cmd = format!("%{}%", query);
context
.sql
.query_map(
.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -220,9 +204,7 @@ impl Chatlist {
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
process_row,
process_rows,
paramsx![MessageState::OutDraft, skip_id, str_like_cmd],
)
.await?
} else {
@@ -235,7 +217,7 @@ impl Chatlist {
} else {
ChatId::new(0)
};
let mut ids = context.sql.query_map(
let mut ids = context.sql.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -251,10 +233,9 @@ 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;",
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
process_row,
process_rows,
paramsx![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
).await?;
if !flag_no_specials {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
{
@@ -329,30 +310,20 @@ impl Chatlist {
// This is because we may want to display drafts here or stuff as
// "is typing".
// Also, sth. as "No messages" would not work if the summary comes from a message.
let mut ret = Lot::new();
let (chat_id, lastmsg_id) = match self.ids.get(index) {
Some(ids) => ids,
None => {
let mut ret = Lot::new();
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return Lot::new();
return ret;
}
};
Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
}
pub async fn get_summary2(
context: &Context,
chat_id: ChatId,
lastmsg_id: MsgId,
chat: Option<&Chat>,
) -> Lot {
let mut ret = Lot::new();
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
} else if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
} else if let Ok(chat) = Chat::load_from_db(context, *chat_id).await {
chat_loaded = chat;
&chat_loaded
} else {
@@ -361,8 +332,10 @@ impl Chatlist {
let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
if lastmsg.from_id != DC_CONTACT_ID_SELF && chat.typ == Chattype::Group {
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id).await {
if lastmsg.from_id != DC_CONTACT_ID_SELF
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
}
@@ -396,15 +369,15 @@ impl Chatlist {
/// Returns the number of archived chats
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
context
let v: i32 = context
.sql
.query_get_value(
context,
.query_value(
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
paramsv![],
paramsx![],
)
.await
.unwrap_or_default()
.unwrap_or_default();
v as u32
}
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
@@ -412,21 +385,21 @@ async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
// sufficient as there are typically only few fresh messages.
context
.sql
.query_get_value(
context,
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN chats c",
" ON c.id=m.chat_id",
" WHERE m.state=10",
" AND m.hidden=0",
" AND c.blocked=2",
" ORDER BY m.timestamp DESC, m.id DESC;"
),
paramsv![],
.query_value(
r#"
SELECT m.id
FROM msgs m
LEFT JOIN chats c
ON c.id=m.chat_id
WHERE m.state=10
AND m.hidden=0
AND c.blocked=2
ORDER BY m.timestamp DESC, m.id DESC;
"#,
paramsx![],
)
.await
.ok()
}
#[cfg(test)]
@@ -438,13 +411,13 @@ mod tests {
#[async_std::test]
async fn test_try_load() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
let chat_id2 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "b chat")
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "c chat")
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
.await
.unwrap();
@@ -487,7 +460,7 @@ mod tests {
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new().await;
t.ctx.update_device_chats().await.unwrap();
create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
@@ -544,7 +517,7 @@ mod tests {
#[async_std::test]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();

View File

@@ -8,8 +8,7 @@ use crate::chat::ChatId;
use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::events::EventType;
use crate::job;
use crate::events::Event;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage;
@@ -25,13 +24,11 @@ pub enum Config {
MailUser,
MailPw,
MailPort,
MailSecurity,
ImapCertificateChecks,
SendServer,
SendUser,
SendPw,
SendPort,
SendSecurity,
SmtpCertificateChecks,
ServerFlags,
@@ -69,9 +66,6 @@ pub enum Config {
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
#[strum(props(default = "1"))]
FetchExisting,
#[strum(props(default = "0"))]
KeyGenType,
@@ -123,34 +117,24 @@ pub enum Config {
#[strum(serialize = "sys.config_keys")]
SysConfigKeys,
Bot,
/// Whether we send a warning if the password is wrong (set to false when we send a warning
/// because we do not want to send a second warning)
#[strum(props(default = "0"))]
NotifyAboutWrongPw,
/// address to webrtc instance to use for videochats
WebrtcInstance,
}
impl Context {
pub async fn config_exists(&self, key: Config) -> bool {
self.sql.get_raw_config(self, key).await.is_some()
self.sql.get_raw_config(key).await.is_some()
}
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Option<String> {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(self, key).await;
let rel_path = self.sql.get_raw_config(key).await;
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(self, key).await,
_ => self.sql.get_raw_config(key).await,
};
if value.is_some() {
@@ -205,7 +189,7 @@ impl Context {
match key {
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsx![])
.await?;
self.sql
.set_raw_config_bool(self, "attach_selfavatar", true)
@@ -234,21 +218,12 @@ impl Context {
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(self, key, value).await;
// Force chatlist reload to delete old messages immediately.
self.emit_event(EventType::MsgsChanged {
self.emit_event(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
ret
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(self, key, value.as_deref()).await
}
Config::DeleteServerAfter => {
let ret = self.sql.set_raw_config(self, key, value).await;
job::schedule_resync(self).await;
ret
}
_ => self.sql.set_raw_config(self, key, value).await,
}
}

View File

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

View File

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

View File

@@ -1,54 +1,43 @@
//! Email accounts autoconfiguration process module
#![forbid(clippy::indexing_slicing)]
mod auto_mozilla;
mod auto_outlook;
mod read_url;
mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use anyhow::{bail, ensure, format_err, Context as _, Result};
use async_std::prelude::*;
use async_std::task;
use itertools::Itertools;
use job::Action;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::imap::Imap;
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::login_param::{CertificateChecks, LoginParam};
use crate::message::Message;
use crate::oauth2::*;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::stock::StockMessage;
use crate::{chat, e2ee, provider};
use crate::{constants::*, job};
use crate::{context::Context, param::Params};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use server_params::{expand_param_vector, ServerParams};
macro_rules! progress {
($context:tt, $progress:expr, $comment:expr) => {
($context:tt, $progress:expr) => {
assert!(
$progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
$context.emit_event($crate::events::EventType::ConfigureProgress {
progress: $progress,
comment: $comment,
});
};
($context:tt, $progress:expr) => {
progress!($context, $progress, None);
$context.emit_event($crate::events::Event::ConfigureProgress($progress));
};
}
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured").await
self.sql.get_raw_config_bool("configured").await
}
/// Configures this account with the currently set parameters.
@@ -83,7 +72,6 @@ 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(&param.addr) {
if let Some(config_defaults) = &provider.config_defaults {
@@ -114,24 +102,12 @@ impl Context {
match success {
Ok(_) => {
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
.await?;
progress!(self, 1000);
Ok(())
}
Err(err) => {
progress!(
self,
0,
Some(
self.stock_string_repl_str(
StockMessage::ConfigurationFailed,
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
format!("{:#}", err),
)
.await
)
);
error!(self, "Configure Failed: {}", err);
progress!(self, 0);
Err(err)
}
}
@@ -139,40 +115,20 @@ impl Context {
}
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let mut param_autoconfig: Option<LoginParam> = None;
let mut keep_flags = 0;
// Read login parameters from the database
progress!(ctx, 1);
// Check basic settings.
ensure!(!param.addr.is_empty(), "Please enter an email address.");
// Only check for IMAP password, SMTP password is an "advanced" setting.
ensure!(!param.imap.password.is_empty(), "Please enter a password.");
if param.smtp.password.is_empty() {
param.smtp.password = param.imap.password.clone()
}
// Normalize authentication flags.
let oauth2 = match param.server_flags & DC_LP_AUTH_FLAGS as i32 {
DC_LP_AUTH_OAUTH2 => true,
DC_LP_AUTH_NORMAL => false,
_ => false,
};
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
param.server_flags |= if oauth2 {
DC_LP_AUTH_OAUTH2 as i32
} else {
DC_LP_AUTH_NORMAL as i32
};
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
// Step 1: Load the parameters and check email-address and password
if oauth2 {
if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 {
// the used oauth2 addr may differ, check this.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.imap.password)
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.mail_pw)
.await
.and_then(|e| e.parse().ok())
{
@@ -193,141 +149,130 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// Step 2: Autoconfig
progress!(ctx, 200);
let param_autoconfig;
if param.imap.server.is_empty()
&& param.imap.port == 0
&& param.imap.security == Socket::Automatic
&& param.imap.user.is_empty()
&& param.smtp.server.is_empty()
&& param.smtp.port == 0
&& param.smtp.security == Socket::Automatic
&& param.smtp.user.is_empty()
// param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then
// param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for
// autoconfig or not
if param.mail_server.is_empty()
&& param.mail_port == 0
&& param.send_server.is_empty()
&& param.send_port == 0
&& param.send_user.is_empty()
&& (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0
{
// no advanced parameters entered by the user: query provider-database or do Autoconfig
keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2;
if let Some(new_param) = get_offline_autoconfig(ctx, &param) {
// got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting
param_autoconfig = Some(new_param);
}
if let Some(servers) = get_offline_autoconfig(ctx, &param.addr) {
param_autoconfig = Some(servers);
} else {
if param_autoconfig.is_none() {
param_autoconfig =
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await;
}
} else {
param_autoconfig = None;
}
// C. Do we have any autoconfig result?
progress!(ctx, 500);
if let Some(ref cfg) = param_autoconfig {
info!(ctx, "Got autoconfig: {}", &cfg);
if !cfg.mail_user.is_empty() {
param.mail_user = cfg.mail_user.clone();
}
// all other values are always NULL when entering autoconfig
param.mail_server = cfg.mail_server.clone();
param.mail_port = cfg.mail_port;
param.send_server = cfg.send_server.clone();
param.send_port = cfg.send_port;
param.send_user = cfg.send_user.clone();
param.server_flags = cfg.server_flags;
// although param_autoconfig's data are no longer needed from,
// it is used to later to prevent trying variations of port/server/logins
}
param.server_flags |= keep_flags;
let servers = expand_param_vector(
param_autoconfig.unwrap_or_else(|| {
vec![
ServerParams {
protocol: Protocol::IMAP,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
},
ServerParams {
protocol: Protocol::SMTP,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
},
]
}),
&param.addr,
&param_domain,
// Step 3: Fill missing fields with defaults
if param.mail_server.is_empty() {
param.mail_server = format!("imap.{}", param_domain,)
}
if param.mail_port == 0 {
param.mail_port = if 0 != param.server_flags & (0x100 | 0x400) {
143
} else {
993
}
}
if param.mail_user.is_empty() {
param.mail_user = param.addr.clone();
}
if param.send_server.is_empty() && !param.mail_server.is_empty() {
param.send_server = param.mail_server.clone();
if param.send_server.starts_with("imap.") {
param.send_server = param.send_server.replacen("imap", "smtp", 1);
}
}
if param.send_port == 0 {
param.send_port = if 0 != param.server_flags & DC_LP_SMTP_SOCKET_STARTTLS as i32 {
587
} else if 0 != param.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 {
25
} else {
465
}
}
if param.send_user.is_empty() && !param.mail_user.is_empty() {
param.send_user = param.mail_user.clone();
}
if param.send_pw.is_empty() && !param.mail_pw.is_empty() {
param.send_pw = param.mail_pw.clone()
}
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_AUTH_FLAGS as i32) {
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
param.server_flags |= DC_LP_AUTH_NORMAL as i32
}
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_IMAP_SOCKET_FLAGS as i32) {
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
param.server_flags |= if param.send_port == 143 {
DC_LP_IMAP_SOCKET_STARTTLS as i32
} else {
DC_LP_IMAP_SOCKET_SSL as i32
}
}
if !dc_exactly_one_bit_set(param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32)) {
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= if param.send_port == 587 {
DC_LP_SMTP_SOCKET_STARTTLS as i32
} else if param.send_port == 25 {
DC_LP_SMTP_SOCKET_PLAIN as i32
} else {
DC_LP_SMTP_SOCKET_SSL as i32
}
}
// do we have a complete configuration?
ensure!(
!param.mail_server.is_empty()
&& param.mail_port != 0
&& !param.mail_user.is_empty()
&& !param.mail_pw.is_empty()
&& !param.send_server.is_empty()
&& param.send_port != 0
&& !param.send_user.is_empty()
&& !param.send_pw.is_empty()
&& param.server_flags != 0,
"Account settings incomplete."
);
progress!(ctx, 550);
// Spawn SMTP configuration task
let mut smtp = Smtp::new();
let context_smtp = ctx.clone();
let mut smtp_param = param.smtp.clone();
let smtp_addr = param.addr.clone();
let smtp_servers: Vec<ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::SMTP)
.cloned()
.collect();
let smtp_config_task = task::spawn(async move {
let mut smtp_configured = false;
let mut errors = Vec::new();
for smtp_server in smtp_servers {
smtp_param.user = smtp_server.username.clone();
smtp_param.server = smtp_server.hostname.clone();
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
match try_smtp_one_param(&context_smtp, &smtp_param, &smtp_addr, oauth2, &mut smtp)
.await
{
Ok(_) => {
smtp_configured = true;
break;
}
Err(e) => errors.push(e),
}
}
if smtp_configured {
Ok(smtp_param)
} else {
Err(errors)
}
});
progress!(ctx, 600);
// Configure IMAP
// try to connect to IMAP - if we did not got an autoconfig,
// do some further tries with different settings and username variations
let (_s, r) = async_std::sync::channel(1);
let mut imap = Imap::new(r);
let mut imap_configured = false;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::IMAP)
.collect();
let imap_servers_count = imap_servers.len();
let mut errors = Vec::new();
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
param.imap.user = imap_server.username.clone();
param.imap.server = imap_server.hostname.clone();
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
match try_imap_one_param(ctx, &param.imap, &param.addr, oauth2, &mut imap).await {
Ok(_) => {
imap_configured = true;
break;
}
Err(e) => errors.push(e),
}
progress!(
ctx,
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
if !imap_configured {
bail!(nicer_configuration_error(ctx, errors).await);
}
progress!(ctx, 850);
// Wait for SMTP configuration
match smtp_config_task.await {
Ok(smtp_param) => {
param.smtp = smtp_param;
}
Err(errors) => {
bail!(nicer_configuration_error(ctx, errors).await);
}
}
try_imap_connections(ctx, param, param_autoconfig.is_some(), &mut imap).await?;
progress!(ctx, 800);
try_smtp_connections(ctx, param, param_autoconfig.is_some()).await?;
progress!(ctx, 900);
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
@@ -353,14 +298,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
e2ee::ensure_secret_key_exists(ctx).await?;
info!(ctx, "key generation completed");
job::add(
ctx,
job::Job::new(Action::FetchExistingMsgs, 0, Params::new(), 0),
)
.await;
progress!(ctx, 940);
update_device_chats_handle.await?;
Ok(())
}
@@ -403,8 +341,8 @@ impl AutoconfigSource {
AutoconfigSource {
provider: AutoconfigProvider::Outlook,
url: format!(
"https://autodiscover.{}/autodiscover/autodiscover.xml",
domain
"https://{}{}/autodiscover/autodiscover.xml",
"autodiscover.", domain
),
},
// always SSL for Thunderbird's database
@@ -415,10 +353,10 @@ impl AutoconfigSource {
]
}
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<Vec<ServerParams>> {
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<LoginParam> {
let params = match self.provider {
AutoconfigProvider::Mozilla => moz_autoconfigure(ctx, &self.url, &param).await?,
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url).await?,
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url, &param).await?,
};
Ok(params)
@@ -434,7 +372,7 @@ async fn get_autoconfig(
param: &LoginParam,
param_domain: &str,
param_addr_urlencoded: &str,
) -> Option<Vec<ServerParams>> {
) -> Option<LoginParam> {
let sources = AutoconfigSource::all(param_domain, param_addr_urlencoded);
let mut progress = 300;
@@ -450,134 +388,212 @@ async fn get_autoconfig(
None
}
fn get_offline_autoconfig(context: &Context, addr: &str) -> Option<Vec<ServerParams>> {
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&addr) {
if let Some(provider) = provider::get_provider_info(&param.addr) {
match provider.status {
provider::Status::OK | provider::Status::PREPARATION => {
if provider.server.is_empty() {
info!(context, "offline autoconfig found, but no servers defined");
None
} else {
info!(context, "offline autoconfig found");
let servers = provider
.server
.iter()
.map(|s| ServerParams {
protocol: s.protocol,
socket: s.socket,
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::EMAIL => addr.to_string(),
UsernamePattern::EMAILLOCALPART => {
if let Some(at) = addr.find('@') {
addr.split_at(at).0.to_string()
} else {
addr.to_string()
}
}
},
})
.collect();
Some(servers)
let imap = provider.get_imap_server();
let smtp = provider.get_smtp_server();
// clippy complains about these is_some()/unwrap() settings,
// however, rewriting the code to "if let" would make things less obvious,
// esp. if we allow more combinations of servers (pop, jmap).
// therefore, #[allow(clippy::unnecessary_unwrap)] is added above.
if let Some(imap) = imap {
if let Some(smtp) = smtp {
let mut p = LoginParam::new();
p.addr = param.addr.clone();
p.mail_server = imap.hostname.to_string();
p.mail_user = imap.apply_username_pattern(param.addr.clone());
p.mail_port = imap.port as i32;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match imap.socket {
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
};
p.send_server = smtp.hostname.to_string();
p.send_user = smtp.apply_username_pattern(param.addr.clone());
p.send_port = smtp.port as i32;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match smtp.socket {
provider::Socket::STARTTLS => DC_LP_SMTP_SOCKET_STARTTLS as i32,
provider::Socket::SSL => DC_LP_SMTP_SOCKET_SSL as i32,
};
info!(context, "offline autoconfig found: {}", p);
return Some(p);
}
}
info!(context, "offline autoconfig found, but no servers defined");
return None;
}
provider::Status::BROKEN => {
info!(context, "offline autoconfig found, provider is broken");
None
return None;
}
}
} else {
info!(context, "no offline autoconfig found");
None
}
info!(context, "no offline autoconfig found");
None
}
async fn try_imap_one_param(
async fn try_imap_connections(
context: &Context,
param: &ServerLoginParam,
addr: &str,
oauth2: bool,
param: &mut LoginParam,
was_autoconfig: bool,
imap: &mut Imap,
) -> Result<(), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
) -> Result<()> {
// manually_set_param is used to check whether a particular setting was set manually by the user.
// If yes, we do not want to change it to avoid confusing error messages
// (you set port 443, but the app tells you it couldn't connect on port 993).
let manually_set_param = LoginParam::from_database(context, "").await;
if let Err(err) = imap.connect(context, param, addr, oauth2).await {
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
info!(context, "success: {}", inf);
Ok(())
}
}
async fn try_smtp_one_param(
context: &Context,
param: &ServerLoginParam,
addr: &str,
oauth2: bool,
smtp: &mut Smtp,
) -> Result<(), ConfigurationError> {
let inf = format!(
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
if let Err(err) = smtp.connect(context, param, addr, oauth2).await {
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
info!(context, "success: {}", inf);
smtp.disconnect().await;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[error("Trying {config}…\nError: {msg}")]
pub struct ConfigurationError {
config: String,
msg: String,
}
async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationError>) -> String {
let first_err = if let Some(f) = errors.first() {
f
} else {
return "".to_string();
};
if errors
.iter()
.all(|e| e.msg.to_lowercase().contains("could not resolve"))
// progress 650 and 660
if try_imap_connection(context, param, &manually_set_param, was_autoconfig, 0, imap)
.await
.is_ok()
{
return context
.stock_str(StockMessage::ErrorNoNetwork)
.await
.to_string();
return Ok(()); // we directly return here if it was autoconfig or the connection succeeded
}
if errors.iter().all(|e| e.msg == first_err.msg) {
return first_err.msg.to_string();
progress!(context, 670);
// try_imap_connection() changed the flags and port. Change them back:
if manually_set_param.server_flags & DC_LP_IMAP_SOCKET_FLAGS == 0 {
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
param.server_flags |= DC_LP_IMAP_SOCKET_SSL;
}
if manually_set_param.mail_port == 0 {
param.mail_port = 993;
}
if let Some(at) = param.mail_user.find('@') {
param.mail_user = param.mail_user.split_at(at).0.to_string();
}
if let Some(at) = param.send_user.find('@') {
param.send_user = param.send_user.split_at(at).0.to_string();
}
// progress 680 and 690
try_imap_connection(context, param, &manually_set_param, was_autoconfig, 1, imap).await
}
async fn try_imap_connection(
context: &Context,
param: &mut LoginParam,
manually_set_param: &LoginParam,
was_autoconfig: bool,
variation: usize,
imap: &mut Imap,
) -> Result<()> {
if try_imap_one_param(context, param, imap).await.is_ok() {
return Ok(());
}
if was_autoconfig {
return Ok(());
}
errors.iter().map(|e| e.to_string()).join("\n\n")
progress!(context, 650 + variation * 30);
if manually_set_param.server_flags & DC_LP_IMAP_SOCKET_FLAGS == 0 {
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS;
if try_imap_one_param(context, &param, imap).await.is_ok() {
return Ok(());
}
}
progress!(context, 660 + variation * 30);
if manually_set_param.mail_port == 0 {
param.mail_port = 143;
try_imap_one_param(context, param, imap).await
} else {
Err(format_err!("no more possible configs"))
}
}
async fn try_imap_one_param(context: &Context, param: &LoginParam, imap: &mut Imap) -> Result<()> {
let inf = format!(
"imap: {}@{}:{} flags=0x{:x} certificate_checks={}",
param.mail_user,
param.mail_server,
param.mail_port,
param.server_flags,
param.imap_certificate_checks
);
info!(context, "Trying: {}", inf);
if imap.connect(context, &param).await {
info!(context, "success: {}", inf);
return Ok(());
}
bail!("Could not connect: {}", inf);
}
async fn try_smtp_connections(
context: &Context,
param: &mut LoginParam,
was_autoconfig: bool,
) -> Result<()> {
// manually_set_param is used to check whether a particular setting was set manually by the user.
// If yes, we do not want to change it to avoid confusing error messages
// (you set port 443, but the app tells you it couldn't connect on port 993).
let manually_set_param = LoginParam::from_database(context, "").await;
let mut smtp = Smtp::new();
// try to connect to SMTP - if we did not got an autoconfig, the first try was SSL-465 and we do
// a second try with STARTTLS-587
if try_smtp_one_param(context, param, &mut smtp).await.is_ok() {
return Ok(());
}
if was_autoconfig {
return Ok(());
}
progress!(context, 850);
if manually_set_param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32) == 0 {
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
}
if manually_set_param.send_port == 0 {
param.send_port = 587;
}
if try_smtp_one_param(context, param, &mut smtp).await.is_ok() {
return Ok(());
}
progress!(context, 860);
if manually_set_param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32) == 0 {
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
}
if manually_set_param.send_port == 0 {
param.send_port = 25;
}
try_smtp_one_param(context, param, &mut smtp).await
}
async fn try_smtp_one_param(context: &Context, param: &LoginParam, smtp: &mut Smtp) -> Result<()> {
let inf = format!(
"smtp: {}@{}:{} flags: 0x{:x}",
param.send_user, param.send_server, param.send_port, param.server_flags
);
info!(context, "Trying: {}", inf);
if let Err(err) = smtp.connect(context, &param).await {
bail!("could not connect: {}", err);
}
info!(context, "success: {}", inf);
smtp.disconnect().await;
Ok(())
}
#[derive(Debug, thiserror::Error)]
@@ -585,14 +601,17 @@ pub enum Error {
#[error("Invalid email address: {0:?}")]
InvalidEmailAddress(String),
#[error("XML error at position {position}: {error}")]
#[error("XML error at position {position}")]
InvalidXml {
position: usize,
#[source]
error: quick_xml::Error,
},
#[error("Failed to get URL: {0}")]
#[error("Bad or incomplete autoconfig")]
IncompleteAutoconfig(LoginParam),
#[error("Failed to get URL")]
ReadUrlError(#[from] self::read_url::Error),
#[error("Number of redirection is exceeded")]
@@ -601,7 +620,6 @@ pub enum Error {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::*;
@@ -625,15 +643,14 @@ mod tests {
async fn test_get_offline_autoconfig() {
let context = TestContext::new().await.ctx;
let addr = "someone123@example.org";
assert!(get_offline_autoconfig(&context, addr).is_none());
let mut params = LoginParam::new();
params.addr = "someone123@example.org".to_string();
assert!(get_offline_autoconfig(&context, &params).is_none());
let addr = "someone123@nauta.cu";
let found_params = get_offline_autoconfig(&context, addr).unwrap();
assert_eq!(found_params.len(), 2);
assert_eq!(found_params[0].protocol, Protocol::IMAP);
assert_eq!(found_params[0].hostname, "imap.nauta.cu".to_string());
assert_eq!(found_params[1].protocol, Protocol::SMTP);
assert_eq!(found_params[1].hostname, "smtp.nauta.cu".to_string());
let mut params = LoginParam::new();
params.addr = "someone123@nauta.cu".to_string();
let found_params = get_offline_autoconfig(&context, &params).unwrap();
assert_eq!(found_params.mail_server, "imap.nauta.cu".to_string());
assert_eq!(found_params.send_server, "smtp.nauta.cu".to_string());
}
}

View File

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

View File

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

View File

@@ -1,9 +1,19 @@
//! # Constants
use deltachat_derive::*;
use once_cell::sync::Lazy;
#![allow(dead_code)]
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
}
// some defaults
const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
const DC_INBOX_WATCH_DEFAULT: i32 = 1;
const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
#[derive(
Debug,
@@ -14,12 +24,11 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(u8)]
#[repr(i32)]
pub enum Blocked {
Not = 0,
Manually = 1,
@@ -32,8 +41,8 @@ impl Default for Blocked {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(i32)]
pub enum ShowEmails {
Off = 0,
AcceptedContacts = 1,
@@ -46,8 +55,8 @@ impl Default for ShowEmails {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(i32)]
pub enum MediaQuality {
Balanced = 0,
Worse = 1,
@@ -59,7 +68,7 @@ impl Default for MediaQuality {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(u8)]
pub enum KeyGenType {
Default = 0,
@@ -73,20 +82,6 @@ impl Default for KeyGenType {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(i8)]
pub enum VideochatType {
Unknown = 0,
BasicWebrtc = 1,
Jitsi = 2,
}
impl Default for VideochatType {
fn default() -> Self {
VideochatType::Unknown
}
}
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
@@ -106,16 +101,14 @@ pub const DC_GCL_ADD_SELF: usize = 0x02;
// unchanged user avatars are resent to the recipients every some days
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
// warn about an outdated app after a given number of days.
// as we use the "provider-db generation date" as reference (that might not be updated very often)
// and as not all system get speedy updates,
// do not use too small value that will annoy users checking for nonexistant updates.
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub const DC_CHAT_ID_DEADDROP: u32 = 1;
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
pub const DC_CHAT_ID_TRASH: u32 = 3;
/// a message is just in creation but not yet assigned to a chat (eg. we may need the message ID to set up blobs; this avoids unready message to be sent and shown)
const DC_CHAT_ID_MSGS_IN_CREATION: u32 = 4;
/// virtual chat showing all messages flagged with msgs.starred=2
pub const DC_CHAT_ID_STARRED: u32 = 5;
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6;
/// only an indicator in a chatlist
@@ -132,17 +125,17 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(u32)]
#[repr(i32)]
pub enum Chattype {
Undefined = 0,
Single = 100,
Group = 120,
VerifiedGroup = 130,
}
impl Default for Chattype {
@@ -156,9 +149,9 @@ pub const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// approx. max. length returned by dc_msg_get_text()
pub const DC_MAX_GET_TEXT_LEN: usize = 30000;
const DC_MAX_GET_TEXT_LEN: usize = 30000;
/// approx. max. length returned by dc_get_msg_info()
pub const DC_MAX_GET_INFO_LEN: usize = 100_000;
const DC_MAX_GET_INFO_LEN: usize = 100_000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
@@ -189,11 +182,44 @@ pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
/// Connect to IMAP via STARTTLS.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_STARTTLS: i32 = 0x100;
/// Connect to IMAP via SSL.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_SSL: i32 = 0x200;
/// Connect to IMAP unencrypted, this should not be used.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_PLAIN: i32 = 0x400;
/// Connect to SMTP via STARTTLS.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_SMTP_SOCKET_STARTTLS: usize = 0x10000;
/// Connect to SMTP via SSL.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
/// Connect to SMTP unencrypted, this should not be used.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
/// if none of these flags are set, the default is chosen
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
/// if none of these flags are set, the default is chosen
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN;
/// if none of these flags are set, the default is chosen
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN;
/// How many existing messages shall be fetched after configuration.
pub const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
// QR code scanning (view from Bob, the joiner)
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
pub const DC_BOB_ERROR: i32 = 0;
pub const DC_BOB_SUCCESS: i32 = 1;
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
@@ -214,10 +240,9 @@ pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(i32)]
pub enum Viewtype {
@@ -267,9 +292,6 @@ pub enum Viewtype {
/// The file is set via dc_msg_set_file()
/// and retrieved via dc_msg_get_file().
File = 60,
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
}
impl Default for Viewtype {
@@ -288,6 +310,64 @@ mod tests {
}
}
// These constants are used as events
// reported to the callback given to dc_context_new().
// If you do not want to handle an event, it is always safe to return 0,
// so there is no need to add a "case" for every event.
const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
// TODO: Strings need some doumentation about used placeholders.
// These constants are used to set stock translation strings
const DC_STR_NOMESSAGES: usize = 1;
const DC_STR_SELF: usize = 2;
const DC_STR_DRAFT: usize = 3;
const DC_STR_VOICEMESSAGE: usize = 7;
const DC_STR_DEADDROP: usize = 8;
const DC_STR_IMAGE: usize = 9;
const DC_STR_VIDEO: usize = 10;
const DC_STR_AUDIO: usize = 11;
const DC_STR_FILE: usize = 12;
const DC_STR_STATUSLINE: usize = 13;
const DC_STR_NEWGROUPDRAFT: usize = 14;
const DC_STR_MSGGRPNAME: usize = 15;
const DC_STR_MSGGRPIMGCHANGED: usize = 16;
const DC_STR_MSGADDMEMBER: usize = 17;
const DC_STR_MSGDELMEMBER: usize = 18;
const DC_STR_MSGGROUPLEFT: usize = 19;
const DC_STR_GIF: usize = 23;
const DC_STR_ENCRYPTEDMSG: usize = 24;
const DC_STR_E2E_AVAILABLE: usize = 25;
const DC_STR_ENCR_TRANSP: usize = 27;
const DC_STR_ENCR_NONE: usize = 28;
const DC_STR_CANTDECRYPT_MSG_BODY: usize = 29;
const DC_STR_FINGERPRINTS: usize = 30;
const DC_STR_READRCPT: usize = 31;
const DC_STR_READRCPT_MAILBODY: usize = 32;
const DC_STR_MSGGRPIMGDELETED: usize = 33;
const DC_STR_E2E_PREFERRED: usize = 34;
const DC_STR_CONTACT_VERIFIED: usize = 35;
const DC_STR_CONTACT_NOT_VERIFIED: usize = 36;
const DC_STR_CONTACT_SETUP_CHANGED: usize = 37;
const DC_STR_ARCHIVEDCHATS: usize = 40;
const DC_STR_STARREDMSGS: usize = 41;
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
const DC_STR_CANNOT_LOGIN: usize = 60;
const DC_STR_SERVER_RESPONSE: usize = 61;
const DC_STR_MSGACTIONBYUSER: usize = 62;
const DC_STR_MSGACTIONBYME: usize = 63;
const DC_STR_MSGLOCATIONENABLED: usize = 64;
const DC_STR_MSGLOCATIONDISABLED: usize = 65;
const DC_STR_LOCATION: usize = 66;
const DC_STR_STICKER: usize = 67;
const DC_STR_COUNT: usize = 67;
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]

View File

@@ -1,10 +1,14 @@
//! Contacts module
#![forbid(clippy::indexing_slicing)]
use std::convert::TryFrom;
use async_std::path::PathBuf;
use deltachat_derive::*;
use async_std::prelude::*;
use itertools::Itertools;
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
use regex::Regex;
use sqlx::Row;
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
@@ -13,14 +17,13 @@ use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::error::{bail, ensure, format_err, Result};
use crate::events::EventType;
use crate::events::Event;
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::MessageState;
use crate::message::{MessageState, MsgId};
use crate::mimeparser::AvatarAction;
use crate::param::*;
use crate::peerstate::*;
use crate::provider::Socket;
use crate::stock::StockMessage;
/// An object representing a single contact in memory.
@@ -71,7 +74,7 @@ pub struct Contact {
/// Possible origins of a contact.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, sqlx::Type,
)]
#[repr(i32)]
pub enum Origin {
@@ -132,6 +135,31 @@ 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
@@ -162,27 +190,18 @@ pub enum VerifiedStatus {
impl Contact {
pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
let mut res = context
let mut res: Contact = context
.sql
.query_row(
"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)
},
r#"
SELECT id, name, addr, origin, blocked, authname, param
FROM contacts
WHERE id=?;
"#,
paramsx![contact_id as i32],
)
.await?;
if contact_id == DC_CONTACT_ID_SELF {
res.name = context.stock_str(StockMessage::SelfMsg).await.to_string();
res.addr = context
@@ -236,7 +255,6 @@ impl Contact {
name: impl AsRef<str>,
addr: impl AsRef<str>,
) -> Result<u32> {
let name = improve_single_line_input(name);
ensure!(
!addr.as_ref().is_empty(),
"Cannot create contact with empty address"
@@ -247,12 +265,13 @@ impl Contact {
let (contact_id, sth_modified) =
Contact::add_or_lookup(context, name, addr, Origin::ManuallyCreated).await?;
let blocked = Contact::is_blocked_load(context, contact_id).await;
match sth_modified {
Modifier::None => {}
Modifier::Modified | Modifier::Created => {
context.emit_event(EventType::ContactsChanged(Some(contact_id)))
}
}
context.emit_event(Event::ContactsChanged(
if sth_modified == Modifier::Created {
Some(contact_id)
} else {
None
},
));
if blocked {
Contact::unblock(context, contact_id).await;
}
@@ -260,20 +279,27 @@ impl Contact {
Ok(contact_id)
}
/// Mark messages from a contact as noticed.
/// The contact is expected to belong to the deaddrop,
/// therefore, DC_EVENT_MSGS_NOTICED(DC_CHAT_ID_DEADDROP) is emitted.
/// Mark all messages sent by the given contact
/// as *noticed*. See also dc_marknoticed_chat() and dc_markseen_msgs()
///
/// Calling this function usually results in the event `#DC_EVENT_MSGS_CHANGED`.
pub async fn mark_noticed(context: &Context, id: u32) {
if context
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
r#"
UPDATE msgs SET state=?
WHERE from_id=? AND state=?;
"#,
paramsx![MessageState::InNoticed, id as i32, MessageState::InFresh],
)
.await
.is_ok()
{
context.emit_event(EventType::MsgsNoticed(ChatId::new(DC_CHAT_ID_DEADDROP)));
context.emit_event(Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
}
@@ -300,15 +326,15 @@ impl Contact {
if addr_cmp(addr_normalized, addr_self) {
return DC_CONTACT_ID_SELF;
}
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![
let v: i32 = context.sql.query_value(
"SELECT id FROM contacts WHERE addr=? COLLATE NOCASE AND id>? AND origin>=? AND blocked=0;",
paramsx![
addr_normalized,
DC_CONTACT_ID_LAST_SPECIAL as i32,
min_origin as u32,
min_origin as i32
],
).await.unwrap_or_default()
).await.unwrap_or_default();
v as u32
}
/// Lookup a contact and create it if it does not exist yet.
@@ -379,39 +405,32 @@ impl Contact {
let mut update_authname = false;
let mut row_id = 0;
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context.sql.query_row(
let res: Result<(i32, String, String, Origin, String), _> = context.sql.query_row(
"SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE;",
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)?;
paramsx![addr.to_string()],
)
.await;
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 {
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
{
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
} else {
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;
}
Ok((row_id, row_name, row_addr, row_origin, row_authname))
},
)
.await {
row_id = id;
row_id = id as u32;
if origin as i32 >= row_origin as i32 && addr != row_addr {
update_addr = true;
}
@@ -430,9 +449,13 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
paramsv![
new_name,
if update_addr { addr.to_string() } else { row_addr },
paramsx![
&new_name,
if update_addr {
addr.to_string()
} else {
row_addr
},
if origin > row_origin {
origin
} else {
@@ -443,7 +466,7 @@ impl Contact {
} else {
row_authname
},
row_id
row_id as i32
],
)
.await
@@ -452,20 +475,10 @@ impl Contact {
if update_name {
// Update the contact name also if it is used as a group name.
// This is one of the few duplicated data, however, getting the chat list is easier this way.
let chat_id = context.sql.query_get_value::<i32>(
context,
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
paramsv![Chattype::Single, row_id]
).await;
if let Some(chat_id) = chat_id {
match context.sql.execute("UPDATE chats SET name=? WHERE id=? AND name!=?1", paramsv![new_name, chat_id]).await {
Err(err) => warn!(context, "Can't update chat name: {}", err),
Ok(count) => if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(ChatId::new(chat_id as u32)));
}
}
}
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]
).await.ok();
}
sth_modified = Modifier::Modified;
}
@@ -478,20 +491,17 @@ impl Contact {
.sql
.execute(
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
paramsv![
name.as_ref().to_string(),
addr,
paramsx![
name.as_ref(),
&addr,
origin,
if update_authname { name.as_ref().to_string() } else { "".to_string() }
if update_authname { name.as_ref() } else { "" }
],
)
.await
.is_ok()
{
row_id = context
.sql
.get_rowid(context, "contacts", "addr", &addr)
.await?;
row_id = context.sql.get_rowid("contacts", "addr", &addr).await?;
sth_modified = Modifier::Created;
info!(context, "added contact id={} addr={}", row_id, &addr);
} else {
@@ -540,7 +550,7 @@ impl Contact {
}
}
if modify_cnt > 0 {
context.emit_event(EventType::ContactsChanged(None));
context.emit_event(Event::ContactsChanged(None));
}
Ok(modify_cnt)
@@ -578,35 +588,32 @@ impl Contact {
.map(|s| s.as_ref().to_string())
.unwrap_or_default()
);
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 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);
}
let self_name = context
.get_config(Config::Displayname)
@@ -627,17 +634,15 @@ impl Contact {
} else {
add_self = true;
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?;
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);
}
}
if flag_add_self && add_self {
@@ -648,32 +653,30 @@ impl Contact {
}
pub async fn get_blocked_cnt(context: &Context) -> usize {
context
let v: i32 = context
.sql
.query_get_value::<isize>(
context,
.query_value(
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
)
.await
.unwrap_or_default() as usize
.unwrap_or_default();
v as usize
}
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Vec<u32> {
context
.sql
.query_map(
.query_values(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(name||addr),id;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|row| row.get::<_, u32>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
)
.await
.unwrap_or_default()
.into_iter()
.map(|id: i32| id as u32)
.collect()
}
/// Returns a textual summary of the encryption state for the contact.
@@ -685,12 +688,13 @@ impl Contact {
let mut ret = String::new();
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await;
let loginparam = LoginParam::from_database(context, "configured_").await;
if peerstate.is_some()
if peerstate.is_ok()
&& peerstate
.as_ref()
.ok()
.and_then(|p| p.peek_key(PeerstateVerifiedStatus::Unverified))
.is_some()
{
@@ -735,8 +739,8 @@ impl Contact {
);
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
}
} else if loginparam.imap.security != Socket::Plain
&& loginparam.smtp.security != Socket::Plain
} else if 0 == loginparam.server_flags & DC_LP_IMAP_SOCKET_PLAIN as i32
&& 0 == loginparam.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32
{
ret += &context.stock_str(StockMessage::EncrTransp).await;
} else {
@@ -759,10 +763,9 @@ impl Contact {
let count_contacts: i32 = context
.sql
.query_get_value(
context,
.query_value(
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
paramsv![contact_id as i32],
paramsx![contact_id as i32],
)
.await
.unwrap_or_default();
@@ -770,10 +773,9 @@ impl Contact {
let count_msgs: i32 = if count_contacts > 0 {
context
.sql
.query_get_value(
context,
.query_value(
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
paramsv![contact_id as i32, contact_id as i32],
paramsx![contact_id as i32, contact_id as i32],
)
.await
.unwrap_or_default()
@@ -786,12 +788,12 @@ impl Contact {
.sql
.execute(
"DELETE FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
paramsx![contact_id as i32],
)
.await
{
Ok(_) => {
context.emit_event(EventType::ContactsChanged(None));
context.emit_event(Event::ContactsChanged(None));
return Ok(());
}
Err(err) => {
@@ -824,7 +826,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET param=? WHERE id=?",
paramsv![self.param.to_string(), self.id as i32],
paramsx![self.param.to_string(), self.id as i32],
)
.await?;
Ok(())
@@ -932,7 +934,7 @@ impl Contact {
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
@@ -946,18 +948,8 @@ impl Contact {
}
}
let peerstate = match Peerstate::from_addr(context, &self.addr).await {
Ok(peerstate) => peerstate,
Err(err) => {
warn!(
context,
"Failed to load peerstate for address {}: {}", self.addr, err
);
return VerifiedStatus::Unverified;
}
};
if let Some(ps) = peerstate {
let peerstate = Peerstate::from_addr(context, &self.addr).await;
if let Ok(ps) = peerstate {
if ps.verified_key.is_some() {
return VerifiedStatus::BidirectVerified;
}
@@ -992,15 +984,15 @@ impl Contact {
return 0;
}
context
let v: i32 = context
.sql
.query_get_value::<isize>(
context,
.query_value(
"SELECT COUNT(*) FROM contacts WHERE id>?;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
)
.await
.unwrap_or_default() as usize
.unwrap_or_default();
v as usize
}
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
@@ -1012,7 +1004,7 @@ impl Contact {
.sql
.exists(
"SELECT id FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
paramsx![contact_id as i32],
)
.await
.unwrap_or_default()
@@ -1023,7 +1015,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
paramsv![origin, contact_id as i32, origin],
paramsx![origin, contact_id as i32, origin],
)
.await
.is_ok()
@@ -1053,7 +1045,9 @@ pub fn addr_normalize(addr: &str) -> &str {
}
fn sanitize_name_and_addr(name: impl AsRef<str>, addr: impl AsRef<str>) -> (String, String) {
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
lazy_static! {
static ref ADDR_WITH_NAME_REGEX: Regex = Regex::new("(.*)<(.*)>").unwrap();
}
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.as_ref().is_empty() {
@@ -1083,7 +1077,7 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
paramsx![new_blocking as i32, contact_id as i32],
)
.await
.is_ok()
@@ -1093,62 +1087,48 @@ 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(
"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()
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(EventType::ContactsChanged(Some(contact_id)));
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) => {
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);
}
contact.param.set(Param::ProfileImage, profile_image);
true
}
AvatarAction::Delete => {
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);
}
contact.param.remove(Param::ProfileImage);
true
}
};
if changed {
contact.update_param(context).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
context.emit_event(Event::ContactsChanged(Some(contact_id)));
}
Ok(())
}

View File

@@ -6,7 +6,6 @@ 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 +13,14 @@ use crate::constants::*;
use crate::contact::*;
use crate::dc_tools::duration_to_str;
use crate::error::*;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::events::{Event, EventEmitter, Events};
use crate::job::{self, Action};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{self, MsgId};
use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId};
use crate::param::Params;
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
use std::time::SystemTime;
@@ -44,23 +45,17 @@ pub struct InnerContext {
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
pub(crate) os_name: Option<String>,
pub(crate) bob: RwLock<Bob>,
pub(crate) bob: RwLock<BobStatus>,
pub(crate) last_smeared_timestamp: RwLock<i64>,
pub(crate) running_state: RwLock<RunningState>,
/// Mutex to avoid generating the key for the user more than once.
pub(crate) generating_key_mutex: Mutex<()>,
/// Mutex to enforce only a single running oauth2 is running.
pub(crate) oauth2_mutex: Mutex<()>,
/// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messeges being sent.
pub(crate) wrong_pw_warning_mutex: Mutex<()>,
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
pub(crate) events: Events,
pub(crate) scheduler: RwLock<Scheduler>,
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
/// Id for this context on the current device.
pub(crate) id: u32,
creation_time: SystemTime,
}
@@ -81,7 +76,11 @@ pub struct RunningState {
pub fn get_info() -> BTreeMap<&'static str, String> {
let mut res = BTreeMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
let version =
String::from_utf8(libsqlite3_sys::SQLITE_VERSION.to_vec()).expect("invalid version");
res.insert("sqlite_version", version);
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
res.insert("level", "awesome".into());
res
@@ -89,9 +88,7 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
impl Context {
/// Creates new context.
pub async fn new(os_name: String, dbfile: PathBuf, id: u32) -> Result<Context> {
// pretty_env_logger::try_init_timed().ok();
pub async fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -99,14 +96,13 @@ impl Context {
if !blobdir.exists().await {
async_std::fs::create_dir_all(&blobdir).await?;
}
Context::with_blobdir(os_name, dbfile, blobdir, id).await
Context::with_blobdir(os_name, dbfile, blobdir).await
}
pub(crate) async fn with_blobdir(
pub async fn with_blobdir(
os_name: String,
dbfile: PathBuf,
blobdir: PathBuf,
id: u32,
) -> Result<Context> {
ensure!(
blobdir.is_dir().await,
@@ -115,7 +111,6 @@ impl Context {
);
let inner = InnerContext {
id,
blobdir,
dbfile,
os_name: Some(os_name),
@@ -125,17 +120,16 @@ 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?;
Ok(ctx)
@@ -190,11 +184,8 @@ impl Context {
}
/// Emits a single event.
pub fn emit_event(&self, event: EventType) {
self.events.emit(Event {
id: self.id,
typ: event,
});
pub fn emit_event(&self, event: Event) {
self.events.emit(event);
}
/// Get the next queued event.
@@ -202,11 +193,6 @@ impl Context {
self.events.get_emitter()
}
/// Get the ID of this context.
pub fn get_id(&self) -> u32 {
self.id
}
// Ongoing process allocation/free/check
pub async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
@@ -277,27 +263,29 @@ impl Context {
let is_configured = self.get_config_int(Config::Configured).await;
let dbversion = self
.sql
.get_raw_config_int(self, "dbversion")
.get_raw_config_int("dbversion")
.await
.unwrap_or_default();
let journal_mode = self
.sql
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
.query_value("PRAGMA journal_mode;", paramsx![])
.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<isize> = self
let prv_key_cnt: Option<i32> = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.await;
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
.await
.ok();
let pub_key_cnt: Option<isize> = self
let pub_key_cnt: Option<i32> = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
.await;
.query_value("SELECT COUNT(*) FROM acpeerstates;", paramsx![])
.await
.ok();
let fingerprint_str = match SignedPublicKey::load_self(self).await {
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {}>", err),
@@ -309,7 +297,7 @@ impl Context {
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
let folders_configured = self
.sql
.get_raw_config_int(self, "folders_configured")
.get_raw_config_int("folders_configured")
.await
.unwrap_or_default();
@@ -370,30 +358,22 @@ impl Context {
pub async fn get_fresh_msgs(&self) -> Vec<MsgId> {
let show_deaddrop: i32 = 0;
self.sql
.query_map(
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" LEFT JOIN chats c",
" ON m.chat_id=c.id",
" WHERE m.state=?",
" AND m.hidden=0",
" AND m.chat_id>?",
" AND ct.blocked=0",
" AND (c.blocked=0 OR c.blocked=?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
),
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)
},
.query_values(
r#"
SELECT m.id
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
LEFT JOIN chats c
ON m.chat_id=c.id
WHERE m.state=?
AND m.hidden=0
AND m.chat_id>?
AND ct.blocked=0
AND (c.blocked=0 OR c.blocked=?)
ORDER BY m.timestamp DESC,m.id DESC;
"#,
paramsx![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
)
.await
.unwrap_or_default()
@@ -409,47 +389,36 @@ impl Context {
let strLikeBeg = format!("{}%", real_query);
let query = if !chat_id.is_unset() {
concat!(
"SELECT m.id AS id, m.timestamp AS timestamp",
" FROM msgs m",
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" WHERE m.chat_id=?",
" AND m.hidden=0",
" AND ct.blocked=0",
" AND (txt LIKE ? OR ct.name LIKE ?)",
" ORDER BY m.timestamp,m.id;"
)
r#"
SELECT m.id
FROM msgs
LEFT JOIN contacts ct
ON m.from_id=ct.id
WHERE m.chat_id=?
AND m.hidden=0
AND ct.blocked=0
AND (txt LIKE ? OR ct.name LIKE ?)
ORDER BY m.timestamp,m.id;
"#
} else {
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;"
)
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;
"#
};
self.sql
.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)
},
)
.query_values(query, paramsx![chat_id, strLikeInText, strLikeBeg])
.await
.unwrap_or_default()
}
@@ -469,11 +438,32 @@ impl Context {
== Some(folder_name.as_ref().to_string())
}
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
dbfile.with_file_name(blob_fname)
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;
}
}
}
}
}
@@ -505,6 +495,13 @@ impl Default for RunningState {
}
}
#[derive(Debug, Default)]
pub(crate) struct BobStatus {
pub expects: i32,
pub status: i32,
pub qr_scan: Option<Lot>,
}
pub fn get_version_str() -> &'static str {
&DC_VERSION_STR
}
@@ -520,7 +517,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new("FakeOs".into(), dbfile.into(), 1).await;
let res = Context::new("FakeOs".into(), dbfile.into()).await;
assert!(res.is_err());
}
@@ -535,9 +532,7 @@ mod tests {
async fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new("FakeOS".into(), dbfile.into(), 1)
.await
.unwrap();
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
@@ -548,7 +543,7 @@ mod tests {
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new("FakeOS".into(), dbfile.into(), 1).await;
let res = Context::new("FakeOS".into(), dbfile.into()).await;
assert!(res.is_err());
}
@@ -558,9 +553,7 @@ mod tests {
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new("FakeOS".into(), dbfile.into(), 1)
.await
.unwrap();
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
@@ -570,7 +563,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir, 1).await;
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
assert!(res.is_err());
}
@@ -579,7 +572,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
assert!(res.is_err());
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,29 +4,24 @@
use core::cmp::{max, min};
use std::borrow::Cow;
use std::fmt;
use std::io::Cursor;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng};
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{Viewtype, DC_OUTDATED_WARNING_DAYS};
use crate::context::Context;
use crate::error::{bail, Error};
use crate::events::EventType;
use crate::message::Message;
use crate::provider::get_provider_update_timestamp;
use crate::stock::StockMessage;
use crate::events::Event;
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
0 != v && 0 == v & (v - 1)
}
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
#[allow(clippy::indexing_slicing)]
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let ellipse = "[...]";
@@ -59,7 +54,6 @@ 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;
@@ -156,73 +150,6 @@ pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize
start
}
// if the system time is not plausible, once a day, add a device message.
// for testing we're using time() as that is also used for message timestamps.
// moreover, add a warning if the app is outdated.
pub(crate) async fn maybe_add_time_based_warnings(context: &Context) {
if !maybe_warn_on_bad_time(context, time(), get_provider_update_timestamp()).await {
maybe_warn_on_outdated(context, time(), get_provider_update_timestamp()).await;
}
}
async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestamp: i64) -> bool {
if now < known_past_timestamp {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(
context
.stock_string_repl_str(
StockMessage::BadTimeMsgBody,
Local
.timestamp(now, 0)
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
)
.await,
);
add_device_msg_with_importance(
context,
Some(
format!(
"bad-time-warning-{}",
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m-%d") // repeat every day
)
.as_str(),
),
Some(&mut msg),
true,
)
.await
.ok();
return true;
}
false
}
async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) {
if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(
context
.stock_str(StockMessage::UpdateReminderMsgBody)
.await
.into(),
);
add_device_msg(
context,
Some(
format!(
"outdated-warning-{}",
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m") // repeat every month
)
.as_str(),
),
Some(&mut msg),
)
.await
.ok();
}
}
/* Message-ID tools */
pub(crate) fn dc_create_id() -> String {
/* generate an id. the generated ID should be as short and as unique as possible:
@@ -271,7 +198,7 @@ fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
pub(crate) fn dc_create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
let hostname = from_addr
.find('@')
.and_then(|k| from_addr.get(k..))
.map(|k| &from_addr[k..])
.unwrap_or("@nohost");
match grpid {
Some(grpid) => format!("Gr.{}.{}{}", grpid, dc_create_id(), hostname),
@@ -313,9 +240,9 @@ pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
/// Returns the `(width, height)` of the given image buffer.
pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
let image = image::io::Reader::new(Cursor::new(buf)).with_guessed_format()?;
let dimensions = image.into_dimensions()?;
Ok(dimensions)
let meta = image_meta::load_from_buf(buf)?;
Ok((meta.dimensions.width, meta.dimensions.height))
}
/// Expand paths relative to $BLOBDIR into absolute paths.
@@ -356,7 +283,7 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
let dpath = format!("{}", path.as_ref().to_string_lossy());
match fs::remove_file(path_abs).await {
Ok(_) => {
context.emit_event(EventType::DeletedBlobFile(dpath));
context.emit_event(Event::DeletedBlobFile(dpath));
true
}
Err(err) => {
@@ -366,23 +293,6 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
}
}
pub async fn dc_delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
match async_std::fs::read_dir(path).await {
Ok(mut read_dir) => {
while let Some(entry) = read_dir.next().await {
match entry {
Ok(file) => {
dc_delete_file(context, file.file_name()).await;
}
Err(e) => warn!(context, "Could not read file to delete: {}", e),
}
}
}
Err(e) => warn!(context, "Could not read dir to delete: {}", e),
}
}
pub(crate) async fn dc_copy_file(
context: &Context,
src_path: impl AsRef<Path>,
@@ -537,7 +447,7 @@ pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
}
}
pub(crate) async fn get_next_backup_path_old(
pub(crate) async fn dc_get_next_backup_path(
folder: impl AsRef<Path>,
backup_time: i64,
) -> Result<PathBuf, Error> {
@@ -557,32 +467,6 @@ pub(crate) async fn get_next_backup_path_old(
bail!("could not create backup file, disk full?");
}
/// Returns Ok((temp_path, dest_path)) on success. The backup can then be written to temp_path. If the backup succeeded,
/// it can be renamed to dest_path. This guarantees that the backup is complete.
pub(crate) async fn get_next_backup_path_new(
folder: impl AsRef<Path>,
backup_time: i64,
) -> Result<(PathBuf, PathBuf), Error> {
let folder = PathBuf::from(folder.as_ref());
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
.format("delta-chat-backup-%Y-%m-%d")
.to_string();
// 64 backup files per day should be enough for everyone
for i in 0..64 {
let mut tempfile = folder.clone();
tempfile.push(format!("{}-{:02}.tar.part", stem, i));
let mut destfile = folder.clone();
destfile.push(format!("{}-{:02}.tar", stem, i));
if !tempfile.exists().await && !destfile.exists().await {
return Ok((tempfile, destfile));
}
}
bail!("could not create backup file, disk full?");
}
pub(crate) fn time() -> i64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
@@ -684,14 +568,6 @@ 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 {
@@ -699,32 +575,8 @@ pub(crate) fn listflags_has(listflags: u32, bitindex: usize) -> bool {
(listflags & bitindex) == bitindex
}
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
input
.as_ref()
.replace("\n", " ")
.replace("\r", " ")
.trim()
.to_string()
}
pub(crate) trait IsNoneOrEmpty<T> {
fn is_none_or_empty(&self) -> bool;
}
impl<T> IsNoneOrEmpty<T> for Option<T>
where
T: AsRef<str>,
{
fn is_none_or_empty(&self) -> bool {
!matches!(self, Some(s) if !s.as_ref().is_empty())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use std::convert::TryInto;
@@ -884,9 +736,6 @@ mod tests {
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
}
use crate::chat;
use crate::chatlist::Chatlist;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use proptest::prelude::*;
proptest! {
@@ -1056,147 +905,4 @@ mod tests {
"3h 1m 0s"
);
}
#[test]
fn test_get_filemeta() {
let data = include_bytes!("../test-data/image/avatar900x900.png");
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 900);
assert_eq!(h, 900);
let data = include_bytes!("../test-data/image/avatar1000x1000.jpg");
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 1000);
assert_eq!(h, 1000);
let data = include_bytes!("../test-data/image/image100x50.gif");
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 100);
assert_eq!(h, 50);
}
#[test]
fn test_improve_single_line_input() {
assert_eq!(improve_single_line_input("Hi\naiae "), "Hi aiae");
assert_eq!(improve_single_line_input("\r\nahte\n\r"), "ahte");
}
#[async_std::test]
async fn test_maybe_warn_on_bad_time() {
let t = TestContext::new().await;
let timestamp_now = time();
let timestamp_future = timestamp_now + 60 * 60 * 24 * 7;
let timestamp_past = NaiveDateTime::new(
NaiveDate::from_ymd(2020, 9, 1),
NaiveTime::from_hms(0, 0, 0),
)
.timestamp_millis()
/ 1_000;
// a correct time must not add a device message
maybe_warn_on_bad_time(&t.ctx, timestamp_now, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// we cannot find out if a date in the future is wrong - a device message is not added
maybe_warn_on_bad_time(&t.ctx, timestamp_future, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// a date in the past must add a device message
maybe_warn_on_bad_time(&t.ctx, timestamp_past, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
// the message should be added only once a day - test that an hour later and nearly a day later
maybe_warn_on_bad_time(
&t.ctx,
timestamp_past + 60 * 60,
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
maybe_warn_on_bad_time(
&t.ctx,
timestamp_past + 60 * 60 * 24 - 1,
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
// next day, there should be another device message
maybe_warn_on_bad_time(
&t.ctx,
timestamp_past + 60 * 60 * 24,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
assert_eq!(device_chat_id, chats.get_chat_id(0));
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 2);
}
#[async_std::test]
async fn test_maybe_warn_on_outdated() {
let t = TestContext::new().await;
let timestamp_now: i64 = time();
// in about 6 months, the app should not be outdated
// (if this fails, provider-db is not updated since 6 months)
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + 180 * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// in 1 year, the app should be considered as outdated
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + 365 * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
// do not repeat the warning every day ...
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + (365 + 1) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
// ... but every month
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + (365 + 31) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 2);
}
}

View File

@@ -2,10 +2,12 @@
//!
//! A module to remove HTML tags from the email text
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
lazy_static! {
static ref LINE_RE: regex::Regex = regex::Regex::new(r"(\r?\n)+").unwrap();
}
struct Dehtml {
strbuilder: String,
@@ -22,20 +24,8 @@ enum AddText {
// dehtml() returns way too many newlines; however, an optimisation on this issue is not needed as
// the newlines are typically removed in further processing by the caller
pub fn dehtml(buf: &str) -> Option<String> {
let s = dehtml_quick_xml(buf);
if !s.trim().is_empty() {
return Some(s);
}
let s = dehtml_manually(buf);
if !s.trim().is_empty() {
return Some(s);
}
None
}
pub fn dehtml_quick_xml(buf: &str) -> String {
let buf = buf.trim().trim_start_matches("<!doctype html>");
pub fn dehtml(buf: &str) -> String {
let buf = buf.trim();
let mut dehtml = Dehtml {
strbuilder: String::with_capacity(buf.len()),
@@ -56,12 +46,6 @@ pub fn dehtml_quick_xml(buf: &str) -> String {
Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::CData(ref e)) => dehtml_cdata_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::Empty(ref e)) => {
// Handle empty tags as a start tag immediately followed by end tag.
// For example, `<p/>` is treated as `<p></p>`.
dehtml_starttag_cb(e, &mut dehtml, &reader);
dehtml_endtag_cb(&BytesEnd::borrowed(e.name()), &mut dehtml);
}
Err(e) => {
eprintln!(
"Parse html error: Error at position {}: {:?}",
@@ -181,28 +165,9 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
}
}
pub fn dehtml_manually(buf: &str) -> String {
// Just strip out everything between "<" and ">"
let mut strbuilder = String::new();
let mut show_next_chars = true;
for c in buf.chars() {
match c {
'<' => show_next_chars = false,
'>' => show_next_chars = true,
_ => {
if show_next_chars {
strbuilder.push(c)
}
}
}
}
strbuilder
}
#[cfg(test)]
mod tests {
use super::*;
use crate::simplify::simplify;
#[test]
fn test_dehtml() {
@@ -211,40 +176,31 @@ mod tests {
"<a href='https://example.com'> Foo </a>",
"[ Foo ](https://example.com)",
),
("<img href='/foo.png'>", ""),
("<b> bar </b>", "* bar *"),
("<b> bar <i> foo", "* bar _ foo"),
("&amp; bar", "& bar"),
// Despite missing ', this should be shown:
("<a href='/foo.png>Hi</a> ", "Hi "),
(
"<a href='https://get.delta.chat/'/>",
"[](https://get.delta.chat/)",
),
("<!doctype html>\n<b>fat text</b>", "*fat text*"),
// Invalid html (at least DC should show the text if the html is invalid):
("<!some invalid html code>\n<b>some text</b>", "some text"),
// Note missing '
("<a href='/foo.png>Hi</a> ", ""),
("", ""),
];
for (input, output) in cases {
assert_eq!(simplify(dehtml(input).unwrap(), true).0, output);
}
let none_cases = vec!["<html> </html>", ""];
for input in none_cases {
assert_eq!(dehtml(input), None);
assert_eq!(dehtml(input), output);
}
}
#[test]
fn test_dehtml_parse_br() {
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
let plain = dehtml(html).unwrap();
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
let plain = dehtml(html);
assert_eq!(plain, "line1\n\r\r\rline2\nline3");
assert_eq!(plain, "line1\n\r\r\rline2");
}
#[test]
fn test_dehtml_parse_href() {
let html = "<a href=url>text</a";
let plain = dehtml(html).unwrap();
let plain = dehtml(html);
assert_eq!(plain, "[text](url)");
}
@@ -252,7 +208,7 @@ mod tests {
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = dehtml(html).unwrap();
let plain = dehtml(html);
assert_eq!(plain, "text *bold*<>");
}
@@ -262,7 +218,7 @@ mod tests {
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = dehtml(html).unwrap();
let plain = dehtml(html);
assert_eq!(
plain,
@@ -285,7 +241,7 @@ mod tests {
</body>
</html>
"##;
let txt = dehtml(input).unwrap();
let txt = dehtml(input);
assert_eq!(txt.trim(), "lots of text");
}
}

View File

@@ -15,6 +15,7 @@ use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::*;
use crate::peerstate::*;
use crate::pgp;
use crate::securejoin::handle_degrade_event;
#[derive(Debug)]
pub struct EncryptHelper {
@@ -51,42 +52,23 @@ impl EncryptHelper {
}
/// Determines if we can and should encrypt.
///
/// For encryption to be enabled, `e2ee_guaranteed` should be true, or strictly more than a half
/// of peerstates should prefer encryption. Own preference is counted equally to peer
/// preferences, even if message copy is not sent to self.
///
/// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
/// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
///
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
pub fn should_encrypt(
&self,
context: &Context,
e2ee_guaranteed: bool,
peerstates: &[(Option<Peerstate>, &str)],
) -> Result<bool> {
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
1
} else {
0
};
if !(self.prefer_encrypt == EncryptPreference::Mutual || e2ee_guaranteed) {
return Ok(false);
}
for (peerstate, addr) in peerstates {
match peerstate {
Some(peerstate) => {
info!(
context,
"peerstate for {:?} is {}", addr, peerstate.prefer_encrypt
);
match peerstate.prefer_encrypt {
EncryptPreference::NoPreference => {}
EncryptPreference::Mutual => prefer_encrypt_count += 1,
EncryptPreference::Reset => {
if !e2ee_guaranteed {
return Ok(false);
}
}
};
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !e2ee_guaranteed {
info!(context, "peerstate for {:?} is no-encrypt", addr);
return Ok(false);
}
}
None => {
let msg = format!("peerstate for {:?} missing, cannot encrypt", addr);
@@ -100,11 +82,7 @@ impl EncryptHelper {
}
}
// Count number of recipients, including self.
// This does not depend on whether we send a copy to self or not.
let recipients_count = peerstates.len() + 1;
Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
Ok(true)
}
/// Tries to encrypt the passed in `mail`.
@@ -113,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();
@@ -137,14 +115,6 @@ impl EncryptHelper {
}
}
/// Tries to decrypt a message, but only if it is structured as an
/// Autocrypt message.
///
/// Returns decrypted body and a set of valid signature fingerprints
/// if successful.
///
/// If the message is wrongly signed, this will still return the decrypted
/// message but the HashSet will be empty.
pub async fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
@@ -158,31 +128,44 @@ pub async fn try_decrypt(
.map(|from| from.addr)
.unwrap_or_default();
let mut peerstate = Peerstate::from_addr(context, &from).await?;
let mut peerstate = None;
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
if message_time > 0 {
peerstate = Peerstate::from_addr(context, &from).await.ok();
// Apply Autocrypt header
if let Some(ref header) = Aheader::from_headers(context, &from, &mail.headers) {
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_header(&header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_header(context, header, message_time);
if let Some(ref header) = autocryptheader {
peerstate.apply_header(&header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) {
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
} else if let Some(ref header) = autocryptheader {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
}
// Possibly perform decryption
/* possibly perform decryption */
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
let mut signatures = HashSet::default();
if let Some(ref mut peerstate) = peerstate {
peerstate.handle_fingerprint_change(context).await?;
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
peerstate = Peerstate::from_addr(&context, &from).await.ok();
}
if let Some(peerstate) = peerstate {
if peerstate.degrade_event.is_some() {
handle_degrade_event(context, &peerstate).await?;
}
if let Some(key) = peerstate.gossip_key {
public_keyring_for_validate.add(key);
}
if let Some(key) = peerstate.public_key {
public_keyring_for_validate.add(key);
}
}
@@ -194,18 +177,6 @@ pub async fn try_decrypt(
&mut signatures,
)
.await?;
if let Some(mut peerstate) = peerstate {
// If message is not encrypted and it is not a read receipt, degrade encryption.
if out_mail.is_none()
&& message_time > peerstate.last_seen_autocrypt
&& !contains_report(mail)
{
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
}
Ok((out_mail, signatures))
}
@@ -216,32 +187,40 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
"Not a multipart/encrypted message: {}",
mail.ctype.mimetype
);
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.len() == 2,
"Invalid Autocrypt Level 1 Mime Parts"
);
ensure!(
second_part.ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
second_part.ctype
);
ensure!(
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
mail.subparts[0].ctype,
);
Ok(second_part)
} else {
bail!("Invalid Autocrypt Level 1 Mime Parts")
}
ensure!(
mail.subparts[1].ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
mail.subparts[1].ctype
);
Ok(&mail.subparts[1])
}
async fn decrypt_if_autocrypt_message(
async fn decrypt_if_autocrypt_message<'a>(
context: &Context,
mail: &ParsedMail<'_>,
mail: &ParsedMail<'a>,
private_keyring: Keyring<SignedSecretKey>,
public_keyring_for_validate: Keyring<SignedPublicKey>,
ret_valid_signatures: &mut HashSet<Fingerprint>,
) -> Result<Option<Vec<u8>>> {
// The returned bool is true if we detected an Autocrypt-encrypted
// message and successfully decrypted it. Decryption then modifies the
// passed in mime structure in place. The returned bool is false
// if it was not an Autocrypt message.
//
// Errors are returned for failures related to decryption of AC-messages.
let encrypted_data_part = match get_autocrypt_mime(mail) {
Err(_) => {
// not an autocrypt mime message, abort and ignore
@@ -281,16 +260,13 @@ async fn decrypt_part(
)
.await?;
// If the message was wrongly or not signed, still return the plain text.
// The caller has to check the signatures then.
ensure!(!ret_valid_signatures.is_empty(), "no valid signatures");
return Ok(Some(plain));
}
Ok(None)
}
#[allow(clippy::indexing_slicing)]
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
if let Some(index) = input.iter().position(|b| *b > b' ') {
if input.len() - index > 26 {
@@ -344,11 +320,6 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
mod tests {
use super::*;
use crate::chat;
use crate::constants::Viewtype;
use crate::contact::{Contact, Origin};
use crate::message::Message;
use crate::param::Param;
use crate::test_utils::*;
mod ensure_secret_key_exists {
@@ -409,161 +380,4 @@ Sent with my Delta Chat Messenger: https://delta.chat";
let data = b"blas";
assert_eq!(has_decrypted_pgp_armor(data), false);
}
#[async_std::test]
async fn test_encrypted_no_autocrypt() -> crate::error::Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let (contact_alice_id, _modified) = Contact::add_or_lookup(
&bob.ctx,
"Alice",
"alice@example.com",
Origin::ManuallyCreated,
)
.await?;
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice.ctx,
"Bob",
"bob@example.net",
Origin::ManuallyCreated,
)
.await?;
let chat_alice = chat::create_by_contact_id(&alice.ctx, contact_bob_id).await?;
let chat_bob = chat::create_by_contact_id(&bob.ctx, contact_alice_id).await?;
// Alice sends unencrypted message to Bob
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
// Bob receives unencrypted message from Alice
let msg = bob.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
// Parsing a message is enough to update peerstate
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
// Bob sends encrypted message to Alice
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&bob.ctx, chat_bob, &mut msg).await?;
chat::send_msg(&bob.ctx, chat_bob, &mut msg).await?;
let sent = bob.pop_sent_msg().await;
// Alice receives encrypted message from Bob
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
let peerstate_bob = Peerstate::from_addr(&alice.ctx, "bob@example.net")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_bob.prefer_encrypt, EncryptPreference::Mutual);
// Now Alice and Bob have established keys.
// Alice sends encrypted message without Autocrypt header.
let mut msg = Message::new(Viewtype::Text);
msg.param.set_int(Param::SkipAutocrypt, 1);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
// Alice sends plaintext message with Autocrypt header.
let mut msg = Message::new(Viewtype::Text);
msg.param.set_int(Param::ForcePlaintext, 1);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
// Alice sends plaintext message without Autocrypt header.
let mut msg = Message::new(Viewtype::Text);
msg.param.set_int(Param::ForcePlaintext, 1);
msg.param.set_int(Param::SkipAutocrypt, 1);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset);
Ok(())
}
fn new_peerstates(
ctx: &Context,
prefer_encrypt: EncryptPreference,
) -> Vec<(Option<Peerstate<'_>>, &str)> {
let addr = "bob@foo.bar";
let pub_key = bob_keypair().public;
let peerstate = Peerstate {
context: &ctx,
addr: addr.into(),
last_seen: 13,
last_seen_autocrypt: 14,
prefer_encrypt,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: Some(pub_key.clone()),
gossip_timestamp: 15,
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
let mut peerstates = Vec::new();
peerstates.push((Some(peerstate), addr));
peerstates
}
#[async_std::test]
async fn test_should_encrypt() {
let t = TestContext::new_alice().await;
let encrypt_helper = EncryptHelper::new(&t.ctx).await.unwrap();
// test with EncryptPreference::NoPreference:
// if e2ee_eguaranteed is unset, there is no encryption as not more than half of peers want encryption
let ps = new_peerstates(&t.ctx, EncryptPreference::NoPreference);
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
// test with EncryptPreference::Reset
let ps = new_peerstates(&t.ctx, EncryptPreference::Reset);
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
// test with EncryptPreference::Mutual (self is also Mutual)
let ps = new_peerstates(&t.ctx, EncryptPreference::Mutual);
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
// test with missing peerstate
let mut ps = Vec::new();
ps.push((None, "bob@foo.bar"));
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
}
}

View File

@@ -1,528 +0,0 @@
//! # Ephemeral messages
//!
//! Ephemeral messages are messages that have an Ephemeral-Timer
//! header attached to them, which specifies time in seconds after
//! which the message should be deleted both from the device and from
//! the server. The timer is started when the message is marked as
//! seen, which usually happens when its contents is displayed on
//! device screen.
//!
//! Each chat, including 1:1, group chats and "saved messages" chat,
//! has its own ephemeral timer setting, which is applied to all
//! messages sent to the chat. The setting is synchronized to all the
//! devices participating in the chat by applying the timer value from
//! all received messages, including BCC-self ones, to the chat. This
//! way the setting is eventually synchronized among all participants.
//!
//! When user changes ephemeral timer setting for the chat, a system
//! message is automatically sent to update the setting for all
//! participants. This allows changing the setting for a chat like any
//! group chat setting, e.g. name and avatar, without the need to
//! write an actual message.
//!
//! ## Device settings
//!
//! In addition to per-chat ephemeral message setting, each device has
//! two global user-configured settings that complement per-chat
//! settings: `delete_device_after` and `delete_server_after`. These
//! settings are not synchronized among devices and apply to all
//! messages known to the device, including messages sent or received
//! before configuring the setting.
//!
//! `delete_device_after` configures the maximum time device is
//! storing the messages locally. `delete_server_after` configures the
//! time after which device will delete the messages it knows about
//! from the server.
//!
//! ## How messages are deleted
//!
//! When the message is deleted locally, its contents is removed and
//! it is moved to the trash chat. This database entry is then used to
//! track the Message-ID and corresponding IMAP folder and UID until
//! the message is deleted from the server. Vice versa, when device
//! deletes the message from the server, it removes IMAP folder and
//! UID information, but keeps the message contents. When database
//! entry is both moved to trash chat and does not contain UID
//! information, it is deleted from the database, leaving no trace of
//! the message.
//!
//! ## When messages are deleted
//!
//! Local deletion happens when the chatlist or chat is loaded. A
//! `MsgsChanged` event is emitted when a message deletion is due, to
//! make UI reload displayed messages and cause actual deletion.
//!
//! Server deletion happens by generating IMAP deletion jobs based on
//! the database entries which are expired either according to their
//! ephemeral message timers or global `delete_server_after` setting.
use 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::EventType;
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(EventType::ChatEphemeralTimerModified {
chat_id: self,
timer,
});
Ok(())
}
/// Set ephemeral message timer value in seconds.
///
/// If timer value is 0, disable ephemeral message timer.
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<(), Error> {
if timer == self.get_ephemeral_timer(context).await? {
return Ok(());
}
self.inner_set_ephemeral_timer(context, timer).await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_ephemeral_timer_changed(context, timer, DC_CONTACT_ID_SELF).await);
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
if let Err(err) = send_msg(context, self, &mut msg).await {
error!(
context,
"Failed to send a message about ephemeral message timer change: {:?}", err
);
}
Ok(())
}
}
/// Returns a stock message saying that ephemeral timer is changed to `timer` by `from_id`.
pub(crate) async fn stock_ephemeral_timer_changed(
context: &Context,
timer: Timer,
from_id: u32,
) -> String {
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,
EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0)
}
);
});
*context.ephemeral_task.write().await = Some(ephemeral_task);
} else {
// Emit event immediately
emit_event!(
context,
EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0)
}
);
}
}
}
/// Returns ID of any expired message that should be deleted from the server.
///
/// It looks up the trash chat too, to find messages that are already
/// deleted locally, but not deleted on the server.
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
let now = time();
let threshold_timestamp = match context.get_config_delete_server_after().await {
None => 0,
Some(delete_server_after) => now - delete_server_after,
};
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."
);
}
}

View File

@@ -1,13 +1,10 @@
//! # Events specification
use std::ops::Deref;
use async_std::path::PathBuf;
use async_std::sync::{channel, Receiver, Sender, TrySendError};
use strum::EnumProperty;
use crate::chat::ChatId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
#[derive(Debug)]
@@ -56,38 +53,14 @@ impl EventEmitter {
async_std::task::block_on(self.recv())
}
/// Async recv of an event. Return `None` if the `Sender` has been droped.
/// Blocking async recv of an event. Return `None` if the `Sender` has been droped.
pub async fn recv(&self) -> Option<Event> {
// TODO: change once we can use async channels internally.
self.0.recv().await.ok()
}
}
impl async_std::stream::Stream for EventEmitter {
type Item = Event;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.0).poll_next(cx)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
pub id: u32,
pub typ: EventType,
}
impl Deref for Event {
type Target = EventType;
fn deref(&self) -> &EventType {
&self.typ
}
}
impl EventType {
impl Event {
/// Returns the corresponding Event id.
pub fn as_id(&self) -> i32 {
self.get_str("id")
@@ -98,7 +71,7 @@ impl EventType {
}
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty)]
pub enum EventType {
pub enum Event {
/// The library-user may write an informational string to the log.
/// Passed to the callback given to dc_context_new().
/// This event should not be reported to the end-user using a popup or something like that.
@@ -125,11 +98,15 @@ pub enum EventType {
#[strum(props(id = "105"))]
ImapMessageMoved(String),
/// Emitted when an IMAP folder was emptied
#[strum(props(id = "106"))]
ImapFolderEmptied(String),
/// Emitted when an new file in the $BLOBDIR was created
#[strum(props(id = "150"))]
NewBlobFile(String),
/// Emitted when an file in the $BLOBDIR was deleted
/// Emitted when an new file in the $BLOBDIR was created
#[strum(props(id = "151"))]
DeletedBlobFile(String),
@@ -162,6 +139,7 @@ pub enum EventType {
/// Network errors should be reported to users in a non-disturbing way,
/// however, as network errors may come in a sequence,
/// it is not useful to raise each an every error to the user.
/// For this purpose, data1 is set to 1 if the error is probably worth reporting.
///
/// Moreover, if the UI detects that the device is offline,
/// it is probably more useful to report this to the user
@@ -192,11 +170,6 @@ pub enum EventType {
#[strum(props(id = "2005"))]
IncomingMsg { chat_id: ChatId, msg_id: MsgId },
/// Messages were seen or noticed.
/// chat id is always set.
#[strum(props(id = "2008"))]
MsgsNoticed(ChatId),
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
#[strum(props(id = "2010"))]
@@ -216,19 +189,9 @@ pub enum EventType {
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
/// This event does not include ephemeral timer modification, which
/// is a separate event.
#[strum(props(id = "2020"))]
ChatModified(ChatId),
/// Chat ephemeral timer changed.
#[strum(props(id = "2021"))]
ChatEphemeralTimerModified {
chat_id: ChatId,
timer: EphemeralTimer,
},
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
@@ -244,16 +207,10 @@ pub enum EventType {
LocationChanged(Option<u32>),
/// Inform about the configuration progress started by configure().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
#[strum(props(id = "2041"))]
ConfigureProgress {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
},
ConfigureProgress(usize),
/// Inform about the import/export progress started by imex().
///

View File

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

View File

@@ -3,6 +3,7 @@ use mailparse::{MailHeader, MailHeaderMap};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[strum(serialize_all = "kebab_case")]
#[allow(dead_code)]
pub enum HeaderDef {
MessageId,
Subject,
@@ -34,7 +35,6 @@ pub enum HeaderDef {
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
ChatWebrtcRoom,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
@@ -42,7 +42,6 @@ pub enum HeaderDef {
SecureJoinFingerprint,
SecureJoinInvitenumber,
SecureJoinAuth,
EphemeralTimer,
_TestHeader,
}

View File

@@ -56,7 +56,7 @@ impl Client {
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
self,
auth_type: S,
authenticator: A,
authenticator: &A,
) -> std::result::Result<Session, (ImapError, Self)> {
let Client { inner, is_secure } = self;
let session =
@@ -85,6 +85,9 @@ impl Client {
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(domain.as_ref(), stream).await?);
let mut client = ImapClient::new(tls_stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
@@ -101,6 +104,9 @@ impl Client {
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
let mut client = ImapClient::new(stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -51,14 +51,6 @@ 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>>(
@@ -84,7 +76,10 @@ impl Imap {
}
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
self.maybe_close_folder(context).await?;
let needs_expunge = { self.config.selected_folder_needs_expunge };
if needs_expunge {
self.close_folder(context).await?;
}
// select new folder
if let Some(ref folder) = folder {

View File

@@ -1,17 +1,10 @@
//! # Import/export module
use std::any::Any;
use std::{
cmp::{max, min},
ffi::OsStr,
};
use std::cmp::{max, min};
use anyhow::Context as _;
use async_std::path::{Path, PathBuf};
use async_std::{
fs::{self, File},
prelude::*,
};
use async_std::prelude::*;
use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
@@ -23,7 +16,7 @@ use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::error::*;
use crate::events::EventType;
use crate::events::Event;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
@@ -31,11 +24,6 @@ use crate::param::*;
use crate::pgp;
use crate::sql::{self, Sql};
use crate::stock::StockMessage;
use async_tar::Archive;
// Name of the database file in the backup.
const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
const BLOBS_BACKUP_NAME: &str = "blobs_backup";
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i32)]
@@ -54,8 +42,8 @@ pub enum ImexMode {
/// Export a backup to the directory given as `param1`.
/// The backup contains all contacts, chats, images and other data and device independent settings.
/// The backup does not contain device dependent settings as ringtones or LED notification settings.
/// The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
/// the format is `delta-chat-<day>-<number>.tar`
/// The name of the backup is typically `delta-chat.<day>.bak`, if more than one backup is create on a day,
/// the format is `delta-chat.<day>-<number>.bak`
ExportBackup = 11,
/// `param1` is the file (not: directory) to import. The file is normally
@@ -83,82 +71,20 @@ pub async fn imex(
what: ImexMode,
param1: Option<impl AsRef<Path>>,
) -> Result<()> {
let cancel = context.alloc_ongoing().await?;
use futures::future::FutureExt;
let res = async {
let success = imex_inner(context, what, param1).await;
match success {
Ok(()) => {
info!(context, "IMEX successfully completed");
context.emit_event(EventType::ImexProgress(1000));
Ok(())
}
Err(err) => {
cleanup_aborted_imex(context, what).await;
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
error!(context, "{:#}", err);
context.emit_event(EventType::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err);
}
}
}
.race(async {
cancel.recv().await.ok();
cleanup_aborted_imex(context, what).await;
Err(format_err!("canceled"))
})
.await;
let cancel = context.alloc_ongoing().await?;
let res = imex_inner(context, what, param1)
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
.await;
context.free_ongoing().await;
res
}
async fn cleanup_aborted_imex(context: &Context, what: ImexMode) {
if what == ImexMode::ImportBackup {
dc_delete_file(context, context.get_dbfile()).await;
dc_delete_files_in_dir(context, context.get_blobdir()).await;
}
if what == ImexMode::ExportBackup || what == ImexMode::ImportBackup {
if let Err(e) = context.sql.open(context, context.get_dbfile(), false).await {
warn!(context, "Re-opening db after imex failed: {}", e);
}
}
}
/// Returns the filename of the backup found (otherwise an error)
pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
let dir_name = dir_name.as_ref();
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
let mut newest_backup_name = "".to_string();
let mut newest_backup_path: Option<PathBuf> = None;
while let Some(dirent) = dir_iter.next().await {
if let Ok(dirent) = dirent {
let path = dirent.path();
let name = dirent.file_name();
let name: String = name.to_string_lossy().into();
if name.starts_with("delta-chat")
&& name.ends_with(".tar")
&& (newest_backup_name.is_empty() || name > newest_backup_name)
{
// We just use string comparison to determine which backup is newer.
// This works fine because the filenames have the form ...delta-chat-backup-2020-07-24-00.tar
newest_backup_path = Some(path);
newest_backup_name = name;
}
}
}
match newest_backup_path {
Some(path) => Ok(path.to_string_lossy().into_owned()),
None => has_backup_old(context, dir_name).await,
// When we decide to remove support for .bak backups, we can replace this with `None => bail!("no backup found in {}", dir_name.display()),`.
}
}
/// Returns the filename of the backup found (otherwise an error)
pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
let dir_name = dir_name.as_ref();
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
let mut newest_backup_time = 0;
@@ -170,27 +96,21 @@ pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Re
let name = name.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
let sql = Sql::new();
match sql.open(context, &path, true).await {
Ok(_) => {
let curr_backup_time = sql
.get_raw_config_int(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;
}
Err(e) => warn!(
context,
"Found backup file {} which could not be opened: {}", name, e
),
sql.open(context, &path, true).await?;
let curr_backup_time = sql
.get_raw_config_int("backup_time")
.await
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
newest_backup_time = curr_backup_time;
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close().await;
}
}
}
match newest_backup_path {
Some(path) => Ok(path.to_string_lossy().into_owned()),
None => bail!("no backup found in {}", dir_name.display()),
@@ -230,8 +150,10 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
msg.param
.set(Param::MimeType, "application/autocrypt-setup");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
msg.param.set_int(Param::ForcePlaintext, 1);
msg.param.set_int(Param::SkipAutocrypt, 1);
msg.param.set_int(
Param::ForcePlaintext,
ForcePlaintext::NoAutocryptHeader as i32,
);
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
info!(context, "Wait for setup message being sent ...",);
@@ -256,11 +178,10 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
///
/// The `passphrase` must be at least 2 characters long.
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
passphrase_begin
} else {
bail!("Passphrase must be at least 2 chars long.");
};
ensure!(
passphrase.len() >= 2,
"Passphrase must be at least 2 chars long."
);
let private_key = SignedSecretKey::load_self(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
false => None,
@@ -275,7 +196,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
"Passphrase-Format: numeric9x4\r\n",
"Passphrase-Begin: {}"
),
passphrase_begin
&passphrase[..2]
);
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
@@ -324,7 +245,7 @@ pub fn create_setup_code(_context: &Context) -> String {
}
async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool(context, "bcc_self").await {
if !context.sql.get_raw_config_bool("bcc_self").await {
let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled.
msg.text = Some(
@@ -447,11 +368,11 @@ async fn imex_inner(
ensure!(param.is_some(), "No Import/export dir/file given.");
info!(context, "Import/export process started.");
context.emit_event(EventType::ImexProgress(10));
context.emit_event(Event::ImexProgress(10));
ensure!(context.sql.is_open().await, "Database not opened.");
let path = param.ok_or_else(|| format_err!("Imex: Param was None"))?;
let path = param.unwrap();
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists
if e2ee::ensure_secret_key_exists(context).await.is_err() {
@@ -461,96 +382,29 @@ async fn imex_inner(
}
}
match what {
let success = match what {
ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
// TODO In some months we can change the export_backup_old() call to export_backup() and delete export_backup_old().
// (now is 07/2020)
ImexMode::ExportBackup => export_backup_old(context, path).await,
// import_backup() will call import_backup_old() if this is an old backup.
ImexMode::ExportBackup => export_backup(context, path).await,
ImexMode::ImportBackup => import_backup(context, path).await,
};
match success {
Ok(()) => {
info!(context, "IMEX successfully completed");
context.emit_event(Event::ImexProgress(1000));
Ok(())
}
Err(err) => {
error!(context, "IMEX FAILED: {}", err);
context.emit_event(Event::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err);
}
}
}
/// Import Backup
async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
if backup_to_import
.as_ref()
.to_string_lossy()
.ends_with(".bak")
{
// Backwards compability
return import_backup_old(context, backup_to_import).await;
}
info!(
context,
"Import \"{}\" to \"{}\".",
backup_to_import.as_ref().display(),
context.get_dbfile().display()
);
ensure!(
!context.is_configured().await,
"Cannot import backups to accounts in use."
);
context.sql.close().await;
dc_delete_file(context, context.get_dbfile()).await;
ensure!(
!context.get_dbfile().exists().await,
"Cannot delete old database."
);
let backup_file = File::open(backup_to_import).await?;
let file_size = backup_file.metadata().await?.len();
let archive = Archive::new(backup_file);
let mut entries = archive.entries()?;
while let Some(file) = entries.next().await {
let f = &mut file?;
let current_pos = f.raw_file_position();
let progress = 1000 * current_pos / file_size;
if progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
context.emit_event(EventType::ImexProgress(progress as usize));
}
if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
// async_tar can't unpack to a specified file name, so we just unpack to the blobdir and then move the unpacked file.
f.unpack_in(context.get_blobdir()).await?;
fs::rename(
context.get_blobdir().join(DBFILE_BACKUP_NAME),
context.get_dbfile(),
)
.await?;
} else {
// async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards.
f.unpack_in(context.get_blobdir()).await?;
let from_path = context.get_blobdir().join(f.path()?);
if from_path.is_file().await {
if let Some(name) = from_path.file_name() {
fs::rename(&from_path, context.get_blobdir().join(name)).await?;
} else {
warn!(context, "No file name");
}
}
}
}
context
.sql
.open(&context, &context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(&context).await?;
Ok(())
}
async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
info!(
context,
"Import \"{}\" to \"{}\".",
@@ -578,52 +432,32 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
context
.sql
.open(&context, &context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
.await?;
delete_and_reset_all_device_msgs(&context).await?;
let total_files_cnt = context
let total_files_cnt: i32 = context
.sql
.query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![])
.query_value("SELECT COUNT(*) FROM backup_blobs;", paramsx![])
.await
.unwrap_or_default() as usize;
.unwrap_or_default();
let total_files_cnt = total_files_cnt as usize;
info!(
context,
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
);
// Load IDs only for now, without the file contents, to avoid
// consuming too much memory.
let file_ids = context
.sql
.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 pool = context.sql.get_pool().await?;
let mut files = sqlx::query_as("SELECT file_name, file_content FROM backup_blobs ORDER BY id;")
.fetch(&pool);
let mut 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
@@ -631,141 +465,47 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
if permille > 990 {
permille = 990
}
context.emit_event(EventType::ImexProgress(permille));
context.emit_event(Event::ImexProgress(permille));
if file_blob.is_empty() {
continue;
}
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, &file_blob).await?;
processed_files_cnt += 1;
}
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");
}
ensure!(
processed_files_cnt == total_files_cnt,
"received stop signal"
);
context
.sql
.execute("DROP TABLE backup_blobs;", paramsx![])
.await?;
context.sql.execute("VACUUM;", paramsx![]).await?;
Ok(())
}
/*******************************************************************************
* Export backup
******************************************************************************/
#[allow(unused)]
/* the FILE_PROGRESS macro calls the callback with the permille of files processed.
The macro avoids weird values of 0% or 100% while still working. */
async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
let now = time();
let (temp_path, dest_path) = get_next_backup_path_new(dir, now).await?;
let _d = DeleteOnDrop(temp_path.clone());
context
.sql
.set_raw_config_int(context, "backup_time", now as i32)
.await?;
sql::housekeeping(context).await;
context
.sql
.execute("VACUUM;", paramsv![])
.await
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e));
// we close the database during the export
context.sql.close().await;
info!(
context,
"Backup '{}' to '{}'.",
context.get_dbfile().display(),
dest_path.display(),
);
let res = export_backup_inner(context, &temp_path).await;
// we re-open the database after export is finished
context
.sql
.open(&context, &context.get_dbfile(), false)
.await;
match &res {
Ok(_) => {
fs::rename(temp_path, &dest_path).await?;
context.emit_event(EventType::ImexFileWritten(dest_path));
}
Err(e) => {
error!(context, "backup failed: {}", e);
}
}
res
}
struct DeleteOnDrop(PathBuf);
impl Drop for DeleteOnDrop {
fn drop(&mut self) {
let file = self.0.clone();
// Not using dc_delete_file() here because it would send a DeletedBlobFile event
async_std::task::block_on(async move { fs::remove_file(file).await.ok() });
}
}
async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<()> {
let file = File::create(temp_path).await?;
let mut builder = async_tar::Builder::new(file);
// append_path_with_name() wants the source path as the first argument, append_dir_all() wants it as the second argument.
builder
.append_path_with_name(context.get_dbfile(), DBFILE_BACKUP_NAME)
.await?;
let read_dir: Vec<_> = fs::read_dir(context.get_blobdir()).await?.collect().await;
let count = read_dir.len();
let mut written_files = 0;
for entry in read_dir.into_iter() {
let entry = entry?;
let name = entry.file_name();
if !entry.file_type().await?.is_file() {
warn!(
context,
"Export: Found dir entry {} that is not a file, ignoring",
name.to_string_lossy()
);
continue;
}
let mut file = File::open(entry.path()).await?;
let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(name);
builder.append_file(path_in_archive, &mut file).await?;
written_files += 1;
let progress = 1000 * written_files / count;
if progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
emit_event!(context, EventType::ImexProgress(progress));
}
}
builder.finish().await?;
Ok(())
}
async fn export_backup_old(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
// FIXME: we should write to a temporary file first and rename it on success. this would guarantee the backup is complete.
// let dest_path_filename = dc_get_next_backup_file(context, dir, res);
let now = time();
let dest_path_filename = get_next_backup_path_old(dir, now).await?;
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;", paramsv![]).await.ok();
context.sql.execute("VACUUM;", paramsx![]).await.ok();
// we close the database during the copy of the dbfile
context.sql.close().await;
@@ -789,24 +529,21 @@ async fn export_backup_old(context: &Context, dir: impl AsRef<Path>) -> Result<(
);
}
let dest_sql = Sql::new();
dest_sql
.open(context, &dest_path_filename, false)
.await
.with_context(|| format!("could not open exported database {}", dest_path_string))?;
dest_sql.open(context, &dest_path_filename, false).await?;
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)
.await?;
context.emit_event(EventType::ImexFileWritten(dest_path_filename));
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;
@@ -819,7 +556,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);",
paramsv![],
paramsx![],
)
.await?;
}
@@ -831,41 +568,38 @@ async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
info!(context, "EXPORT: total_files_cnt={}", total_files_cnt);
sql.with_conn_async(|conn| async move {
// scan directory, pass 2: copy files
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
// 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(EventType::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") {
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() {
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() {
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])?;
}
// bail out if we can't insert
sql.execute(
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
paramsx![name.as_ref(), buf],
)
.await?;
}
Ok(())
})
.await?;
}
Ok(())
}
@@ -928,30 +662,19 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let mut export_errors = 0;
let 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 pool = context.sql.get_pool().await?;
Ok((id, public_key, private_key, is_default))
},
|keys| {
keys.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
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);
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
@@ -975,6 +698,7 @@ async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
}
ensure!(export_errors == 0, "errors while exporting keys");
Ok(())
}
@@ -1010,7 +734,7 @@ where
if res.is_err() {
error!(context, "Cannot write key to {}", file_name.display());
} else {
context.emit_event(EventType::ImexFileWritten(file_name));
context.emit_event(Event::ImexFileWritten(file_name));
}
res
}
@@ -1084,21 +808,6 @@ mod tests {
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[async_std::test]
async fn test_export_and_import_key() {
let context = TestContext::new().await;
context.configure_alice().await;
let blobdir = "$BLOBDIR";
assert!(imex(&context.ctx, ImexMode::ExportSelfKeys, Some(blobdir))
.await
.is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
assert!(imex(&context.ctx, ImexMode::ImportSelfKeys, Some(blobdir))
.await
.is_ok());
}
#[test]
fn test_normalize_setup_code() {
let norm = normalize_setup_code("123422343234423452346234723482349234");

View File

@@ -6,37 +6,38 @@
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};
use crate::config::Config;
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::EventType;
use crate::events::Event;
use crate::imap::*;
use crate::location;
use crate::login_param::LoginParam;
use crate::message::MsgId;
use crate::message::{self, Message, MessageState};
use crate::mimefactory::MimeFactory;
use crate::param::*;
use crate::smtp::Smtp;
use crate::{blob::BlobObject, contact::normalize_name, contact::Modifier, contact::Origin};
use crate::{scheduler::InterruptInfo, sql};
// results in ~3 weeks for the last backoff timespan
const JOB_RETRIES: u32 = 17;
/// Thread IDs
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
#[repr(i32)]
pub(crate) enum Thread {
Unknown = 0,
@@ -74,17 +75,7 @@ impl Default for Thread {
}
#[derive(
Debug,
Display,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, FromPrimitive, ToPrimitive, Sqlx,
)]
#[repr(i32)]
pub enum Action {
@@ -92,7 +83,8 @@ pub enum Action {
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
Housekeeping = 105, // low priority ...
FetchExistingMsgs = 110,
EmptyServer = 107,
OldDeleteMsgOnImap = 110,
MarkseenMsgOnImap = 130,
// Moving message is prioritized lower than deletion so we don't
@@ -100,10 +92,6 @@ pub enum Action {
MoveMsg = 200,
DeleteMsgOnImap = 210,
// UID synchronization is high-priority to make sure correct UIDs
// are used by message moving/deletion.
ResyncFolders = 300,
// Jobs in the SMTP-thread, range from DC_SMTP_THREAD..DC_SMTP_THREAD+999
MaybeSendLocations = 5005, // low priority ...
MaybeSendLocationsEnded = 5007,
@@ -125,9 +113,9 @@ impl From<Action> for Thread {
Unknown => Thread::Unknown,
Housekeeping => Thread::Imap,
FetchExistingMsgs => Thread::Imap,
OldDeleteMsgOnImap => Thread::Imap,
DeleteMsgOnImap => Thread::Imap,
ResyncFolders => Thread::Imap,
EmptyServer => Thread::Imap,
MarkseenMsgOnImap => Thread::Imap,
MoveMsg => Thread::Imap,
@@ -157,6 +145,32 @@ 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();
@@ -182,7 +196,7 @@ impl Job {
if self.job_id != 0 {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
.execute("DELETE FROM jobs WHERE id=?;", paramsx![self.job_id as i32])
.await?;
}
@@ -202,22 +216,22 @@ impl Job {
.sql
.execute(
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
paramsv![
paramsx![
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 (?,?,?,?,?,?);",
paramsv![
paramsx![
self.added_timestamp,
thread,
self.action,
self.foreign_id,
self.foreign_id as i32,
self.param.to_string(),
self.desired_timestamp
]
@@ -256,48 +270,40 @@ impl Job {
let res = match err {
async_smtp::smtp::error::Error::Permanent(ref response) => {
// Workaround for incorrectly configured servers returning permanent errors
// instead of temporary ones.
let maybe_transient = match response.code {
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,
..
} => {
// 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,
};
} => Status::RetryLater,
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())
_ => {
// 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) => error!(
context,
"couldn't load chat_id to inform user about SMTP error: {}", e
),
};
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
}
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)))
}
}
}
async_smtp::smtp::error::Error::Transient(_) => {
@@ -341,9 +347,12 @@ impl Job {
pub(crate) async fn send_msg_to_smtp(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
// SMTP server, if not yet done
if let Err(err) = smtp.connect_configured(context).await {
warn!(context, "SMTP connection failure: {:?}", err);
return Status::RetryLater;
if !smtp.is_connected().await {
let loginparam = LoginParam::from_database(context, "configured_").await;
if let Err(err) = smtp.connect(context, &loginparam).await {
warn!(context, "SMTP connection failure: {:?}", err);
return Status::RetryLater;
}
}
let filename = job_try!(job_try!(self
@@ -401,40 +410,32 @@ 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();
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);
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);
}
}
}
Ok((job_ids, rfc724_mids))
}
@@ -486,9 +487,12 @@ impl Job {
let recipients = vec![recipient];
// connect to SMTP server, if not yet done
if let Err(err) = smtp.connect_configured(context).await {
warn!(context, "SMTP connection failure: {:?}", err);
return Status::RetryLater;
if !smtp.is_connected().await {
let loginparam = LoginParam::from_database(context, "configured_").await;
if let Err(err) = smtp.connect(context, &loginparam).await {
warn!(context, "SMTP connection failure: {:?}", err);
return Status::RetryLater;
}
}
self.smtp_send(context, recipients, body, self.job_id, smtp, || {
@@ -562,11 +566,6 @@ impl Job {
context,
"The message is deleted from the server when all parts are deleted.",
);
} else if cnt == 0 {
warn!(
context,
"The message {} has no UID on the server to delete", &msg.rfc724_mid
);
} else {
/* if this is the last existing part of the message,
we delete the message from the server */
@@ -621,74 +620,19 @@ impl Job {
}
}
/// Read the recipients from old emails sent by the user and add them as contacts.
/// This way, we can already offer them some email addresses they can write to.
///
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
/// and show them in the chat list.
async fn fetch_existing_msgs(&mut self, context: &Context, imap: &mut Imap) -> Status {
async fn empty_server(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
add_all_recipients_as_contacts(context, imap, Config::ConfiguredSentboxFolder).await;
add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await;
add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).await;
if context.get_config_bool(Config::FetchExisting).await {
for config in &[
Config::ConfiguredMvboxFolder,
Config::ConfiguredInboxFolder,
Config::ConfiguredSentboxFolder,
] {
if let Some(folder) = context.get_config(*config).await {
if let Err(e) = imap.fetch_new_messages(context, folder, true).await {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(context, "Could not fetch messages, retrying: {:#}", e);
return Status::RetryLater;
};
}
if self.foreign_id & DC_EMPTY_MVBOX > 0 {
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await {
imap.empty_folder(context, &mvbox_folder).await;
}
}
info!(context, "Done fetching existing messages.");
Status::Finished(Ok(()))
}
/// Synchronizes UIDs for sentbox, inbox and mvbox, in this order.
///
/// If a copy of the message is present in multiple folders, mvbox
/// is preferred to inbox, which is in turn preferred to
/// sentbox. This is because in the database it is impossible to
/// store multiple UIDs for one message, so we prefer to
/// automatically delete messages in the folders managed by Delta
/// Chat in contrast to the Sent folder, which is normally managed
/// by the user via webmail or another email client.
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
if let Some(sentbox_folder) = &context.get_config(Config::ConfiguredSentboxFolder).await {
job_try!(
imap.resync_folder_uids(context, sentbox_folder.to_string())
.await
);
}
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
job_try!(
imap.resync_folder_uids(context, inbox_folder.to_string())
.await
);
}
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await {
job_try!(
imap.resync_folder_uids(context, mvbox_folder.to_string())
.await
);
if self.foreign_id & DC_EMPTY_INBOX > 0 {
imap.empty_folder(context, "INBOX").await;
}
Status::Finished(Ok(()))
}
@@ -702,21 +646,7 @@ 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();
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 {
match imap.set_seen(context, folder, msg.server_uid).await {
ImapActionResult::RetryLater => Status::RetryLater,
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
ImapActionResult::Success | ImapActionResult::Failed => {
@@ -724,18 +654,7 @@ 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 {
@@ -753,21 +672,27 @@ impl Job {
pub async fn kill_action(context: &Context, action: Action) -> bool {
context
.sql
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
.execute("DELETE FROM jobs WHERE action=?;", paramsx![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(",")
),
job_ids.iter().map(|i| i as &dyn crate::ToSql).collect(),
args,
)
.await?;
Ok(())
@@ -776,7 +701,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=?;", paramsv![action])
.exists("SELECT id FROM jobs WHERE action=?;", paramsx![action])
.await
.unwrap_or_default()
}
@@ -785,58 +710,10 @@ 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_get_value(
context,
"SELECT chat_id FROM msgs WHERE id=?",
paramsv![msg_id],
)
.query_value("SELECT chat_id FROM msgs WHERE id=?", paramsx![])
.await
.unwrap_or_default();
context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
}
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) {
let mailbox = if let Some(m) = context.get_config(folder).await {
m
} else {
return;
};
if let Err(e) = imap.select_with_uidvalidity(context, &mailbox).await {
warn!(context, "Could not select {}: {}", mailbox, e);
return;
}
match imap.get_all_recipients(context).await {
Ok(contacts) => {
let mut any_modified = false;
for contact in contacts {
let display_name_normalized = contact
.display_name
.as_ref()
.map(normalize_name)
.unwrap_or_default();
match Contact::add_or_lookup(
context,
display_name_normalized,
contact.addr,
Origin::OutgoingTo,
)
.await
{
Ok((_, modified)) => {
if modified != Modifier::None {
any_modified = true;
}
}
Err(e) => warn!(context, "Could not add recipient: {}", e),
}
}
if any_modified {
context.emit_event(EventType::ContactsChanged(None));
}
}
Err(e) => warn!(context, "Could not add recipients: {}", e),
};
context.emit_event(Event::MsgDelivered { chat_id, msg_id });
}
/// Constructs a job for sending a message.
@@ -961,6 +838,25 @@ 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(
@@ -1083,18 +979,23 @@ async fn perform_job_action(
Action::MaybeSendLocationsEnded => {
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::ResyncFolders => job.resync_folders(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::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
Action::Housekeeping => {
sql::housekeeping(context).await;
if let Err(err) = sql::housekeeping(context).await {
error!(context, "housekeeping failed: {}", err);
}
Status::Finished(Ok(()))
}
};
info!(context, "Finished immediate try {} of job {}", tries, job);
info!(
context,
"Inbox finished immediate try {} of job {}", tries, job
);
try_res
}
@@ -1119,15 +1020,6 @@ async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
Ok(())
}
pub(crate) async fn schedule_resync(context: &Context) {
kill_action(context, Action::ResyncFolders).await;
add(
context,
Job::new(Action::ResyncFolders, 0, Params::new(), 0),
)
.await;
}
/// Creates a job.
pub fn create(action: Action, foreign_id: i32, param: Params, delay_seconds: i64) -> Result<Job> {
ensure!(
@@ -1150,10 +1042,10 @@ pub async fn add(context: &Context, job: Job) {
match action {
Action::Unknown => unreachable!(),
Action::Housekeeping
| Action::EmptyServer
| Action::OldDeleteMsgOnImap
| Action::DeleteMsgOnImap
| Action::ResyncFolders
| Action::MarkseenMsgOnImap
| Action::FetchExistingMsgs
| Action::MoveMsg => {
info!(context, "interrupt: imap");
context
@@ -1187,9 +1079,8 @@ pub(crate) async fn load_next(
info!(context, "loading job for {}-thread", thread);
let query;
let params;
let params: Box<dyn Fn() -> sqlx::sqlite::SqliteArguments<'static> + 'static + Send>;
let t = time();
let m;
let thread_i = thread as i64;
if let Some(msg_id) = info.msg_id {
@@ -1200,8 +1091,7 @@ WHERE thread=? AND foreign_id=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
m = msg_id;
params = paramsv![thread_i, m];
params = Box::new(move || paramsx![thread_i, msg_id]);
} else if !info.probe_network {
// processing for first-try and after backoff-timeouts:
// process jobs in the order they were added.
@@ -1212,7 +1102,7 @@ WHERE thread=? AND desired_timestamp<=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
params = paramsv![thread_i, t];
params = Box::new(move || paramsx![thread_i, t]);
} else {
// processing after call to dc_maybe_network():
// process _all_ pending jobs that failed before
@@ -1224,27 +1114,12 @@ WHERE thread=? AND tries>0
ORDER BY desired_timestamp, action DESC
LIMIT 1;
"#;
params = paramsv![thread_i];
params = Box::new(move || paramsx![thread_i]);
};
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;
let job: Option<Job> = loop {
let p = params();
let job_res = context.sql.query_row_optional(query, p).await;
match job_res {
Ok(job) => break job,
@@ -1253,15 +1128,13 @@ LIMIT 1;
info!(context, "cleaning up job, because of {}", err);
// TODO: improve by only doing a single query
match context
.sql
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
.await
{
let p = params();
let id: Result<i32, _> = context.sql.query_value(query, p).await;
match id {
Ok(id) => {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
.execute("DELETE FROM jobs WHERE id=?;", paramsx![id])
.await
.ok();
}
@@ -1311,7 +1184,7 @@ mod tests {
"INSERT INTO jobs
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
VALUES (?, ?, ?, ?, ?, ?);",
paramsv![
paramsx![
now,
Thread::from(Action::MoveMsg),
Action::MoveMsg,

View File

@@ -116,22 +116,21 @@ impl DcKey for SignedPublicKey {
type KeyType = SignedPublicKey;
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
let res: std::result::Result<Vec<u8>, _> = context
.sql
.query_row(
.query_value(
r#"
SELECT public_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
paramsv![],
|row| row.get::<_, Vec<u8>>(0),
paramsx![],
)
.await
{
.await;
match res {
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
}
@@ -161,22 +160,21 @@ impl DcKey for SignedSecretKey {
type KeyType = SignedSecretKey;
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
let res: std::result::Result<Vec<u8>, _> = context
.sql
.query_row(
.query_value(
r#"
SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
paramsv![],
|row| row.get::<_, Vec<u8>>(0),
paramsx![],
)
.await
{
.await;
match res {
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
let keypair = generate_keypair(context).await?;
Ok(keypair.secret)
}
@@ -222,32 +220,31 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.await
.ok_or(Error::NoConfiguredAddr)?;
.ok_or_else(|| Error::NoConfiguredAddr)?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().await;
// Check if the key appeared while we were waiting on the lock.
match context
let res: std::result::Result<(Vec<u8>, Vec<u8>), _> = context
.sql
.query_row(
r#"
SELECT public_key, private_key
FROM keypairs
WHERE addr=?1
WHERE addr=?
AND is_default=1;
"#,
paramsv![addr],
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
paramsx![addr.to_string()],
)
.await
{
.await;
match res {
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
addr,
public: SignedPublicKey::from_slice(&pub_bytes)?,
secret: SignedSecretKey::from_slice(&sec_bytes)?,
}),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let start = std::time::SystemTime::now();
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
let start = std::time::Instant::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
.unwrap_or_default();
info!(context, "Generating keypair with type {}", keytype);
@@ -258,7 +255,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().unwrap_or_default().as_secs()
start.elapsed().as_secs()
);
Ok(keypair)
}
@@ -321,14 +318,14 @@ pub async fn store_self_keypair(
.sql
.execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
paramsv![public_key, secret_key],
paramsx![&public_key, &secret_key],
)
.await
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
.execute("UPDATE keypairs SET is_default=0;", paramsx![])
.await
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
@@ -340,7 +337,7 @@ pub async fn store_self_keypair(
let addr = keypair.addr.to_string();
let t = time();
let params = paramsv![addr, is_default, public_key, secret_key, t];
let params = paramsx![addr, is_default, public_key, secret_key, t];
context
.sql
.execute(
@@ -355,7 +352,7 @@ pub async fn store_self_keypair(
}
/// A key fingerprint
#[derive(Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Fingerprint(Vec<u8>);
impl Fingerprint {
@@ -375,14 +372,6 @@ impl Fingerprint {
}
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Fingerprint")
.field("hex", &self.hex())
.finish()
}
}
/// Make a human-readable fingerprint.
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -431,9 +420,11 @@ mod tests {
use std::error::Error;
use async_std::sync::Arc;
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
lazy_static! {
static ref KEYPAIR: KeyPair = alice_keypair();
}
#[test]
fn test_from_armored_string() {
@@ -535,11 +526,11 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
#[test]
fn test_from_slice_bad_data() {
let mut bad_data: [u8; 4096] = [0; 4096];
for (i, v) in bad_data.iter_mut().enumerate() {
*v = (i & 0xff) as u8;
for i in 0..4096 {
bad_data[i] = (i & 0xff) as u8;
}
for j in 0..(4096 / 40) {
let slice = &bad_data.get(j..j + 4096 / 2 + j).unwrap();
let slice = &bad_data[j..j + 4096 / 2 + j];
assert!(SignedPublicKey::from_slice(slice).is_err());
assert!(SignedSecretKey::from_slice(slice).is_err());
}
@@ -599,7 +590,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
let ctx0 = ctx.clone();
let thr0 =
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx0)));
let ctx1 = ctx;
let ctx1 = ctx.clone();
let thr1 =
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx1)));
let res0 = thr0.join().unwrap();
@@ -622,10 +613,12 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
let ctx1 = ctx.clone();
let nrows = || async {
ctx1.sql
.query_get_value::<u32>(&ctx1, "SELECT COUNT(*) FROM keypairs;", paramsv![])
let val: i32 = ctx1
.sql
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
.await
.unwrap()
.unwrap();
val as usize
};
assert_eq!(nrows().await, 0);
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)

View File

@@ -1,26 +1,15 @@
#![forbid(unsafe_code)]
#![deny(
clippy::correctness,
missing_debug_implementations,
clippy::all,
clippy::indexing_slicing
)]
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
#![allow(clippy::match_bool)]
#[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]
@@ -48,13 +37,11 @@ pub mod constants;
pub mod contact;
pub mod context;
mod e2ee;
pub mod ephemeral;
mod imap;
pub mod imex;
mod scheduler;
#[macro_use]
pub mod job;
mod format_flowed;
pub mod key;
mod keyring;
pub mod location;
@@ -80,8 +67,6 @@ mod dehtml;
pub mod dc_receive_imf;
pub mod dc_tools;
pub mod accounts;
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";

View File

@@ -1,5 +1,6 @@
//! Location handling
use async_std::prelude::*;
use bitflags::bitflags;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
@@ -9,7 +10,7 @@ use crate::constants::*;
use crate::context::*;
use crate::dc_tools::*;
use crate::error::{ensure, Error};
use crate::events::EventType;
use crate::events::Event;
use crate::job::{self, Job};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
@@ -37,6 +38,34 @@ 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>,
@@ -197,14 +226,16 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
if context
.sql
.execute(
"UPDATE chats \
SET locations_send_begin=?, \
locations_send_until=? \
WHERE id=?",
paramsv![
r#"
UPDATE chats
SET locations_send_begin=?,
locations_send_until=?
WHERE id=?
"#,
paramsx![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id,
chat_id
],
)
.await
@@ -227,7 +258,7 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
.await;
chat::add_info_msg(context, chat_id, stock_str).await;
}
context.emit_event(EventType::ChatModified(chat_id));
context.emit_event(Event::ChatModified(chat_id));
if 0 != seconds {
schedule_maybe_send_locations(context, false).await;
job::add(
@@ -260,53 +291,60 @@ 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>?;",
paramsv![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
paramsx![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) -> bool {
pub async fn set(
context: &Context,
latitude: f64,
longitude: f64,
accuracy: f64,
) -> Result<bool, Error> {
if latitude == 0.0 && longitude == 0.0 {
return true;
return Ok(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);
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;
}
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 continue_streaming {
context.emit_event(EventType::LocationChanged(Some(DC_CONTACT_ID_SELF)));
};
schedule_maybe_send_locations(context, false).await;
}
continue_streaming
if continue_streaming {
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
};
schedule_maybe_send_locations(context, false).await;
Ok(continue_streaming)
}
pub async fn get_range(
@@ -319,16 +357,21 @@ pub async fn get_range(
if timestamp_to == 0 {
timestamp_to = time() + 10;
}
context
.sql
.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![
.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![
if chat_id.is_unset() { 1 } else { 0 },
chat_id,
if contact_id == 0 { 1 } else { 0 },
@@ -336,36 +379,6 @@ pub async fn get_range(
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()
@@ -379,9 +392,9 @@ fn is_marker(txt: &str) -> bool {
pub async fn delete_all(context: &Context) -> Result<(), Error> {
context
.sql
.execute("DELETE FROM locations;", paramsv![])
.execute("DELETE FROM locations;", paramsx![])
.await?;
context.emit_event(EventType::LocationChanged(None));
context.emit_event(Event::LocationChanged(None));
Ok(())
}
@@ -393,16 +406,10 @@ 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) = context.sql.query_row(
let (locations_send_begin, locations_send_until, locations_last_sent): (i64, i64, i64) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
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?;
paramsx![chat_id]
).await?;
let now = time();
let mut location_count = 0;
@@ -413,40 +420,41 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
self_addr,
);
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)?);
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;
}
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>";
}
@@ -469,7 +477,7 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String
<Document>\n\
<Placemark>\
<Timestamp><when>{}</when></Timestamp>\
<Point><coordinates>{},{}</coordinates></Point>\
<Point><coordinates>{:.2},{:.2}</coordinates></Point>\
</Placemark>\n\
</Document>\n\
</kml>",
@@ -488,7 +496,7 @@ pub async fn set_kml_sent_timestamp(
.sql
.execute(
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
paramsv![timestamp, chat_id],
paramsx![timestamp, chat_id],
)
.await?;
Ok(())
@@ -503,7 +511,7 @@ pub async fn set_msg_location_id(
.sql
.execute(
"UPDATE msgs SET location_id=? WHERE id=?;",
paramsv![location_id, msg_id],
paramsx![location_id as i32, msg_id],
)
.await?;
@@ -530,50 +538,51 @@ pub async fn save(
accuracy,
..
} = location;
let (loc_id, ts) = context
let exists: Option<i32> = context
.sql
.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 (?,?,?,?,?,?,?);",
)?;
.query_value_optional(
"SELECT id FROM locations WHERE timestamp=? AND from_id=?",
paramsx![timestamp, contact_id as i32],
)
.await?;
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
if independent || !exists {
stmt_insert.execute(paramsv![
if independent || exists.is_none() {
context
.sql
.execute(
r#"
INSERT INTO locations
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent)
VALUES (?,?,?,?,?,?,?);
"#,
paramsx![
timestamp,
contact_id as i32,
chat_id,
latitude,
longitude,
accuracy,
independent,
])?;
independent
],
)
.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;
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?;
}
}
}
Ok(newest_location_id)
@@ -587,85 +596,75 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
" ----------------- MAYBE_SEND_LOCATIONS -------------- ",
);
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 pool = match context.sql.get_pool().await {
Ok(pool) => pool,
Err(err) => {
return job::Status::Finished(Err(err.into()));
}
};
// 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;
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);
if rows.is_ok() {
let msgs = context
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
.sql
.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)
})
.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,
],
)
.await
.unwrap_or_default();
for (chat_id, mut msg) in msgs.into_iter() {
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);
// TODO: better error handling
chat::send_msg(context, chat_id, &mut msg)
.await
@@ -676,6 +675,7 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
if continue_streaming {
schedule_maybe_send_locations(context, true).await;
}
job::Status::Finished(Ok(()))
}
@@ -689,13 +689,12 @@ pub(crate) async fn job_maybe_send_locations_ended(
let chat_id = ChatId::new(job.foreign_id);
let (send_begin, send_until) = job_try!(
let (send_begin, send_until): (i64, i64) = job_try!(
context
.sql
.query_row(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
paramsv![chat_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
paramsx![chat_id],
)
.await
);
@@ -707,15 +706,15 @@ 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=?",
paramsv![chat_id],
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
paramsx![chat_id],
).await);
let stock_str = context
.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0)
.await;
chat::add_info_msg(context, chat_id, stock_str).await;
context.emit_event(EventType::ChatModified(chat_id));
context.emit_event(Event::ChatModified(chat_id));
}
}
job::Status::Finished(Ok(()))
@@ -723,8 +722,6 @@ pub(crate) async fn job_maybe_send_locations_ended(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::test_utils::TestContext;
@@ -759,22 +756,4 @@ mod tests {
assert!(locations_ref[1].accuracy < 2.6f64);
assert_eq!(locations_ref[1].timestamp, 1544739072);
}
#[async_std::test]
async fn test_get_message_kml() {
let context = TestContext::new().await;
let timestamp = 1598490000;
let xml = get_message_kml(timestamp, 51.423723f64, 8.552556f64);
let kml = Kml::parse(&context.ctx, xml.as_bytes()).expect("parsing failed");
let locations_ref = &kml.locations;
assert_eq!(locations_ref.len(), 1);
assert!(locations_ref[0].latitude >= 51.423723f64);
assert!(locations_ref[0].latitude < 51.423724f64);
assert!(locations_ref[0].longitude >= 8.552556f64);
assert!(locations_ref[0].longitude < 8.552557f64);
assert!(locations_ref[0].accuracy.abs() < f64::EPSILON);
assert_eq!(locations_ref[0].timestamp, timestamp);
}
}

View File

@@ -11,7 +11,7 @@ macro_rules! info {
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::EventType::Info(full));
emit_event!($ctx, $crate::Event::Info(full));
}};
}
@@ -26,7 +26,7 @@ macro_rules! warn {
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::EventType::Warning(full));
emit_event!($ctx, $crate::Event::Warning(full));
}};
}
@@ -37,18 +37,7 @@ macro_rules! error {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
emit_event!($ctx, $crate::EventType::Error(formatted));
}};
}
#[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::EventType::ErrorNetwork(formatted));
emit_event!($ctx, $crate::Event::Error(formatted));
}};
}

View File

@@ -3,16 +3,13 @@
use std::borrow::Cow;
use std::fmt;
use crate::{context::Context, provider::Socket};
use crate::context::Context;
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Display, FromPrimitive)]
#[repr(i32)]
#[strum(serialize_all = "snake_case")]
pub enum CertificateChecks {
/// Same as AcceptInvalidCertificates unless overridden by
/// `strict_tls` setting in provider database.
Automatic = 0,
Strict = 1,
/// Same as AcceptInvalidCertificates
@@ -28,29 +25,30 @@ impl Default for CertificateChecks {
}
}
/// Login parameters for a single server, either IMAP or SMTP
#[derive(Default, Debug, Clone)]
pub struct ServerLoginParam {
pub server: String,
pub user: String,
pub password: String,
pub port: u16,
pub security: Socket,
/// TLS options: whether to allow invalid certificates and/or
/// invalid hostnames
pub certificate_checks: CertificateChecks,
}
#[derive(Default, Debug, Clone)]
#[derive(Default, Debug)]
pub struct LoginParam {
pub addr: String,
pub imap: ServerLoginParam,
pub smtp: ServerLoginParam,
pub mail_server: String,
pub mail_user: String,
pub mail_pw: String,
pub mail_port: i32,
/// IMAP TLS options: whether to allow invalid certificates and/or invalid hostnames
pub imap_certificate_checks: CertificateChecks,
pub send_server: String,
pub send_user: String,
pub send_pw: String,
pub send_port: i32,
/// SMTP TLS options: whether to allow invalid certificates and/or invalid hostnames
pub smtp_certificate_checks: CertificateChecks,
pub server_flags: i32,
}
impl LoginParam {
/// Create a new `LoginParam` with default values.
pub fn new() -> Self {
Default::default()
}
/// Read the login parameters from the database.
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
let prefix = prefix.as_ref();
@@ -58,96 +56,67 @@ impl LoginParam {
let key = format!("{}addr", prefix);
let addr = sql
.get_raw_config(context, key)
.get_raw_config(key)
.await
.unwrap_or_default()
.trim()
.to_string();
let key = format!("{}mail_server", prefix);
let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_server = sql.get_raw_config(key).await.unwrap_or_default();
let key = format!("{}mail_port", prefix);
let mail_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let mail_port = sql.get_raw_config_int(key).await.unwrap_or_default();
let key = format!("{}mail_user", prefix);
let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_user = sql.get_raw_config(key).await.unwrap_or_default();
let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}mail_security", prefix);
let mail_security = sql
.get_raw_config_int(context, key)
.await
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let mail_pw = sql.get_raw_config(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(context, key).await {
if let Some(certificate_checks) = sql.get_raw_config_int(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(context, key).await.unwrap_or_default();
let send_server = sql.get_raw_config(key).await.unwrap_or_default();
let key = format!("{}send_port", prefix);
let send_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let send_port = sql.get_raw_config_int(key).await.unwrap_or_default();
let key = format!("{}send_user", prefix);
let send_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let send_user = sql.get_raw_config(key).await.unwrap_or_default();
let key = format!("{}send_pw", prefix);
let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}send_security", prefix);
let send_security = sql
.get_raw_config_int(context, key)
.await
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let send_pw = sql.get_raw_config(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(context, key).await {
if let Some(certificate_checks) = sql.get_raw_config_int(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(context, key)
.await
.unwrap_or_default();
let server_flags = sql.get_raw_config_int(key).await.unwrap_or_default();
LoginParam {
addr,
imap: ServerLoginParam {
server: mail_server,
user: mail_user,
password: mail_pw,
port: mail_port as u16,
security: mail_security,
certificate_checks: imap_certificate_checks,
},
smtp: ServerLoginParam {
server: send_server,
user: send_user,
password: send_pw,
port: send_port as u16,
security: send_security,
certificate_checks: smtp_certificate_checks,
},
mail_server,
mail_user,
mail_pw,
mail_port,
imap_certificate_checks,
send_server,
send_user,
send_pw,
send_port,
smtp_certificate_checks,
server_flags,
}
}
@@ -165,51 +134,41 @@ impl LoginParam {
sql.set_raw_config(context, key, Some(&self.addr)).await?;
let key = format!("{}mail_server", prefix);
sql.set_raw_config(context, key, Some(&self.imap.server))
sql.set_raw_config(context, key, Some(&self.mail_server))
.await?;
let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(context, key, self.imap.port as i32)
.await?;
sql.set_raw_config_int(context, key, self.mail_port).await?;
let key = format!("{}mail_user", prefix);
sql.set_raw_config(context, key, Some(&self.imap.user))
sql.set_raw_config(context, key, Some(&self.mail_user))
.await?;
let key = format!("{}mail_pw", prefix);
sql.set_raw_config(context, key, Some(&self.imap.password))
.await?;
let key = format!("{}mail_security", prefix);
sql.set_raw_config_int(context, key, self.imap.security as i32)
sql.set_raw_config(context, key, Some(&self.mail_pw))
.await?;
let key = format!("{}imap_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.imap.certificate_checks as i32)
sql.set_raw_config_int(context, key, self.imap_certificate_checks as i32)
.await?;
let key = format!("{}send_server", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.server))
sql.set_raw_config(context, key, Some(&self.send_server))
.await?;
let key = format!("{}send_port", prefix);
sql.set_raw_config_int(context, key, self.smtp.port as i32)
.await?;
sql.set_raw_config_int(context, key, self.send_port).await?;
let key = format!("{}send_user", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.user))
sql.set_raw_config(context, key, Some(&self.send_user))
.await?;
let key = format!("{}send_pw", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.password))
.await?;
let key = format!("{}send_security", prefix);
sql.set_raw_config_int(context, key, self.smtp.security as i32)
sql.set_raw_config(context, key, Some(&self.send_pw))
.await?;
let key = format!("{}smtp_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.smtp.certificate_checks as i32)
sql.set_raw_config_int(context, key, self.smtp_certificate_checks as i32)
.await?;
let key = format!("{}server_flags", prefix);
@@ -231,24 +190,16 @@ impl fmt::Display for LoginParam {
f,
"{} imap:{}:{}:{}:{}:cert_{} smtp:{}:{}:{}:{}:cert_{} {}",
unset_empty(&self.addr),
unset_empty(&self.imap.user),
if !self.imap.password.is_empty() {
pw
} else {
unset
},
unset_empty(&self.imap.server),
self.imap.port,
self.imap.certificate_checks,
unset_empty(&self.smtp.user),
if !self.smtp.password.is_empty() {
pw
} else {
unset
},
unset_empty(&self.smtp.server),
self.smtp.port,
self.smtp.certificate_checks,
unset_empty(&self.mail_user),
if !self.mail_pw.is_empty() { pw } else { unset },
unset_empty(&self.mail_server),
self.mail_port,
self.imap_certificate_checks,
unset_empty(&self.send_user),
if !self.send_pw.is_empty() { pw } else { unset },
unset_empty(&self.send_server),
self.send_port,
self.smtp_certificate_checks,
flags_readable,
)
}
@@ -277,6 +228,30 @@ fn get_readable_flags(flags: i32) -> String {
res += "AUTH_NORMAL ";
flag_added = true;
}
if 1 << bit == 0x100 {
res += "IMAP_STARTTLS ";
flag_added = true;
}
if 1 << bit == 0x200 {
res += "IMAP_SSL ";
flag_added = true;
}
if 1 << bit == 0x400 {
res += "IMAP_PLAIN ";
flag_added = true;
}
if 1 << bit == 0x10000 {
res += "SMTP_STARTTLS ";
flag_added = true;
}
if 1 << bit == 0x20000 {
res += "SMTP_SSL ";
flag_added = true;
}
if 1 << bit == 0x40000 {
res += "SMTP_PLAIN ";
flag_added = true;
}
if flag_added {
res += &format!("{:#0x}", 1 << bit);
}

View File

@@ -1,4 +1,4 @@
use deltachat_derive::{FromSql, ToSql};
use deltachat_derive::*;
use crate::key::Fingerprint;
@@ -22,7 +22,7 @@ pub struct Lot {
}
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
pub enum Meaning {
None = 0,
Text1Draft = 1,
@@ -67,7 +67,7 @@ impl Lot {
}
#[repr(i32)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
pub enum LotState {
// Default
Undefined = 0,
@@ -91,9 +91,6 @@ pub enum LotState {
/// text1=domain
QrAccount = 250,
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,
/// id=contact
QrAddr = 320,

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
use async_std::prelude::*;
use chrono::TimeZone;
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
@@ -9,9 +10,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::format_flowed::{format_flowed, format_flowed_quote};
use crate::location;
use crate::message::{self, Message};
use crate::mimeparser::SystemMessage;
@@ -89,32 +88,33 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
if chat.is_self_talk() {
recipients.push((from_displayname.to_string(), from_addr.to_string()));
} else {
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 pool = context.sql.get_pool().await?;
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).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
{
req_mdn = true;
}
}
@@ -122,18 +122,15 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.sql
.query_row(
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE 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),
))
},
paramsx![msg.id],
)
.await?;
.await
.map(|(in_reply_to, references): (String, String)| {
(
render_rfc724_mid_list(&in_reply_to),
render_rfc724_mid_list(&references),
)
})?;
let default_str = context
.stock_str(StockMessage::StatusLine)
@@ -145,7 +142,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
selfstatus: context
.get_config(Config::Selfstatus)
.await
.unwrap_or(default_str),
.unwrap_or_else(|| default_str),
recipients,
timestamp: msg.timestamp_sort,
loaded: Loaded::Message { chat },
@@ -183,7 +180,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let selfstatus = context
.get_config(Config::Selfstatus)
.await
.unwrap_or(default_str);
.unwrap_or_else(|| default_str);
let timestamp = dc_create_smeared_timestamp(context).await;
let res = MimeFactory::<'a, 'b> {
@@ -208,7 +205,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
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)
@@ -222,7 +219,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.filter(|(_, addr)| addr != &self_addr)
{
res.push((
Peerstate::from_addr(self.context, addr).await?,
Peerstate::from_addr(self.context, addr).await.ok(),
addr.as_str(),
));
}
@@ -233,20 +230,26 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
fn is_e2ee_guaranteed(&self) -> bool {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
if chat.typ == Chattype::VerifiedGroup {
return true;
}
!self
let force_plaintext = self
.msg
.param
.get_bool(Param::ForcePlaintext)
.unwrap_or_default()
&& self
.get_int(Param::ForcePlaintext)
.unwrap_or_default();
if force_plaintext == 0 {
return self
.msg
.param
.get_bool(Param::GuaranteeE2ee)
.get_int(Param::GuaranteeE2ee)
.unwrap_or_default()
!= 0;
}
false
}
Loaded::MDN { .. } => false,
}
@@ -255,7 +258,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
fn min_verified(&self) -> PeerstateVerifiedStatus {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
if chat.typ == Chattype::VerifiedGroup {
PeerstateVerifiedStatus::BidirectVerified
} else {
PeerstateVerifiedStatus::Unverified
@@ -265,30 +268,19 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
fn should_force_plaintext(&self) -> bool {
fn should_force_plaintext(&self) -> i32 {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
false
if chat.typ == Chattype::VerifiedGroup {
0
} else {
self.msg
.param
.get_bool(Param::ForcePlaintext)
.get_int(Param::ForcePlaintext)
.unwrap_or_default()
}
}
Loaded::MDN { .. } => true,
}
}
fn should_skip_autocrypt(&self) -> bool {
match &self.loaded {
Loaded::Message { .. } => self
.msg
.param
.get_bool(Param::SkipAutocrypt)
.unwrap_or_default(),
Loaded::MDN { .. } => true,
Loaded::MDN { .. } => ForcePlaintext::NoAutocryptHeader as i32,
}
}
@@ -345,7 +337,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.stock_str(StockMessage::AcSetupMsgSubject)
.await
.into_owned()
} else if chat.typ == Chattype::Group {
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
let re = if self.in_reply_to.is_empty() {
""
} else {
@@ -459,6 +451,21 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
unprotected_headers.push(Header::new("Date".into(), date));
let os_name = &self.context.os_name;
let os_part = os_name
.as_ref()
.map(|s| format!("/{}", s))
.unwrap_or_default();
let version = get_version_str();
// Add a X-Mailer header.
// This is only informational for debugging and may be removed in the release.
// We do not rely on this header as it may be removed by MTAs.
unprotected_headers.push(Header::new(
"X-Mailer".into(),
format!("Delta Chat Core {}{}", version, os_part),
));
unprotected_headers.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
if let Loaded::MDN { .. } = self.loaded {
@@ -481,21 +488,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let min_verified = self.min_verified();
let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext();
let skip_autocrypt = self.should_skip_autocrypt();
let subject_str = self.subject_str().await;
let e2ee_guaranteed = self.is_e2ee_guaranteed();
let encrypt_helper = EncryptHelper::new(self.context).await?;
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 subject = encode_words(&subject_str);
let mut message = match self.loaded {
Loaded::Message { .. } => {
@@ -505,7 +502,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Loaded::MDN { .. } => self.render_mdn().await?,
};
if !skip_autocrypt {
if force_plaintext != ForcePlaintext::NoAutocryptHeader as i32 {
// unless determined otherwise we add the Autocrypt header
let aheader = encrypt_helper.get_aheader().to_string();
unprotected_headers.push(Header::new("Autocrypt".into(), aheader));
@@ -516,21 +513,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let peerstates = self.peerstates_for_recipients().await?;
let should_encrypt =
encrypt_helper.should_encrypt(self.context, e2ee_guaranteed, &peerstates)?;
let is_encrypted = should_encrypt && !force_plaintext;
let is_encrypted = should_encrypt && force_plaintext == 0;
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
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.
@@ -708,11 +697,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let mut placeholdertext = None;
let mut meta_part = None;
if chat.is_protected() {
if chat.typ == Chattype::VerifiedGroup {
protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
}
if chat.typ == Chattype::Group {
if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
protected_headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
let encoded = encode_words(&chat.name);
@@ -781,26 +770,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
"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()));
@@ -846,18 +815,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
};
}
}
SystemMessage::ChatProtectionEnabled => {
protected_headers.push(Header::new(
"Chat-Content".to_string(),
"protection-enabled".to_string(),
));
}
SystemMessage::ChatProtectionDisabled => {
protected_headers.push(Header::new(
"Chat-Content".to_string(),
"protection-disabled".to_string(),
));
}
_ => {}
}
@@ -874,19 +831,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
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
@@ -929,18 +873,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
};
let quoted_text = self
.msg
.quoted_text()
.map(|quote| format_flowed_quote(&quote) + "\r\n\r\n");
let flowed_text = format_flowed(final_text);
let footer = &self.selfstatus;
let message_text = format!(
"{}{}{}{}{}{}",
"{}{}{}{}{}",
fwdhint.unwrap_or_default(),
quoted_text.unwrap_or_default(),
escape_message_footer_marks(&flowed_text),
escape_message_footer_marks(final_text),
if !final_text.is_empty() && !footer.is_empty() {
"\r\n\r\n"
} else {
@@ -952,10 +889,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
// Message is sent as text/plain, with charset = utf-8
let main_part = PartBuilder::new()
.header((
"Content-Type".to_string(),
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
))
.content_type(&mime::TEXT_PLAIN_UTF_8)
.body(message_text);
let mut parts = Vec::new();
@@ -1439,54 +1373,6 @@ 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 {
@@ -1523,7 +1409,7 @@ mod tests {
mf.subject_str().await
}
// Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`.
// Creates a mimefactory for a message that replies "Hi" to the incoming message in `imf_raw`.
async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message {
context
.set_config(Config::ShowEmails, Some("2"))

View File

@@ -2,10 +2,10 @@ use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use deltachat_derive::{FromSql, ToSql};
use deltachat_derive::*;
use lazy_static::lazy_static;
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use once_cell::sync::Lazy;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
@@ -16,14 +16,14 @@ use crate::dc_tools::*;
use crate::dehtml::dehtml;
use crate::e2ee;
use crate::error::{bail, Result};
use crate::events::EventType;
use crate::format_flowed::unformat_flowed;
use crate::events::Event;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::Fingerprint;
use crate::location;
use crate::message;
use crate::param::*;
use crate::peerstate::Peerstate;
use crate::securejoin::handle_degrade_event;
use crate::simplify::*;
use crate::stock::StockMessage;
@@ -46,14 +46,7 @@ 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,
@@ -71,7 +64,7 @@ pub(crate) enum AvatarAction {
Change(String),
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
#[repr(i32)]
pub enum SystemMessage {
Unknown = 0,
@@ -83,13 +76,6 @@ pub enum SystemMessage {
SecurejoinMessage = 7,
LocationStreamingEnabled = 8,
LocationOnly = 9,
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged = 10,
// Chat protection state changed
ChatProtectionEnabled = 11,
ChatProtectionDisabled = 12,
}
impl Default for SystemMessage {
@@ -127,80 +113,58 @@ impl MimeMessage {
// remove headers that are allowed _only_ in the encrypted part
headers.remove("secure-join-fingerprint");
headers.remove("chat-verified");
// Memory location for a possible decrypted message.
let mail_raw;
let mut gossipped_addr = Default::default();
let (mail, signatures, warn_empty_signature) =
match e2ee::try_decrypt(context, &mail, message_time).await {
Ok((raw, signatures)) => {
if let Some(raw) = raw {
// Encrypted, but maybe unsigned message. Only if
// `signatures` set is non-empty, it is a valid
// autocrypt message.
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "decrypted message mime-body:");
println!("{}", String::from_utf8_lossy(&mail_raw));
}
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
// but only if the mail was correctly signed:
if !signatures.is_empty() {
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossipped_addr = update_gossip_peerstates(
context,
message_time,
&mail,
gossip_headers,
)
.await?;
}
// let known protected headers from the decrypted
// part override the unencrypted top-level
// Signature was checked for original From, so we
// do not allow overriding it.
let mut throwaway_from = from.clone();
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
// See https://github.com/deltachat/deltachat-core-rust/issues/1790.
headers.remove("subject");
MimeMessage::merge_headers(
context,
&mut headers,
&mut recipients,
&mut throwaway_from,
&mut chat_disposition_notification_to,
&decrypted_mail.headers,
);
(decrypted_mail, signatures, true)
} else {
// Message was not encrypted
(mail, signatures, false)
let (mail, signatures) = match e2ee::try_decrypt(context, &mail, message_time).await {
Ok((raw, signatures)) => {
if let Some(raw) = raw {
// Valid autocrypt message, encrypted
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "decrypted message mime-body:");
println!("{}", String::from_utf8_lossy(&mail_raw));
}
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossipped_addr =
update_gossip_peerstates(context, message_time, &mail, gossip_headers)
.await?;
// let known protected headers from the decrypted
// part override the unencrypted top-level
MimeMessage::merge_headers(
context,
&mut headers,
&mut recipients,
&mut from,
&mut chat_disposition_notification_to,
&decrypted_mail.headers,
);
(decrypted_mail, signatures)
} else {
// Message was not encrypted
(mail, signatures)
}
Err(err) => {
// continue with the current, still encrypted, mime tree.
// unencrypted parts will be replaced by an error message
// that is added as "the message" to the chat then.
//
// if we just return here, the header is missing
// and the caller cannot display the message
// and try to assign the message to a chat
warn!(context, "decryption failed: {}", err);
(mail, Default::default(), true)
}
};
}
Err(err) => {
// continue with the current, still encrypted, mime tree.
// unencrypted parts will be replaced by an error message
// that is added as "the message" to the chat then.
//
// if we just return here, the header is missing
// and the caller cannot display the message
// and try to assign the message to a chat
warn!(context, "decryption failed: {}", err);
(mail, Default::default())
}
};
let mut parser = MimeMessage {
parts: Vec::new(),
@@ -223,16 +187,9 @@ impl MimeMessage {
failure_report: None,
};
parser.parse_mime_recursive(context, &mail).await?;
parser.maybe_remove_bad_parts().await;
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context)?;
if warn_empty_signature && parser.signatures.is_empty() {
for part in parser.parts.iter_mut() {
part.error = Some("No valid signature".to_string());
}
}
Ok(parser)
}
@@ -257,12 +214,6 @@ 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;
} else if value == "protection-enabled" {
self.is_system_message = SystemMessage::ChatProtectionEnabled;
} else if value == "protection-disabled" {
self.is_system_message = SystemMessage::ChatProtectionDisabled;
}
}
Ok(())
@@ -279,24 +230,10 @@ 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 = {
@@ -330,21 +267,22 @@ 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 {
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.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 part.typ == Viewtype::Image {
if self.parts[0].typ == Viewtype::Image {
if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "sticker" {
part.typ = Viewtype::Sticker;
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Sticker;
}
}
}
let part = &self.parts[0];
if part.typ == Viewtype::Audio
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video
@@ -352,19 +290,17 @@ 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 {
part.param.set_int(Param::Duration, duration_ms);
let part_mut = &mut self.parts[0];
part_mut.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() {
@@ -380,11 +316,12 @@ impl MimeMessage {
}
}
if prepend_subject {
let subj = subject
.find('[')
.and_then(|n| subject.get(..n))
.unwrap_or(subject)
.trim();
let subj = if let Some(n) = subject.find('[') {
&subject[0..n]
} else {
subject
}
.trim();
if !subj.is_empty() {
for part in self.parts.iter_mut() {
@@ -442,7 +379,8 @@ impl MimeMessage {
Some(AvatarAction::Delete)
} else {
let mut i = 0;
while let Some(part) = self.parts.get_mut(i) {
while i != self.parts.len() {
let part = &mut self.parts[i];
if let Some(part_filename) = &part.org_filename {
if part_filename == &header_value {
if let Some(blob) = part.param.get(Param::File) {
@@ -459,11 +397,6 @@ 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()
}
@@ -599,7 +532,7 @@ impl MimeMessage {
part.typ = Viewtype::Text;
part.msg_raw = Some(txt.clone());
part.msg = txt;
part.error = Some("Decryption failed".to_string());
part.error = "Decryption failed".to_string();
self.parts.push(part);
@@ -611,11 +544,11 @@ 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 simply take the first body part and
verify the digital signature." We simpliy 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) */
if let Some(first) = mail.subparts.get(0) {
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first).await?;
}
}
@@ -652,7 +585,7 @@ impl MimeMessage {
}
}
Some(_) => {
if let Some(first) = mail.subparts.get(0) {
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first).await?;
}
}
@@ -714,55 +647,23 @@ impl MimeMessage {
}
};
let mut dehtml_failed = false;
let (simplified_txt, is_forwarded, top_quote) = if decoded_data.is_empty() {
("".to_string(), false, None)
let (simplified_txt, is_forwarded) = if decoded_data.is_empty() {
("".into(), false)
} else {
let is_html = mime_type == mime::TEXT_HTML;
let out = if is_html {
dehtml(&decoded_data).unwrap_or_else(|| {
dehtml_failed = true;
decoded_data.clone()
})
dehtml(&decoded_data)
} else {
decoded_data.clone()
};
simplify(out, self.has_chat_version())
};
let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
{
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
};
let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
&& mime_type.subtype() == mime::PLAIN
&& is_format_flowed
{
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
};
let unflowed_text = unformat_flowed(&simplified_txt, delsp);
let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
(unflowed_text, unflowed_quote)
} else {
(simplified_txt, top_quote)
};
if !simplified_txt.is_empty() || simplified_quote.is_some() {
if !simplified_txt.is_empty() {
let mut part = Part::default();
part.dehtlm_failed = dehtml_failed;
part.typ = Viewtype::Text;
part.mimetype = Some(mime_type);
part.msg = simplified_txt;
if let Some(quote) = simplified_quote {
part.param.set(Param::Quote, quote);
}
part.msg_raw = Some(decoded_data);
self.do_add_single_part(part);
}
@@ -864,12 +765,16 @@ impl MimeMessage {
}
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
self.is_system_message = SystemMessage::Unknown;
if let Some(part) = self.parts.first_mut() {
part.typ = Viewtype::Text;
part.msg = format!("[{}]", error_msg.as_ref());
self.parts.truncate(1);
if self.parts.is_empty() {
return;
}
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> {
@@ -920,11 +825,7 @@ impl MimeMessage {
report: &mailparse::ParsedMail<'_>,
) -> Result<Option<Report>> {
// parse as mailheaders
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_body = report.subparts[1].get_body_raw()?;
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// must be present
@@ -999,21 +900,10 @@ impl MimeMessage {
Ok(None)
}
async fn maybe_remove_bad_parts(&mut self) {
let good_parts = self.parts.iter().filter(|p| !p.dehtlm_failed).count();
if good_parts == 0 {
// We have no good part but show at least one bad part in order to show anything at all
self.parts.truncate(1);
} else if good_parts < self.parts.len() {
self.parts.retain(|p| !p.dehtlm_failed);
}
}
/// 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) {
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();
from.contains("mailer-daemon") || from.contains("mail-daemon")
@@ -1021,8 +911,9 @@ impl MimeMessage {
false
};
if maybe_ndn && self.failure_report.is_none() {
static RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"Message-ID:(.*)").unwrap());
lazy_static! {
static ref RE: regex::Regex = regex::Regex::new(r"Message-ID:(.*)").unwrap();
}
for captures in self
.parts
.iter()
@@ -1042,6 +933,7 @@ impl MimeMessage {
}
}
}
None // Always return None, we just return anything so that we can use the '?' operator.
}
/// Handle reports
@@ -1061,41 +953,23 @@ impl MimeMessage {
if let Some((chat_id, msg_id)) =
message::handle_mdn(context, from_id, original_message_id, sent_timestamp).await
{
context.emit_event(EventType::MsgRead { chat_id, msg_id });
context.emit_event(Event::MsgRead { chat_id, msg_id });
}
}
}
if let Some(failure_report) = &self.failure_report {
let error = parts
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
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()
});
message::handle_ndn(context, failure_report, error).await
}
}
/// Returns timestamp of the parent message.
///
/// If there is no parent message or it is not found in the
/// database, returns None.
pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
let parent_timestamp = if let Some(field) = self
.get(HeaderDef::InReplyTo)
.and_then(|msgid| parse_message_id(msgid).ok())
{
context
.sql
.query_get_value_result(
"SELECT timestamp FROM msgs WHERE rfc724_mid=?",
paramsv![field],
)
.await?
} else {
None
};
Ok(parent_timestamp)
}
}
async fn update_gossip_peerstates(
@@ -1115,17 +989,19 @@ async fn update_gossip_peerstates(
.iter()
.any(|info| info.addr == header.addr.to_lowercase())
{
let mut peerstate = Peerstate::from_addr(context, &header.addr).await?;
let mut peerstate = Peerstate::from_addr(context, &header.addr).await.ok();
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(context, header, message_time);
let p = Peerstate::from_gossip(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
if let Some(peerstate) = peerstate {
peerstate.handle_fingerprint_change(context).await?;
if peerstate.degrade_event.is_some() {
handle_degrade_event(context, &peerstate).await?;
}
}
gossipped_addr.insert(header.addr.clone());
@@ -1155,7 +1031,6 @@ 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();
@@ -1183,21 +1058,11 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
}
fn is_known(key: &str) -> bool {
matches!(
key,
"return-path"
| "date"
| "from"
| "sender"
| "reply-to"
| "to"
| "cc"
| "bcc"
| "message-id"
| "in-reply-to"
| "references"
| "subject"
)
match key {
"return-path" | "date" | "from" | "sender" | "reply-to" | "to" | "cc" | "bcc"
| "message-id" | "in-reply-to" | "references" | "subject" => true,
_ => false,
}
}
#[derive(Debug, Default, Clone)]
@@ -1209,8 +1074,7 @@ pub struct Part {
pub bytes: usize,
pub param: Params,
org_filename: Option<String>,
pub error: Option<String>,
dehtlm_failed: bool,
pub error: String,
}
/// return mimetype and viewtype for a parsed mail
@@ -1312,9 +1176,9 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String
}
/// Returned addresses are normalized and lowercased.
pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
get_all_addresses_from_header(headers, |header_key| {
header_key == "to" || header_key == "cc" || header_key == "bcc"
header_key == "to" || header_key == "cc"
})
}
@@ -1359,8 +1223,6 @@ where
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::test_utils::*;
@@ -1478,39 +1340,6 @@ mod tests {
assert!(mimeparser.chat_disposition_notification_to.is_none());
}
#[async_std::test]
async fn test_get_parent_timestamp() {
let context = TestContext::new().await;
let raw = b"From: foo@example.org\n\
Content-Type: text/plain\n\
Chat-Version: 1.0\n\
In-Reply-To: <Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org>\n\
\n\
Some reply\n\
";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(
mimeparser.get_parent_timestamp(&context.ctx).await.unwrap(),
None
);
let timestamp = 1570435529;
context
.ctx
.sql
.execute(
"INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)",
paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp],
)
.await
.expect("Failed to write to the database");
assert_eq!(
mimeparser.get_parent_timestamp(&context.ctx).await.unwrap(),
Some(timestamp)
);
}
#[async_std::test]
async fn test_mimeparser_with_context() {
let context = TestContext::new().await;
@@ -1604,28 +1433,6 @@ 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;
@@ -1892,55 +1699,6 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
assert_eq!(message.parts[0].msg, "Hello!");
}
#[async_std::test]
async fn test_hide_html_without_content() {
let t = TestContext::new().await;
let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC)
From: sender@example.com
To: receiver@example.com
Subject: Mail with inline attachment
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_Part_25_46172632.1581201680436"
------=_Part_25_46172632.1581201680436
Content-Type: text/html; charset=utf-8
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Windows-1252">
<meta name="GENERATOR" content="MSHTML 11.00.10570.1001"></head>
<body><img align="baseline" alt="" src="cid:1712254131-1" border="0" hspace="0">
</body>
------=_Part_25_46172632.1581201680436
Content-Type: application/pdf; name="some_pdf.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="some_pdf.pdf"
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nGVOuwoCMRDs8xVbC8aZvC4Hx4Hno7ATAhZi56MTtPH33YtXiLKQ3ZnM
MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
------=_Part_25_46172632.1581201680436--
"#;
let message = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::File);
assert_eq!(message.parts[0].msg, "");
// Make sure the file is there even though the html is wrong:
let param = &message.parts[0].param;
let blob: BlobObject = param
.get_blob(Param::File, &t.ctx, false)
.await
.unwrap()
.unwrap();
let f = async_std::fs::File::open(blob.to_abs_path()).await.unwrap();
let size = f.metadata().await.unwrap().len();
assert_eq!(size, 154);
}
#[async_std::test]
async fn parse_inline_image() {
let context = TestContext::new().await;
@@ -2198,72 +1956,4 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
let test = parse_message_ids(" < ").unwrap();
assert!(test.is_empty());
}
#[async_std::test]
async fn parse_format_flowed_quote() {
let context = TestContext::new().await;
let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Subject: Re: swipe-to-reply
MIME-Version: 1.0
In-Reply-To: <bar@example.org>
Date: Tue, 06 Oct 2020 00:00:00 +0000
Chat-Version: 1.0
Message-ID: <foo@example.org>
To: bob <bob@example.org>
From: alice <alice@example.org>
> Long
> quote.
Reply
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(
message.get_subject(),
Some("Re: swipe-to-reply".to_string())
);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::Text);
assert_eq!(
message.parts[0].param.get(Param::Quote).unwrap(),
"Long quote."
);
assert_eq!(message.parts[0].msg, "Reply");
}
#[async_std::test]
async fn parse_quote_without_reply() {
let context = TestContext::new().await;
let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Subject: Re: swipe-to-reply
MIME-Version: 1.0
In-Reply-To: <bar@example.org>
Date: Tue, 06 Oct 2020 00:00:00 +0000
Message-ID: <foo@example.org>
To: bob <bob@example.org>
From: alice <alice@example.org>
> Just a quote.
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(
message.get_subject(),
Some("Re: swipe-to-reply".to_string())
);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::Text);
assert_eq!(
message.parts[0].param.get(Param::Quote).unwrap(),
"Just a quote."
);
assert_eq!(message.parts[0].msg, "");
}
}

View File

@@ -95,10 +95,7 @@ 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(context, "oauth2_access_token")
.await;
let access_token = context.sql.get_raw_config("oauth2_access_token").await;
if access_token.is_some() {
// success
return access_token;
@@ -106,13 +103,10 @@ pub async fn dc_get_oauth2_access_token(
}
// generate new token: build & call auth url
let refresh_token = context
.sql
.get_raw_config(context, "oauth2_refresh_token")
.await;
let refresh_token = context.sql.get_raw_config("oauth2_refresh_token").await;
let refresh_token_for = context
.sql
.get_raw_config(context, "oauth2_refresh_token_for")
.get_raw_config("oauth2_refresh_token_for")
.await
.unwrap_or_else(|| "unset".into());
@@ -122,7 +116,7 @@ pub async fn dc_get_oauth2_access_token(
(
context
.sql
.get_raw_config(context, "oauth2_pending_redirect_uri")
.get_raw_config("oauth2_pending_redirect_uri")
.await
.unwrap_or_else(|| "unset".into()),
oauth2.init_token,
@@ -136,7 +130,7 @@ pub async fn dc_get_oauth2_access_token(
(
context
.sql
.get_raw_config(context, "oauth2_redirect_uri")
.get_raw_config("oauth2_redirect_uri")
.await
.unwrap_or_else(|| "unset".into()),
oauth2.refresh_token,
@@ -170,14 +164,16 @@ pub async fn dc_get_oauth2_access_token(
}
// ... and POST
let mut req = surf::post(post_url).build();
if let Err(err) = req.body_form(&post_param) {
warn!(context, "Error calling OAuth2 at {}: {:?}", token_url, err);
let response = surf::post(post_url).body_form(&post_param);
if response.is_err() {
warn!(
context,
"Error calling OAuth2 at {}: {:?}", token_url, response
);
return None;
}
let client = surf::Client::new();
let parsed: Result<Response, _> = client.recv_json(req).await;
let parsed: Result<Response, _> = response.unwrap().recv_json().await;
if parsed.is_err() {
warn!(
context,
@@ -303,7 +299,7 @@ impl Oauth2 {
let mut fqdn: String = String::from(domain.as_ref());
if !fqdn.ends_with('.') {
fqdn.push('.');
fqdn.push_str(".");
}
if let Ok(res) = resolver.mx_lookup(fqdn).await {
@@ -321,7 +317,7 @@ impl Oauth2 {
}
async fn get_addr(&self, context: &Context, access_token: impl AsRef<str>) -> Option<String> {
let userinfo_url = self.get_userinfo.unwrap_or("");
let userinfo_url = self.get_userinfo.unwrap_or_else(|| "");
let userinfo_url = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token);
// should returns sth. as
@@ -358,7 +354,7 @@ impl Oauth2 {
async fn is_expired(context: &Context) -> bool {
let expire_timestamp = context
.sql
.get_raw_config_int64(context, "oauth2_timestamp_expires")
.get_raw_config_int64("oauth2_timestamp_expires")
.await
.unwrap_or_default();

View File

@@ -3,13 +3,12 @@ use std::fmt;
use std::str;
use async_std::path::PathBuf;
use itertools::Itertools;
use num_traits::FromPrimitive;
use serde::{Deserialize, Serialize};
use crate::blob::{BlobError, BlobObject};
use crate::context::Context;
use crate::error::{self, bail};
use crate::error::{self, bail, ensure};
use crate::message::MsgId;
use crate::mimeparser::SystemMessage;
@@ -41,21 +40,16 @@ pub enum Param {
/// 'c' nor 'e' are preset, the messages is only transport encrypted.
ErroneousE2ee = b'e',
/// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
/// For Messages: force unencrypted message, either `ForcePlaintext::AddAutocryptHeader` (1),
/// `ForcePlaintext::NoAutocryptHeader` (2) or 0.
ForcePlaintext = b'u',
/// For Messages: do not include Autocrypt header.
SkipAutocrypt = b'o',
/// For Messages
WantsMdn = b'r',
/// For Messages
Forwarded = b'a',
/// For Messages: quoted text.
Quote = b'q',
/// For Messages
Cmd = b'S',
@@ -74,9 +68,6 @@ 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
@@ -101,12 +92,6 @@ pub enum Param {
Recipients = b'R',
/// For Groups
///
/// An unpromoted group has not had any messages sent to it and thus only exists on the
/// creator's device. Any changes made to an unpromoted group do not need to send
/// system messages to the group members to update them of the changes. Once a message
/// has been sent to a group it is promoted and group changes require sending system
/// messages to all members.
Unpromoted = b'U',
/// For Groups and Contacts
@@ -134,6 +119,14 @@ pub enum Param {
MsgId = b'I',
}
/// Possible values for `Param::ForcePlaintext`.
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive)]
#[repr(u8)]
pub enum ForcePlaintext {
AddAutocryptHeader = 1,
NoAutocryptHeader = 2,
}
/// An object for handling key=value parameter lists.
///
/// The structure is serialized by calling `to_string()` on it.
@@ -150,12 +143,7 @@ impl fmt::Display for Params {
if i > 0 {
writeln!(f)?;
}
write!(
f,
"{}={}",
*key as u8 as char,
value.split('\n').join("\n\n")
)?;
write!(f, "{}={}", *key as u8 as char, value)?;
}
Ok(())
}
@@ -166,28 +154,27 @@ impl str::FromStr for Params {
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut inner = BTreeMap::new();
let mut lines = s.lines().peekable();
for pair in s.trim().lines() {
let pair = pair.trim();
if pair.is_empty() {
continue;
}
// TODO: probably nicer using a regex
ensure!(pair.len() > 1, "Invalid key pair: '{}'", pair);
let mut split = pair.splitn(2, '=');
let key = split.next();
let value = split.next();
while let Some(line) = lines.next() {
if let [key, value] = line.splitn(2, '=').collect::<Vec<_>>()[..] {
let key = key.to_string();
let mut value = value.to_string();
while let Some(s) = lines.peek() {
if !s.is_empty() {
break;
}
lines.next();
value.push('\n');
value += lines.next().unwrap_or_default();
}
ensure!(key.is_some(), "Missing key");
ensure!(value.is_some(), "Missing value");
if let Some(key) = key.as_bytes().first().and_then(|key| Param::from_u8(*key)) {
inner.insert(key, value);
} else {
bail!("Unknown key: {}", key);
}
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]) {
inner.insert(key, value.to_string());
} else {
bail!("Not a key-value pair: {:?}", line);
bail!("Unknown key: {}", key);
}
}
@@ -383,7 +370,7 @@ mod tests {
#[test]
fn test_dc_param() {
let mut p1: Params = "a=1\nf=2\nc=3".parse().unwrap();
let mut p1: Params = "\r\n\r\na=1\nf=2\n\nc = 3 ".parse().unwrap();
assert_eq!(p1.get_int(Param::Forwarded), Some(1));
assert_eq!(p1.get_int(Param::File), Some(2));
@@ -417,14 +404,6 @@ mod tests {
assert_eq!(p1.len(), 0)
}
#[test]
fn test_roundtrip() {
let mut params = Params::new();
params.set(Param::Height, "foo\nbar=baz\nquux");
params.set(Param::Width, "\n\n\na=\n=");
assert_eq!(params.to_string().parse::<Params>().unwrap(), params);
}
#[test]
fn test_regression() {
let p1: Params = "a=cli%40deltachat.de\nn=\ni=TbnwJ6lSvD5\ns=0ejvbdFSQxB"
@@ -487,8 +466,8 @@ mod tests {
);
// Blob in blobdir, expect blob.
let bar_path = t.ctx.get_blobdir().join("bar");
p.set(Param::File, bar_path.to_str().unwrap());
let bar = t.ctx.get_blobdir().join("bar");
p.set(Param::File, bar.to_str().unwrap());
let blob = p
.get_blob(Param::File, &t.ctx, false)
.await

View File

@@ -1,18 +1,13 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
use std::collections::HashSet;
use std::fmt;
use num_traits::FromPrimitive;
use std::collections::HashSet;
use anyhow::Result;
use crate::aheader::*;
use crate::chat;
use crate::constants::Blocked;
use crate::context::Context;
use crate::error::{bail, Result};
use crate::events::EventType;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::sql::Sql;
use crate::stock::StockMessage;
#[derive(Debug)]
pub enum PeerstateKeyType {
@@ -29,8 +24,8 @@ pub enum PeerstateVerifiedStatus {
}
/// Peerstate represents the state of an Autocrypt peer.
pub struct Peerstate<'a> {
pub context: &'a Context,
#[derive(Debug, PartialEq, Eq)]
pub struct Peerstate {
pub addr: String,
pub last_seen: i64,
pub last_seen_autocrypt: i64,
@@ -43,46 +38,52 @@ pub struct Peerstate<'a> {
pub verified_key: Option<SignedPublicKey>,
pub verified_key_fingerprint: Option<Fingerprint>,
pub to_save: Option<ToSave>,
pub fingerprint_changed: bool,
pub degrade_event: Option<DegradeEvent>,
}
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.fingerprint_changed == other.fingerprint_changed
}
}
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> Eq for Peerstate<'a> {}
let mut res = Self::new(row.try_get("addr")?);
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("fingerprint_changed", &self.fingerprint_changed)
.finish()
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)
}
}
@@ -93,125 +94,105 @@ pub enum ToSave {
All = 0x02,
}
impl<'a> Peerstate<'a> {
pub fn from_header(context: &'a Context, header: &Aheader, message_time: i64) -> Self {
Peerstate {
context,
addr: header.addr.clone(),
last_seen: message_time,
last_seen_autocrypt: message_time,
prefer_encrypt: header.prefer_encrypt,
public_key: Some(header.public_key.clone()),
public_key_fingerprint: Some(header.public_key.fingerprint()),
gossip_key: None,
gossip_key_fingerprint: None,
gossip_timestamp: 0,
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum DegradeEvent {
/// Recoverable by an incoming encrypted mail.
EncryptionPaused = 0x01,
pub fn from_gossip(context: &'a Context, gossip_header: &Aheader, message_time: i64) -> Self {
/// Recoverable by a new verify.
FingerprintChanged = 0x02,
}
impl Peerstate {
pub fn new(addr: String) -> Self {
Peerstate {
context,
addr: gossip_header.addr.clone(),
addr,
last_seen: 0,
last_seen_autocrypt: 0,
prefer_encrypt: Default::default(),
public_key: None,
public_key_fingerprint: None,
gossip_key: Some(gossip_header.public_key.clone()),
gossip_key_fingerprint: Some(gossip_header.public_key.fingerprint()),
gossip_timestamp: message_time,
gossip_key: None,
gossip_key_fingerprint: None,
gossip_timestamp: 0,
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
to_save: None,
degrade_event: None,
}
}
pub async fn from_addr(context: &'a Context, addr: &str) -> Result<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 fn from_header(header: &Aheader, message_time: i64) -> Self {
let mut res = Self::new(header.addr.clone());
res.last_seen = message_time;
res.last_seen_autocrypt = message_time;
res.to_save = Some(ToSave::All);
res.prefer_encrypt = header.prefer_encrypt;
res.public_key = Some(header.public_key.clone());
res.recalc_fingerprint();
res
}
pub fn from_gossip(gossip_header: &Aheader, message_time: i64) -> Self {
let mut res = Self::new(gossip_header.addr.clone());
res.gossip_timestamp = message_time;
res.to_save = Some(ToSave::All);
res.gossip_key = Some(gossip_header.public_key.clone());
res.recalc_fingerprint();
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_fingerprint(
context: &'a Context,
_sql: &Sql,
context: &Context,
fingerprint: &Fingerprint,
) -> Result<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
) -> 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
}
async fn from_stmt(
context: &'a Context,
query: &str,
params: Vec<&dyn crate::ToSql>,
) -> Result<Option<Peerstate<'a>>> {
let peerstate = context
.sql
.query_row_optional(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
*/
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?;
let res = Peerstate {
context,
addr: row.get(0)?,
last_seen: row.get(1)?,
last_seen_autocrypt: row.get(2)?,
prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(),
public_key: row
.get(4)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
public_key_fingerprint: row
.get::<_, Option<String>>(7)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_key: row
.get(6)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
gossip_key_fingerprint: row
.get::<_, Option<String>>(8)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_timestamp: row.get(5)?,
verified_key: row
.get(9)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
verified_key_fingerprint: row
.get::<_, Option<String>>(10)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
to_save: None,
fingerprint_changed: false,
};
Ok(res)
})
.await?;
Ok(peerstate)
}
@@ -226,7 +207,7 @@ impl<'a> Peerstate<'a> {
{
self.to_save = Some(ToSave::All);
if old_public_fingerprint.is_some() {
self.fingerprint_changed = true;
self.degrade_event = Some(DegradeEvent::FingerprintChanged);
}
}
}
@@ -240,57 +221,29 @@ impl<'a> Peerstate<'a> {
|| old_gossip_fingerprint != self.gossip_key_fingerprint
{
self.to_save = Some(ToSave::All);
// Warn about gossip key change only if there is no public key obtained from
// Autocrypt header, which overrides gossip key.
if old_gossip_fingerprint.is_some() && self.public_key_fingerprint.is_none() {
self.fingerprint_changed = true;
if old_gossip_fingerprint.is_some() {
self.degrade_event = Some(DegradeEvent::FingerprintChanged);
}
}
}
}
pub fn degrade_encryption(&mut self, message_time: i64) {
if self.prefer_encrypt == EncryptPreference::Mutual {
self.degrade_event = Some(DegradeEvent::EncryptionPaused);
}
self.prefer_encrypt = EncryptPreference::Reset;
self.last_seen = message_time;
self.to_save = Some(ToSave::All);
}
/// Adds a warning to the chat corresponding to peerstate if fingerprint has changed.
pub(crate) async fn handle_fingerprint_change(&self, context: &Context) -> Result<()> {
if self.fingerprint_changed {
if let Some(contact_id) = context
.sql
.query_get_value_result(
"SELECT id FROM contacts WHERE addr=?;",
paramsv![self.addr],
)
.await?
{
let (contact_chat_id, _) =
chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Deaddrop)
.await
.unwrap_or_default();
let msg = context
.stock_string_repl_str(StockMessage::ContactSetupChanged, self.addr.clone())
.await;
chat::add_info_msg(context, contact_chat_id, msg).await;
emit_event!(context, EventType::ChatModified(contact_chat_id));
} else {
bail!("contact with peerstate.addr {:?} not found", &self.addr);
}
}
Ok(())
}
pub fn apply_header(&mut self, header: &Aheader, message_time: i64) {
if self.addr.to_lowercase() != header.addr.to_lowercase() {
return;
}
if message_time > self.last_seen {
if message_time > self.last_seen_autocrypt {
self.last_seen = message_time;
self.last_seen_autocrypt = message_time;
self.to_save = Some(ToSave::Timestamps);
@@ -298,6 +251,11 @@ impl<'a> Peerstate<'a> {
|| header.prefer_encrypt == EncryptPreference::NoPreference)
&& header.prefer_encrypt != self.prefer_encrypt
{
if self.prefer_encrypt == EncryptPreference::Mutual
&& header.prefer_encrypt != EncryptPreference::Mutual
{
self.degrade_event = Some(DegradeEvent::EncryptionPaused);
}
self.prefer_encrypt = header.prefer_encrypt;
self.to_save = Some(ToSave::All)
}
@@ -424,21 +382,24 @@ impl<'a> Peerstate<'a> {
}
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> {
if create {
sql.execute(
"INSERT INTO acpeerstates (addr) VALUES(?);",
paramsx![&self.addr],
)
.await?;
}
if self.to_save == Some(ToSave::All) || create {
sql.execute(
if create {
"INSERT INTO acpeerstates (last_seen, last_seen_autocrypt, prefer_encrypted, \
public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, addr \
) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
} else {
"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![
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![
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
@@ -449,18 +410,17 @@ impl<'a> Peerstate<'a> {
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=?;",
paramsv![
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? WHERE addr=?;",
paramsx![
self.last_seen,
self.last_seen_autocrypt,
self.gossip_timestamp,
self.addr
&self.addr
],
)
.await?;
@@ -478,12 +438,6 @@ impl<'a> Peerstate<'a> {
}
}
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::*;
@@ -498,7 +452,6 @@ 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,
@@ -511,7 +464,7 @@ mod tests {
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
to_save: Some(ToSave::All),
fingerprint_changed: false,
degrade_event: None,
};
assert!(
@@ -521,17 +474,14 @@ mod tests {
let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr)
.await
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
.expect("failed to load peerstate from db");
// 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, &ctx.ctx.sql, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db");
assert_eq!(peerstate, peerstate_new2);
}
@@ -542,7 +492,6 @@ mod tests {
let pub_key = alice_keypair().public;
let peerstate = Peerstate {
context: &ctx.ctx,
addr: addr.into(),
last_seen: 10,
last_seen_autocrypt: 11,
@@ -555,7 +504,7 @@ mod tests {
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
degrade_event: None,
};
assert!(
@@ -576,7 +525,6 @@ 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,
@@ -589,7 +537,7 @@ mod tests {
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
degrade_event: None,
};
assert!(
@@ -603,76 +551,6 @@ mod tests {
// clear to_save, as that is not persissted
peerstate.to_save = None;
assert_eq!(Some(peerstate), peerstate_new);
}
#[async_std::test]
async fn test_peerstate_load_db_defaults() {
let ctx = crate::test_utils::TestContext::new().await;
let addr = "hello@mail.com";
// Old code created peerstates with this code and updated
// other values later. If UPDATE failed, other columns had
// default values, in particular fingerprints were set to
// empty strings instead of NULL. This should not be the case
// anymore, but the regression test still checks that defaults
// can be loaded without errors.
ctx.ctx
.sql
.execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr])
.await
.expect("Failed to write to the database");
let peerstate = Peerstate::from_addr(&ctx.ctx, addr)
.await
.expect("Failed to load peerstate from db")
.expect("Loaded peerstate is empty");
// Check that default values for fingerprints are treated like
// NULL.
assert_eq!(peerstate.public_key_fingerprint, None);
assert_eq!(peerstate.gossip_key_fingerprint, None);
assert_eq!(peerstate.verified_key_fingerprint, None);
}
#[async_std::test]
async fn test_peerstate_degrade_reordering() {
let context = crate::test_utils::TestContext::new().await.ctx;
let addr = "example@example.org";
let pub_key = alice_keypair().public;
let header = Aheader::new(addr.to_string(), pub_key, EncryptPreference::Mutual);
let mut peerstate = Peerstate {
context: &context,
addr: addr.to_string(),
last_seen: 0,
last_seen_autocrypt: 0,
prefer_encrypt: EncryptPreference::NoPreference,
public_key: None,
public_key_fingerprint: None,
gossip_key: None,
gossip_timestamp: 0,
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
to_save: None,
fingerprint_changed: false,
};
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::NoPreference);
peerstate.apply_header(&header, 100);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
peerstate.degrade_encryption(300);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
// This has message time 200, while encryption was degraded at timestamp 300.
// Because of reordering, header should not be applied.
peerstate.apply_header(&header, 200);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
// Same header will be applied in the future.
peerstate.apply_header(&header, 400);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
assert_eq!(peerstate, peerstate_new);
}
}

View File

@@ -365,13 +365,11 @@ 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<_>>>()?;
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")
ensure!(!msgs.is_empty(), "No valid messages found");
match msgs[0].get_content()? {
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
}
})
.await
@@ -381,7 +379,7 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
mod tests {
use super::*;
use crate::test_utils::*;
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
#[test]
fn test_split_armored_data_1() {
@@ -439,9 +437,9 @@ mod tests {
let bob = bob_keypair();
TestKeys {
alice_secret: alice.secret.clone(),
alice_public: alice.public,
alice_public: alice.public.clone(),
bob_secret: bob.secret.clone(),
bob_public: bob.public,
bob_public: bob.public.clone(),
}
}
}
@@ -449,29 +447,26 @@ mod tests {
/// The original text of [CTEXT_SIGNED]
static CLEARTEXT: &[u8] = b"This is a test";
/// Initialised [TestKeys] for tests.
static KEYS: Lazy<TestKeys> = Lazy::new(TestKeys::new);
lazy_static! {
/// Initialised [TestKeys] for tests.
static ref KEYS: TestKeys = TestKeys::new();
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
static CTEXT_SIGNED: Lazy<String> = Lazy::new(|| {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(
CLEARTEXT,
keyring,
Some(KEYS.alice_secret.clone()),
))
.unwrap()
});
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
static ref CTEXT_SIGNED: String = {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
smol::block_on(pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))).unwrap()
};
/// A cyphertext encrypted to Alice & Bob, not signed.
static CTEXT_UNSIGNED: Lazy<String> = Lazy::new(|| {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
});
/// A cyphertext encrypted to Alice & Bob, not signed.
static ref CTEXT_UNSIGNED: String = {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
smol::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
};
}
#[test]
fn test_encrypt_signed() {

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,9 @@ mod data;
use crate::config::Config;
use crate::dc_tools::EmailAddress;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_UPDATED};
use chrono::{NaiveDateTime, NaiveTime};
use crate::provider::data::PROVIDER_DATA;
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
#[derive(Debug, Copy, Clone, PartialEq, ToPrimitive)]
#[repr(u8)]
pub enum Status {
OK = 1,
@@ -15,29 +14,21 @@ pub enum Status {
BROKEN = 3,
}
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Protocol {
SMTP = 1,
IMAP = 2,
}
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Socket {
Automatic = 0,
SSL = 1,
STARTTLS = 2,
Plain = 3,
STARTTLS = 1,
SSL = 2,
}
impl Default for Socket {
fn default() -> Self {
Socket::Automatic
}
}
#[derive(Debug, PartialEq, Clone)]
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum UsernamePattern {
EMAIL = 1,
@@ -51,7 +42,7 @@ pub enum Oauth2Authorizer {
Gmail = 2,
}
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct Server {
pub protocol: Protocol,
pub socket: Socket,
@@ -60,6 +51,20 @@ pub struct Server {
pub username_pattern: UsernamePattern,
}
impl Server {
pub fn apply_username_pattern(&self, addr: String) -> String {
match self.username_pattern {
UsernamePattern::EMAIL => addr,
UsernamePattern::EMAILLOCALPART => {
if let Some(at) = addr.find('@') {
return addr.split_at(at).0.to_string();
}
addr
}
}
}
}
#[derive(Debug)]
pub struct ConfigDefault {
pub key: Config,
@@ -78,6 +83,25 @@ pub struct Provider {
pub oauth2_authorizer: Option<Oauth2Authorizer>,
}
impl Provider {
pub fn get_server(&self, protocol: Protocol) -> Option<&Server> {
for record in self.server.iter() {
if record.protocol == protocol {
return Some(record);
}
}
None
}
pub fn get_imap_server(&self) -> Option<&Server> {
self.get_server(Protocol::IMAP)
}
pub fn get_smtp_server(&self) -> Option<&Server> {
self.get_server(Protocol::SMTP)
}
}
pub fn get_provider_info(addr: &str) -> Option<&Provider> {
let domain = match addr.parse::<EmailAddress>() {
Ok(addr) => addr.domain,
@@ -92,18 +116,9 @@ pub fn get_provider_info(addr: &str) -> Option<&Provider> {
None
}
// returns update timestamp in seconds, UTC, compatible for comparison with time() and database times
pub fn get_provider_update_timestamp() -> i64 {
NaiveDateTime::new(*PROVIDER_UPDATED, NaiveTime::from_hms(0, 0, 0)).timestamp_millis() / 1_000
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::dc_tools::time;
use chrono::NaiveDate;
#[test]
fn test_get_provider_info_unexistant() {
@@ -122,16 +137,15 @@ mod tests {
let provider = get_provider_info("nauta.cu"); // this is no email address
assert!(provider.is_none());
let addr = "user@nauta.cu";
let provider = get_provider_info(addr).unwrap();
let provider = get_provider_info("user@nauta.cu").unwrap();
assert!(provider.status == Status::OK);
let server = &provider.server[0];
let server = provider.get_imap_server().unwrap();
assert_eq!(server.protocol, Protocol::IMAP);
assert_eq!(server.socket, Socket::STARTTLS);
assert_eq!(server.hostname, "imap.nauta.cu");
assert_eq!(server.port, 143);
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
let server = &provider.server[1];
let server = provider.get_smtp_server().unwrap();
assert_eq!(server.protocol, Protocol::SMTP);
assert_eq!(server.socket, Socket::STARTTLS);
assert_eq!(server.hostname, "smtp.nauta.cu");
@@ -146,16 +160,4 @@ mod tests {
let provider = get_provider_info("user@googlemail.com").unwrap();
assert!(provider.status == Status::PREPARATION);
}
#[test]
fn test_get_provider_update_timestamp() {
let timestamp_past = NaiveDateTime::new(
NaiveDate::from_ymd(2020, 9, 9),
NaiveTime::from_hms(0, 0, 0),
)
.timestamp_millis()
/ 1_000;
assert!(get_provider_update_timestamp() <= time());
assert!(get_provider_update_timestamp() > timestamp_past);
}
}

View File

@@ -4,7 +4,6 @@
import sys
import os
import yaml
import datetime
out_all = ""
out_domains = ""
@@ -42,8 +41,8 @@ def process_config_defaults(data):
config_defaults = data.get("config_defaults", "")
for key in config_defaults:
value = str(config_defaults[key])
defaults += " ConfigDefault { key: Config::" + camel(key) + ", value: \"" + value + "\" },\n"
defaults += " ])"
defaults += " ConfigDefault { key: Config::" + camel(key) + ", value: \"" + value + "\" },\n"
defaults += " ])"
return defaults
@@ -66,7 +65,7 @@ def process_data(data, file):
raise TypeError("domain used twice: " + domain)
domains_dict[domain] = True
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
comment += domain + ", "
@@ -96,7 +95,7 @@ def process_data(data, file):
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
raise TypeError("bad username pattern")
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
config_defaults = process_config_defaults(data)
@@ -111,16 +110,16 @@ def process_data(data, file):
before_login_hint = cleanstr(data.get("before_login_hint", ""))
after_login_hint = cleanstr(data.get("after_login_hint", ""))
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
provider += "static " + file2varname(file) + ": Lazy<Provider> = Lazy::new(|| Provider {\n"
provider += " status: Status::" + status + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " strict_tls: " + strict_tls + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += "});\n\n"
provider += " static ref " + file2varname(file) + ": Provider = Provider {\n"
provider += " status: Status::" + status + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " strict_tls: " + strict_tls + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += " };\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")
@@ -129,7 +128,7 @@ def process_data(data, file):
# finally, add the provider
global out_all, out_domains
out_all += "// " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
out_all += " // " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
# also add provider with no special things to do -
# eg. _not_ supporting oauth2 is also an information and we can skip the mx-lookup in this case
@@ -164,16 +163,12 @@ if __name__ == "__main__":
"use crate::provider::UsernamePattern::*;\n"
"use crate::provider::*;\n"
"use std::collections::HashMap;\n\n"
"use once_cell::sync::Lazy;\n\n")
"lazy_static::lazy_static! {\n\n")
process_dir(sys.argv[1])
out_all += "pub static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| [\n"
out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n"
out_all += out_domains;
out_all += "].iter().copied().collect());\n\n"
now = datetime.datetime.utcnow()
out_all += "pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "\
"Lazy::new(|| chrono::NaiveDate::from_ymd("+str(now.year)+", "+str(now.month)+", "+str(now.day)+"));\n"
out_all += " ].iter().copied().collect();\n}"
print(out_all)

134
src/qr.rs
View File

@@ -1,6 +1,6 @@
//! # QR code module
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
use percent_encoding::percent_decode_str;
use serde::Deserialize;
@@ -12,13 +12,11 @@ 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";
@@ -53,8 +51,6 @@ 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) {
@@ -72,7 +68,6 @@ 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()..];
@@ -143,13 +138,10 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
let mut lot = Lot::new();
// retrieve known state for this fingerprint
let peerstate = match Peerstate::from_fingerprint(context, &context.sql, &fingerprint).await {
Ok(peerstate) => peerstate,
Err(err) => return format_err!("Can't load peerstate: {}", err).into(),
};
let peerstate = Peerstate::from_fingerprint(context, &fingerprint).await;
if invitenumber.is_none() || auth.is_none() {
if let Some(peerstate) = peerstate {
if let Ok(peerstate) = peerstate {
lot.state = LotState::QrFprOk;
lot.id = Contact::add_or_lookup(
@@ -195,14 +187,13 @@ 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() == "http" || url.scheme() == "https" {
if url.scheme() == "https" {
lot.state = LotState::QrAccount;
lot.text1 = url.host_str().map(|x| x.to_string());
} else {
@@ -217,31 +208,6 @@ 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,
@@ -251,8 +217,7 @@ 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
#[allow(clippy::indexing_slicing)]
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
let response: Result<CreateAccountResponse, surf::Error> =
@@ -272,24 +237,9 @@ async fn set_account_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()..];
@@ -311,7 +261,6 @@ 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()..];
@@ -334,7 +283,6 @@ 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.
@@ -358,23 +306,24 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Lot {
Lot::from_address(context, name, addr).await
}
static VCARD_NAME_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
static VCARD_EMAIL_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
lazy_static! {
static ref VCARD_NAME_RE: regex::Regex =
regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap();
static ref VCARD_EMAIL_RE: regex::Regex =
regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap();
}
/// 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)
.and_then(|caps| {
let last_name = caps.get(1)?.as_str().trim();
let first_name = caps.get(2)?.as_str().trim();
.map(|caps| {
let last_name = &caps[1];
let first_name = &caps[2];
Some(format!("{} {}", first_name, last_name))
format!("{} {}", first_name.trim(), last_name.trim())
})
.unwrap_or_default();
@@ -662,31 +611,12 @@ 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:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
)
.await;
assert_eq!(res.get_state(), LotState::QrError);
@@ -695,40 +625,10 @@ mod tests {
// Test it again with lowercased "dcaccount:" uri scheme
let res = check_qr(
&ctx.ctx,
"dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
"dcaccount:http://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"
);
}
}

View File

@@ -1,9 +1,10 @@
#![warn(clippy::indexing_slicing)]
use async_std::prelude::*;
use async_std::sync::{channel, Receiver, Sender};
use async_std::task;
use crate::context::Context;
use crate::dc_tools::maybe_add_time_based_warnings;
use crate::imap::Imap;
use crate::job::{self, Thread};
use crate::{config::Config, message::MsgId, smtp::Smtp};
@@ -75,15 +76,6 @@ 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);
}
maybe_add_time_based_warnings(&ctx).await;
info = if ctx.get_config_bool(Config::InboxWatch).await {
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
} else {
@@ -108,7 +100,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_network!(ctx, "{}", err);
error!(ctx, "{}", err);
return;
}
@@ -130,8 +122,8 @@ 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 {
warn!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(&ctx, Some(watch_folder)).await;
error!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(&ctx, None).await;
}
// fetch
@@ -415,7 +407,10 @@ impl Scheduler {
/// Check if the scheduler is running.
pub fn is_running(&self) -> bool {
matches!(self, Scheduler::Running { .. })
match self {
Scheduler::Running { .. } => true,
_ => false,
}
}
}

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