Compare commits

...
Author SHA1 Message Date
Hocuri ed349cd604 Introduce config caching 2022-03-16 11:52:11 +01:00
Hocuri da73f90423 Benchmark more "boring" emails, not ones from the test-data dir 2022-03-16 11:31:59 +01:00
Hocuri 5d1c845285 Benchmark fewer emails to make the benchmark complete faster 2022-03-16 10:53:33 +01:00
Hocuri db0f5ed5a6 try to speed up needs_move(), introduce LazyMsg 2022-03-16 10:50:11 +01:00
Hocuri ac51939c08 Allow to provide backup file which will be imported 2022-03-15 17:49:56 +01:00
Hocuri e20ee3bfe5 Benchmark dc_receive_imf()
Don't count the account creation in the receive emails benchmark

Use Criterion's async support

See https://bheisler.github.io/criterion.rs/book/user_guide/benchmarking_async.html
2022-03-15 13:37:36 +01:00
bjoern dd4f2ac671 prepare 1.65 (#2812)
* update changelog for 1.65.0

* bump version to 1.65.0
2021-11-15 14:51:28 +01:00
bjoern eebb2a3b68 do not assume 'no ephemeral timer' on partial downloads (#2811)
* do not assume 'no ephemeral timer' on partial downloads

* add a test to check that ephemeral timers are not disabled on partial downloads
2021-11-15 11:45:06 +01:00
link2xt 0d62069b67 python: add mypy support and some type hints
`deltachat.const` module now defines `__getattr__` and `__dir__` as
suggested by https://www.python.org/dev/peps/pep-0562/
mypy detects that `__getattr__` is defined and does not show errors
for `DC_*` constants which cannot be detected statically.
mypy is added to `tox.ini`, so type check can be run with `tox -e mypy`.
2021-11-14 11:06:44 +03:00
link2xt 56cf2e6596 Replace error! on verification failure with warn!
A message is added into 1:1 chat anyway, and user does not know what `StockMessage::ContactNotVerified` means.
2021-11-14 02:02:23 +03:00
Simon Laux 59bd5481b9 fix 1.61.0 changelog (#2806) 2021-11-13 22:06:29 +01:00
Hocuri 6c8da526a0 Fix: Only show the "Cannot login" device message if it's actually authentication that failed (#2804)
Generally we could also just remove the device message as we have the
connectivity view, OTOH if you are not using DC a lot, it may be useful
to be notified without opening DC.

And apart from this one bug it's working fine.
2021-11-13 19:20:01 +01:00
link2xt 13bc8b78d7 python: enable isolated build in tox.ini
This makes tox install build system configured in pyproject.toml
according to PEP 518 rather than assuming setuptools.
2021-11-13 03:45:58 +00:00
link2xt c7c68094d9 setup.py: restore compatibility with setup.py sdist command
Otherwise source package with version 0.0.0 is created.
2021-11-12 23:41:17 +00:00
bjoern 84f54b10dc prepare 1.64 (#2802)
* update changelog for 1.64

* bump version to 1.64.0
2021-11-11 16:45:30 +01:00
bjoern cebc9e3e91 add 'waiting for being added to the group' only for group-joins (#2797)
* add 'waiting for being added to the group' only for group-joins, not for setup-contact

* add a comment why the message is not added on setup-contact
2021-11-07 20:30:55 +01:00
link2xt 1379f8a055 Factor apply_group_changes out of create_or_lookup_group
`apply_group_changes` is executed regardless of whether the group is
created via `create_or_lookup_group` or found via
`lookup_chat_by_reply`. This change removes the need for
`lookup_chat_by_reply` to return `None` when group ID exists in the
database to let `create_or_lookup_group` run. As a side effect of this
Delta Chat replies to ad hoc groups are now correctly assigned to
chats when there are multiple groups with the same group ID, such as
ad hoc groups with the same member lists.
2021-11-07 18:34:44 +03:00
bjoern 53d049e5f5 prepare 1.63 (#2796)
* update changelog for 1.63

* bump version to 1.63.0
2021-11-06 21:00:54 +01:00
bjoern 4968f72dfb fix permanently hiding of one-to-one chats after secure-join (#2791)
* test one-to-one chats on setup-contact/secure-join

only one chat is created after scanning a QR code:

- on setup-contact, one-to-ones are created on both sided

- on secure-join, the joined group chat is created;
  one-to-ones are not created intitally,
  but should become visible on receiving messages

* make sure, Alice creates the chat with Bob on setup-contact

not totally sure if that change in #2508 was on-purpose,
however, all yet released versions
did create the one-to-one chat also on the Inviter's (Alice) side,
so, let's stay with that,
i do not see many reasons to change that.

* unblock hidden (Blocked::Yes) one-to-one chats

one-to-one chats may be hidden by secure-join,
in case someone later writes a message to it
(not unlikely), the chat needs to be shown.

before, messages are just not shown,
the corresponding chat did not appear.

the 'Blocked' wording of a 'Chat' must not be mixed with the
'Blocking' of a contact. 'Chat-Blocking' is mostly a visibility thing,
that may change as messages come in.

this change should not affect _really_ blocked contacts -
they are filtered out already before
and their messages are usually not even downloaded.
also, before allow_creation is checked,
that may disallow chat creation for show_emails reasons.

all in all, it just does the same
as if the user has manualy deleted the chat before and it would be created.

* simplify test
2021-11-06 18:46:10 +01:00
dependabot[bot] b24a0ed8fd Merge pull request #2794 from deltachat/dependabot/cargo/serde_json-1.0.69 2021-11-05 23:29:32 +00:00
dependabot[bot] c810347c7c cargo: bump serde_json from 1.0.68 to 1.0.69
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.68 to 1.0.69.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.68...v1.0.69)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-05 21:13:20 +00:00
dependabot[bot] cf8c4142c7 Merge pull request #2784 from deltachat/dependabot/cargo/libc-0.2.106 2021-11-05 19:35:21 +00:00
dependabot[bot] f71901b5f9 Merge pull request #2786 from deltachat/dependabot/cargo/backtrace-0.3.63 2021-11-05 19:33:54 +00:00
dependabot[bot] 4419f9c4e7 Merge pull request #2789 from deltachat/dependabot/cargo/anyhow-1.0.45 2021-11-05 19:33:02 +00:00
dependabot[bot] 4e5982b682 cargo: bump anyhow from 1.0.44 to 1.0.45
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.44 to 1.0.45.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.44...1.0.45)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-02 21:14:41 +00:00
bjoern 39e1510e64 add dc_get_last_error() (#2788)
* add dc_get_last_error()

* make clippy happy

* simplify block_on() call
2021-11-02 22:09:04 +01:00
dependabot[bot] 64206160cc Merge pull request #2785 from deltachat/dependabot/cargo/surf-2.3.2 2021-11-01 22:56:28 +00:00
dependabot[bot] 6376659348 cargo: bump backtrace from 0.3.62 to 0.3.63
Bumps [backtrace](https://github.com/rust-lang/backtrace-rs) from 0.3.62 to 0.3.63.
- [Release notes](https://github.com/rust-lang/backtrace-rs/releases)
- [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.62...0.3.63)

---
updated-dependencies:
- dependency-name: backtrace
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 21:17:04 +00:00
dependabot[bot] 43b2a4ad27 cargo: bump surf from 2.3.1 to 2.3.2
Bumps [surf](https://github.com/http-rs/surf) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/http-rs/surf/releases)
- [Changelog](https://github.com/http-rs/surf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/http-rs/surf/compare/v2.3.1...v2.3.2)

---
updated-dependencies:
- dependency-name: surf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 21:16:52 +00:00
dependabot[bot] eaf06bb239 cargo: bump libc from 0.2.105 to 0.2.106
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.105 to 0.2.106.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.105...0.2.106)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 21:16:34 +00:00
link2xt 5e26b5bfdc Merge pull request #2743 from deltachat/improve-gossip
Optimize Autocrypt gossip
2021-11-01 02:01:51 +03:00
link2xt 60fbb6df5a Add python test for gossip optimization 2021-10-31 22:39:05 +00:00
link2xt 3e60ee9d3e python: use datetime.fromtimestamp() instead of datetime.utcfromtimestamp()
utcfromtimestamp() is not recommended by the official documentation,
because many methods, including timestamp(), work incorrectly with
"naive" datetimes returned by utcfromtimestamp().
2021-10-31 22:39:05 +00:00
link2xt 54e79409e6 Optimize Autocrypt gossip
Update gossiped_timestamp when someone else sends autocrypt gossip in
the group, so we postpone sending gossip again ourselves.

- Warn about failures to parse Autocrypt-Gossip header
- Move gossip-related methods into ChatId impl
- Fix a "gossi_pp_ed" typo
2021-11-01 01:17:51 +03:00
37 changed files with 1161 additions and 544 deletions
+1 -1
View File
@@ -150,4 +150,4 @@ jobs:
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e lint,doc,py3
run: tox -e lint,mypy,doc,py3
+37 -2
View File
@@ -1,5 +1,40 @@
# Changelog
## 1.65.0
### Changes
- python: add mypy support and some type hints #2809
### Fixes
- do not disable ephemeral timer when downloading a message partially #2811
- apply existing ephemeral timer also to partially downloaded messages;
after full download, the ephemeral timer starts over #2811
- replace user-visible error on verification failure with warning;
the error is logged to the corresponding chat anyway #2808
## 1.64.0
### Fixes
- add 'waiting for being added to the group' only for group-joins,
not for setup-contact #2797
- prioritize In-Reply-To: and References: headers over group IDs when assigning
messages to chats to fix incorrect assignment of Delta Chat replies to
classic email threads #2795
## 1.63.0
### API changes
- `dc_get_last_error()` added #2788
### Changes
- Optimize Autocrypt gossip #2743
### Fixes
- fix permanently hiding of one-to-one chats after secure-join #2791
## 1.62.0
### API Changes
@@ -28,10 +63,10 @@
## 1.61.0
### API Changes
- download-on-demand added: `dc_msg_get_download_status()`, `dc_download_full_msg()`
- download-on-demand added: `dc_msg_get_download_state()`, `dc_download_full_msg()`
and `download_limit` config option #2631 #2696
- `dc_create_broadcast_list()` and chat type `DC_CHAT_TYPE_BROADCAST` added #2707 #2722
- allow ui-specific configs: `dc_set_ui_config()` and `dc_get_ui_config()` #2672
- allow ui-specific configs using `ui.`-prefix in key (`dc_set_config(context, "ui.*", value)`) #2672
- new strings from `DC_STR_PARTIAL_DOWNLOAD_MSG_BODY`
to `DC_STR_PART_OF_TOTAL_USED` #2631 #2694 #2707 #2723
- emit warnings and errors from account manager with account-id 0 #2712
Generated
+19 -16
View File
@@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "addr2line"
version = "0.16.0"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
@@ -114,9 +114,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
[[package]]
name = "arrayvec"
@@ -423,9 +423,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "backtrace"
version = "0.3.62"
version = "0.3.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "091bcdf2da9950f96aa522681ce805e6857f6ca8df73833d35736ab2dc78e152"
checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6"
dependencies = [
"addr2line",
"cc",
@@ -847,11 +847,13 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10"
dependencies = [
"async-std",
"atty",
"cast",
"clap",
"criterion-plot",
"csv",
"futures",
"itertools",
"lazy_static",
"num-traits",
@@ -1072,7 +1074,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.62.0"
version = "1.65.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1152,7 +1154,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.62.0"
version = "1.65.0"
dependencies = [
"anyhow",
"async-std",
@@ -1734,9 +1736,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.25.0"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
checksum = "81a03ce013ffccead76c11a15751231f777d9295b845cc1266ed4d34fcbd7977"
[[package]]
name = "gloo-timers"
@@ -2104,9 +2106,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.105"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673"
[[package]]
name = "libm"
@@ -3324,9 +3326,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.68"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8"
dependencies = [
"itoa",
"ryu",
@@ -3614,10 +3616,11 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "surf"
version = "2.3.1"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f856d60bdb4679fc9ec516c34093484e963431b5016a8429f85a8e74b5ccaa"
checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7"
dependencies = [
"async-native-tls",
"async-std",
"async-trait",
"cfg-if 1.0.0",
+6 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.62.0"
version = "1.65.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -79,7 +79,7 @@ humansize = "1"
[dev-dependencies]
ansi_term = "0.12.0"
async-std = { version = "1", features = ["unstable", "attributes"] }
criterion = "0.3"
criterion = { version = "0.3.4", features = ["async_std"] }
futures-lite = "1.12"
log = "0.4"
pretty_assertions = "1.0"
@@ -116,6 +116,10 @@ harness = false
name = "search_msgs"
harness = false
[[bench]]
name = "receive_emails"
harness = false
[features]
default = ["vendored"]
internals = []
+1 -1
View File
@@ -8,7 +8,7 @@ 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();
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
for expected_id in 2..n {
let id = accounts.add_account().await.unwrap();
+117
View File
@@ -0,0 +1,117 @@
use std::convert::TryInto;
use async_std::{path::PathBuf, task::block_on};
use criterion::{
async_executor::AsyncStdExecutor, black_box, criterion_group, criterion_main, BatchSize,
BenchmarkId, Criterion,
};
use deltachat::{
config::Config,
context::Context,
dc_receive_imf::dc_receive_imf,
imex::{imex, ImexMode},
};
use tempfile::tempdir;
async fn recv_emails(context: Context, emails: &[&[u8]]) -> Context {
for (i, bytes) in emails.iter().enumerate() {
dc_receive_imf(
&context,
bytes,
"INBOX",
black_box(i.try_into().unwrap()),
false,
)
.await
.unwrap();
}
context
}
async fn recv_all_emails(mut context: Context, needs_move_enabled: bool) -> Context {
context.disable_needs_move = !needs_move_enabled;
for i in 0..100 {
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Mr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
In-Reply-To: Mr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
dc_receive_imf(
&context,
imf_raw.as_bytes(),
"INBOX",
black_box(i.try_into().unwrap()),
false,
)
.await
.unwrap();
}
context
}
async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new("FakeOS".into(), dbfile.into(), id)
.await
.unwrap();
let backup: PathBuf = std::env::current_dir()
.unwrap()
.join("delta-chat-backup.tar")
.into();
if backup.exists().await {
println!("Importing backup");
imex(&context, ImexMode::ImportBackup, &backup)
.await
.unwrap();
}
let addr = "alice@example.com";
context.set_config(Config::Addr, Some(addr)).await.unwrap();
context
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.unwrap();
context
.set_config(Config::Configured, Some("1"))
.await
.unwrap();
context
}
fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("from_elem");
for needs_move_enabled in [false, true] {
group.bench_with_input(
BenchmarkId::new("Receive many messages", needs_move_enabled),
&needs_move_enabled,
|b, needs_move_enabled| {
b.to_async(AsyncStdExecutor).iter_batched(
|| block_on(create_context()),
|context| recv_all_emails(black_box(context), *needs_move_enabled),
BatchSize::LargeInput,
);
},
);
}
group.finish();
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.62.0"
version = "1.65.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
+16
View File
@@ -2414,6 +2414,22 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
void dc_delete_all_locations (dc_context_t* context);
/**
* Get last error string.
*
* This is the same error string as logged via #DC_EVENT_ERROR,
* however, using this function avoids race conditions
* if the failing function is called in another thread than dc_get_next_event().
*
* @memberof dc_context_t
* @param context The context object.
* @return Last error or an empty string if there is no last error.
* NULL is never returned.
* The returned value must be released using dc_str_unref() after usage.
*/
char* dc_get_last_error (dc_context_t* context);
/**
* Release a string returned by another deltachat-core function.
* - Strings returned by any deltachat-core-function
+10
View File
@@ -2206,6 +2206,16 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_last_error()");
return "".strdup();
}
let ctx = &*context;
block_on(ctx.get_last_error()).strdup()
}
// dc_array_t
pub type dc_array_t = dc_array::dc_array_t;
+19
View File
@@ -0,0 +1,19 @@
[mypy]
[mypy-deltachat.capi.*]
ignore_missing_imports = True
[mypy-pluggy.*]
ignore_missing_imports = True
[mypy-cffi.*]
ignore_missing_imports = True
[mypy-imapclient.*]
ignore_missing_imports = True
[mypy-pytest.*]
ignore_missing_imports = True
[mypy-_pytest.*]
ignore_missing_imports = True
+1
View File
@@ -1,5 +1,6 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
root = ".."
+1
View File
@@ -12,6 +12,7 @@ def main():
long_description=long_description,
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient', 'requests'],
setup_requires=['setuptools_scm'], # required for compatibility with `python3 setup.py sdist`
packages=setuptools.find_packages('src'),
package_dir={'': 'src'},
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
+2 -2
View File
@@ -19,9 +19,9 @@ except DistributionNotFound:
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
if not _DC_EVENTNAME_MAP:
for name, val in vars(const).items():
for name in dir(const):
if name.startswith("DC_EVENT_"):
_DC_EVENTNAME_MAP[val] = name
_DC_EVENTNAME_MAP[getattr(const, name)] = name
return _DC_EVENTNAME_MAP[integer]
+68 -55
View File
@@ -15,6 +15,7 @@ from .contact import Contact
from .tracker import ImexTracker, ConfigureTracker
from . import hookspec
from .events import EventThread
from typing import Union, Any, Dict, Optional, List, Generator
class MissingCredentials(ValueError):
@@ -28,7 +29,7 @@ class Account(object):
"""
MissingCredentials = MissingCredentials
def __init__(self, db_path, os_name=None, logging=True):
def __init__(self, db_path, os_name=None, logging=True) -> None:
""" initialize account object.
:param db_path: a path to the account database. The database
@@ -58,11 +59,11 @@ class Account(object):
hook = hookspec.Global._get_plugin_manager().hook
hook.dc_account_init(account=self)
def disable_logging(self):
def disable_logging(self) -> None:
""" disable logging. """
self._logging = False
def enable_logging(self):
def enable_logging(self) -> None:
""" re-enable logging. """
self._logging = True
@@ -73,7 +74,7 @@ class Account(object):
if self._logging:
self._pm.hook.ac_log_line(message=msg)
def _check_config_key(self, name):
def _check_config_key(self, name: str) -> None:
if name not in self._configkeys:
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
name, self._configkeys))
@@ -105,19 +106,19 @@ class Account(object):
cursor += len(entry) + 1
log("")
def set_stock_translation(self, id, string):
def set_stock_translation(self, id: int, string: str) -> None:
""" set stock translation string.
:param id: id of stock string (const.DC_STR_*)
:param value: string to set as new transalation
:returns: None
"""
string = string.encode("utf8")
res = lib.dc_set_stock_translation(self._dc_context, id, string)
bytestring = string.encode("utf8")
res = lib.dc_set_stock_translation(self._dc_context, id, bytestring)
if res == 0:
raise ValueError("could not set translation string")
def set_config(self, name, value):
def set_config(self, name: str, value: Optional[str]) -> None:
""" set configuration values.
:param name: config key name (unicode)
@@ -125,16 +126,16 @@ class Account(object):
:returns: None
"""
self._check_config_key(name)
name = name.encode("utf8")
if name == b"addr" and self.is_configured():
namebytes = name.encode("utf8")
if namebytes == b"addr" and self.is_configured():
raise ValueError("can not change 'addr' after account is configured.")
if value is not None:
value = value.encode("utf8")
valuebytes = value.encode("utf8")
else:
value = ffi.NULL
lib.dc_set_config(self._dc_context, name, value)
valuebytes = ffi.NULL
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
def get_config(self, name):
def get_config(self, name: str):
""" return unicode string value.
:param name: configuration key to lookup (eg "addr" or "mail_pw")
@@ -143,12 +144,12 @@ class Account(object):
"""
if name != "sys.config_keys":
self._check_config_key(name)
name = name.encode("utf8")
res = lib.dc_get_config(self._dc_context, name)
namebytes = name.encode("utf8")
res = lib.dc_get_config(self._dc_context, namebytes)
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr, public, secret):
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
@@ -160,7 +161,7 @@ class Account(object):
if res == 0:
raise Exception("Failed to set key")
def update_config(self, kwargs):
def update_config(self, kwargs: Dict[str, Any]) -> None:
""" update config values.
:param kwargs: name=value config settings for this account.
@@ -170,7 +171,7 @@ class Account(object):
for key, value in kwargs.items():
self.set_config(key, str(value))
def is_configured(self):
def is_configured(self) -> bool:
""" determine if the account is configured already; an initial connection
to SMTP/IMAP has been verified.
@@ -178,7 +179,7 @@ class Account(object):
"""
return True if lib.dc_is_configured(self._dc_context) else False
def set_avatar(self, img_path):
def set_avatar(self, img_path: Optional[str]) -> None:
"""Set self avatar.
:raises ValueError: if profile image could not be set
@@ -190,12 +191,12 @@ class Account(object):
assert os.path.exists(img_path), img_path
self.set_config("selfavatar", img_path)
def check_is_configured(self):
def check_is_configured(self) -> None:
""" Raise ValueError if this account is not configured. """
if not self.is_configured():
raise ValueError("need to configure first")
def get_latest_backupfile(self, backupdir):
def get_latest_backupfile(self, backupdir) -> Optional[str]:
""" return the latest backup file in a given directory.
"""
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
@@ -203,7 +204,7 @@ class Account(object):
return None
return from_dc_charpointer(res)
def get_blobdir(self):
def get_blobdir(self) -> Optional[str]:
""" return the directory for files.
All sent files are copied to this directory if necessary.
@@ -211,15 +212,15 @@ class Account(object):
"""
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
def get_self_contact(self):
def get_self_contact(self) -> Contact:
""" return this account's identity as a :class:`deltachat.contact.Contact`.
:returns: :class:`deltachat.contact.Contact`
"""
return Contact(self, const.DC_CONTACT_ID_SELF)
def create_contact(self, obj, name=None):
""" create a (new) Contact or return an existing one.
def create_contact(self, obj, name: Optional[str] = None) -> Contact:
"""create a (new) Contact or return an existing one.
Calling this method will always result in the same
underlying contact id. If there already is a Contact
@@ -236,13 +237,13 @@ class Account(object):
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id)
def get_contact(self, obj):
def get_contact(self, obj) -> Optional[Contact]:
if isinstance(obj, Contact):
return obj
(_, addr) = self.get_contact_addr_and_name(obj)
return self.get_contact_by_addr(addr)
def get_contact_addr_and_name(self, obj, name=None):
def get_contact_addr_and_name(self, obj, name: Optional[str] = None):
if isinstance(obj, Account):
if not obj.is_configured():
raise ValueError("can only add addresses from configured accounts")
@@ -260,7 +261,7 @@ class Account(object):
name = displayname
return (name, addr)
def delete_contact(self, contact):
def delete_contact(self, contact: Contact) -> bool:
""" delete a Contact.
:param contact: contact object obtained
@@ -271,22 +272,23 @@ class Account(object):
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
def get_contact_by_addr(self, email):
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
""" get a contact for the email address or None if it's blocked or doesn't exist. """
_, addr = parseaddr(email)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
if contact_id:
return self.get_contact_by_id(contact_id)
return None
def get_contact_by_id(self, contact_id):
""" return Contact instance or None.
def get_contact_by_id(self, contact_id: int) -> Contact:
""" return Contact instance or raise an exception.
:param contact_id: integer id of this contact.
:returns: None or :class:`deltachat.contact.Contact` instance.
:returns: :class:`deltachat.contact.Contact` instance.
"""
return Contact(self, contact_id)
def get_blocked_contacts(self):
def get_blocked_contacts(self) -> List[Contact]:
""" return a list of all blocked contacts.
:returns: list of :class:`deltachat.contact.Contact` objects.
@@ -297,8 +299,13 @@ class Account(object):
)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_contacts(self, query=None, with_self=False, only_verified=False):
""" get a (filtered) list of contacts.
def get_contacts(
self,
query: Optional[str] = None,
with_self: bool = False,
only_verified: bool = False,
) -> List[Contact]:
"""get a (filtered) list of contacts.
:param query: if a string is specified, only return contacts
whose name or e-mail matches query.
@@ -318,7 +325,7 @@ class Account(object):
)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_fresh_messages(self):
def get_fresh_messages(self) -> Generator[Message, None, None]:
""" yield all fresh messages from all chats. """
dc_array = ffi.gc(
lib.dc_get_fresh_msgs(self._dc_context),
@@ -326,12 +333,17 @@ class Account(object):
)
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
def create_chat(self, obj):
def create_chat(self, obj) -> Chat:
""" Create a 1:1 chat with Account, Contact or e-mail address. """
return self.create_contact(obj).create_chat()
def create_group_chat(self, name, contacts=None, verified=False):
""" create a new group chat object.
def create_group_chat(
self,
name: str,
contacts: Optional[List[Contact]] = None,
verified: bool = False,
) -> Chat:
"""create a new group chat object.
Chats are unpromoted until the first message is sent.
@@ -347,7 +359,7 @@ class Account(object):
chat.add_contact(contact)
return chat
def get_chats(self):
def get_chats(self) -> List[Chat]:
""" return list of chats.
:returns: a list of :class:`deltachat.chat.Chat` objects.
@@ -364,17 +376,17 @@ class Account(object):
chatlist.append(Chat(self, chat_id))
return chatlist
def get_device_chat(self):
def get_device_chat(self) -> Chat:
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
def get_message_by_id(self, msg_id):
def get_message_by_id(self, msg_id: int) -> Message:
""" return Message instance.
:param msg_id: integer id of this message.
:returns: :class:`deltachat.message.Message` instance.
"""
return Message.from_db(self, msg_id)
def get_chat_by_id(self, chat_id):
def get_chat_by_id(self, chat_id: int) -> Chat:
""" return Chat instance.
:param chat_id: integer id of this chat.
:returns: :class:`deltachat.chat.Chat` instance.
@@ -386,19 +398,18 @@ class Account(object):
lib.dc_chat_unref(res)
return Chat(self, chat_id)
def mark_seen_messages(self, messages):
def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None:
""" mark the given set of messages as seen.
:param messages: a list of message ids or Message instances.
"""
arr = array("i")
for msg in messages:
msg = getattr(msg, "id", msg)
arr.append(msg)
arr.append(getattr(msg, "id", msg))
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
def forward_messages(self, messages, chat):
def forward_messages(self, messages: List[Message], chat: Chat) -> None:
""" Forward list of messages to a chat.
:param messages: list of :class:`deltachat.message.Message` object.
@@ -408,7 +419,7 @@ class Account(object):
msg_ids = [msg.id for msg in messages]
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
def delete_messages(self, messages):
def delete_messages(self, messages: List[Message]) -> None:
""" delete messages (local and remote).
:param messages: list of :class:`deltachat.message.Message` object.
@@ -477,7 +488,7 @@ class Account(object):
raise RuntimeError("could not send out autocrypt setup message")
return from_dc_charpointer(res)
def get_setup_contact_qr(self):
def get_setup_contact_qr(self) -> Optional[str]:
""" get/create Setup-Contact QR Code as ascii-string.
this string needs to be transferred to another DC account
@@ -527,7 +538,9 @@ class Account(object):
raise ValueError("could not join group")
return Chat(self, chat_id)
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
def set_location(
self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0
) -> None:
"""set a new location. It effects all chats where we currently
have enabled location streaming.
@@ -621,7 +634,7 @@ class Account(object):
"""
lib.dc_maybe_network(self._dc_context)
def configure(self, reconfigure=False):
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
""" Start configuration process and return a Configtracker instance
on which you can block with wait_finish() to get a True/False success
value for the configuration process.
@@ -634,11 +647,11 @@ class Account(object):
lib.dc_configure(self._dc_context)
return configtracker
def wait_shutdown(self):
def wait_shutdown(self) -> None:
""" wait until shutdown of this account has completed. """
self._shutdown_event.wait()
def stop_io(self):
def stop_io(self) -> None:
""" stop core IO scheduler if it is running. """
self.log("stop_ongoing")
self.stop_ongoing()
@@ -646,7 +659,7 @@ class Account(object):
self.log("dc_stop_io (stop core IO scheduler)")
lib.dc_stop_io(self._dc_context)
def shutdown(self):
def shutdown(self) -> None:
""" shutdown and destroy account (stop callback thread, close and remove
underlying dc_context)."""
if self._dc_context is None:
+31 -29
View File
@@ -3,12 +3,13 @@
import mimetypes
import calendar
import json
from datetime import datetime
from datetime import datetime, timezone
import os
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .capi import lib, ffi
from . import const
from .message import Message
from typing import Optional
class Chat(object):
@@ -17,20 +18,20 @@ class Chat(object):
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, account, id):
def __init__(self, account, id) -> None:
from .account import Account
assert isinstance(account, Account), repr(account)
self.account = account
self.id = id
def __eq__(self, other):
def __eq__(self, other) -> bool:
return self.id == getattr(other, "id", None) and \
self.account._dc_context == other.account._dc_context
def __ne__(self, other):
def __ne__(self, other) -> bool:
return not (self == other)
def __repr__(self):
def __repr__(self) -> str:
return "<Chat id={} name={}>".format(self.id, self.get_name())
@property
@@ -40,7 +41,7 @@ class Chat(object):
lib.dc_chat_unref
)
def delete(self):
def delete(self) -> None:
"""Delete this chat and all its messages.
Note:
@@ -50,24 +51,24 @@ class Chat(object):
"""
lib.dc_delete_chat(self.account._dc_context, self.id)
def block(self):
def block(self) -> None:
"""Block this chat."""
lib.dc_block_chat(self.account._dc_context, self.id)
def accept(self):
def accept(self) -> None:
"""Accept this contact request chat."""
lib.dc_accept_chat(self.account._dc_context, self.id)
# ------ chat status/metadata API ------------------------------
def is_group(self):
def is_group(self) -> bool:
""" return true if this chat is a group chat.
: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
def is_muted(self):
def is_muted(self) -> bool:
""" return true if this chat is muted.
:returns: True if chat is muted, False otherwise.
@@ -90,7 +91,7 @@ class Chat(object):
"""
return not lib.dc_chat_is_unpromoted(self._dc_chat)
def can_send(self):
def can_send(self) -> bool:
"""Check if messages can be sent to a give chat.
This is not true eg. for the contact requests or for the device-talk
@@ -98,30 +99,30 @@ class Chat(object):
"""
return lib.dc_chat_can_send(self._dc_chat)
def is_protected(self):
def is_protected(self) -> bool:
""" 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)
def get_name(self):
def get_name(self) -> Optional[str]:
""" return name of this chat.
:returns: unicode name
"""
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
def set_name(self, name):
def set_name(self, name: str) -> bool:
""" set name of this chat.
:param name: as a unicode string.
:returns: None
:returns: True on success, False otherwise
"""
name = as_dc_charpointer(name)
return lib.dc_set_chat_name(self.account._dc_context, self.id, name)
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name))
def mute(self, duration=None):
def mute(self, duration: Optional[int] = None) -> None:
""" mutes the chat
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
@@ -135,7 +136,7 @@ class Chat(object):
if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self):
def unmute(self) -> None:
""" unmutes the chat
:returns: None
@@ -144,7 +145,7 @@ class Chat(object):
if not bool(ret):
raise ValueError("Failed to unmute chat")
def get_mute_duration(self):
def get_mute_duration(self) -> int:
""" Returns the number of seconds until the mute of this chat is lifted.
:param duration:
@@ -152,37 +153,37 @@ class Chat(object):
"""
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
def get_ephemeral_timer(self):
def get_ephemeral_timer(self) -> int:
""" 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):
def set_ephemeral_timer(self, timer: int) -> bool:
""" set ephemeral timer.
:param: timer value in seconds
:returns: None
:returns: True on success, False otherwise
"""
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer))
def get_type(self):
def get_type(self) -> int:
""" (deprecated) return type of this chat.
:returns: one of const.DC_CHAT_TYPE_*
"""
return lib.dc_chat_get_type(self._dc_chat)
def get_encryption_info(self):
def get_encryption_info(self) -> Optional[str]:
"""Return encryption info for this chat.
:returns: a string with encryption preferences of all chat members"""
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
return from_dc_charpointer(res)
def get_join_qr(self):
def get_join_qr(self) -> Optional[str]:
""" get/create Join-Group QR Code as ascii-string.
this string needs to be transferred to another DC account
@@ -194,7 +195,7 @@ class Chat(object):
# ------ chat messaging API ------------------------------
def send_msg(self, msg):
def send_msg(self, msg: Message) -> Message:
"""send a message by using a ready Message object.
:param msg: a :class:`deltachat.message.Message` instance
@@ -512,8 +513,9 @@ class Chat(object):
latitude=lib.dc_array_get_latitude(dc_array, i),
longitude=lib.dc_array_get_longitude(dc_array, i),
accuracy=lib.dc_array_get_accuracy(dc_array, i),
timestamp=datetime.utcfromtimestamp(
lib.dc_array_get_timestamp(dc_array, i)
timestamp=datetime.fromtimestamp(
lib.dc_array_get_timestamp(dc_array, i),
timezone.utc
),
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
)
+9 -3
View File
@@ -1,7 +1,13 @@
from typing import Any, List
from .capi import lib
for name in dir(lib):
def __getattr__(name: str) -> Any:
if name.startswith("DC_"):
globals()[name] = getattr(lib, name)
del name
return getattr(lib, name)
return globals()[name]
def __dir__() -> List[str]:
return sorted(name for name in dir(lib) if name.startswith("DC_"))
+10 -6
View File
@@ -1,6 +1,9 @@
from .capi import lib
from .capi import ffi
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional, TypeVar, Generator, Callable
T = TypeVar('T')
def as_dc_charpointer(obj):
@@ -11,21 +14,22 @@ def as_dc_charpointer(obj):
return obj
def iter_array(dc_array_t, constructor):
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
yield constructor(lib.dc_array_get_id(dc_array_t, i))
def from_dc_charpointer(obj):
def from_dc_charpointer(obj) -> Optional[str]:
if obj != ffi.NULL:
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
return None
class DCLot:
def __init__(self, dc_lot):
def __init__(self, dc_lot) -> None:
self._dc_lot = dc_lot
def id(self):
def id(self) -> int:
return lib.dc_lot_get_id(self._dc_lot)
def state(self):
@@ -44,4 +48,4 @@ class DCLot:
ts = lib.dc_lot_get_timestamp(self._dc_lot)
if ts == 0:
return None
return datetime.utcfromtimestamp(ts)
return datetime.fromtimestamp(ts, timezone.utc)
+2 -2
View File
@@ -11,7 +11,7 @@ from imapclient import IMAPClient
from imapclient.exceptions import IMAPClientError
import imaplib
import deltachat
from deltachat import const
from deltachat import const, Account
SEEN = b'\\Seen'
@@ -62,7 +62,7 @@ def dc_account_after_shutdown(account):
class DirectImap:
def __init__(self, account):
def __init__(self, account: Account) -> None:
self.account = account
self.logid = account.get_config("displayname") or id(account)
self._idling = False
+11 -10
View File
@@ -13,7 +13,7 @@ from .cutil import from_dc_charpointer
class FFIEvent:
def __init__(self, name, data1, data2):
def __init__(self, name: str, data1, data2):
self.name = name
self.data1 = data1
self.data2 = data2
@@ -29,13 +29,13 @@ class FFIEventLogger:
# to prevent garbled logging
_loglock = threading.RLock()
def __init__(self, account):
def __init__(self, account) -> None:
self.account = account
self.logid = self.account.get_config("displayname")
self.init_time = time.time()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None:
self.account.log(str(ffi_event))
@account_hookimpl
@@ -69,7 +69,7 @@ class FFIEventTracker:
self._event_queue = Queue()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
def ac_process_ffi_event(self, ffi_event: FFIEvent):
self._event_queue.put(ffi_event)
def set_timeout(self, timeout):
@@ -96,7 +96,7 @@ class FFIEventTracker:
if rex.match(ev.name):
return ev
def get_info_contains(self, regex):
def get_info_contains(self, regex: str) -> FFIEvent:
rex = re.compile(regex)
while 1:
ev = self.get_matching("DC_EVENT_INFO")
@@ -176,6 +176,7 @@ class FFIEventTracker:
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data2 > 0:
return self.account.get_message_by_id(ev.data2)
return None
def wait_msg_delivered(self, msg):
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
@@ -189,7 +190,7 @@ class EventThread(threading.Thread):
With each Account init this callback thread is started.
"""
def __init__(self, account):
def __init__(self, account) -> None:
self.account = account
super(EventThread, self).__init__(name="events")
self.setDaemon(True)
@@ -202,17 +203,17 @@ class EventThread(threading.Thread):
yield
self.account.log(message + " FINISHED")
def mark_shutdown(self):
def mark_shutdown(self) -> None:
self._marked_for_shutdown = True
def wait(self, timeout=None):
def wait(self, timeout=None) -> None:
if self == threading.current_thread():
# we are in the callback thread and thus cannot
# wait for the thread-loop to finish.
return
self.join(timeout=timeout)
def run(self):
def run(self) -> None:
""" get and run events until shutdown. """
with self.log_execution("EVENT THREAD"):
self._inner_run()
@@ -250,7 +251,7 @@ class EventThread(threading.Thread):
if self.account._dc_context is not None:
raise
def _map_ffi_event(self, ffi_event):
def _map_ffi_event(self, ffi_event: FFIEvent):
name = ffi_event.name
account = self.account
if name == "DC_EVENT_CONFIGURE_PROGRESS":
+4 -4
View File
@@ -6,7 +6,7 @@ from . import props
from .cutil import from_dc_charpointer, as_dc_charpointer
from .capi import lib, ffi
from . import const
from datetime import datetime
from datetime import datetime, timezone
class Message(object):
@@ -170,7 +170,7 @@ class Message(object):
:returns: naive datetime.datetime() object.
"""
ts = lib.dc_msg_get_timestamp(self._dc_msg)
return datetime.utcfromtimestamp(ts)
return datetime.fromtimestamp(ts, timezone.utc)
@props.with_doc
def time_received(self):
@@ -180,7 +180,7 @@ class Message(object):
"""
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
if ts:
return datetime.utcfromtimestamp(ts)
return datetime.fromtimestamp(ts, timezone.utc)
@props.with_doc
def ephemeral_timer(self):
@@ -200,7 +200,7 @@ class Message(object):
"""
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
if ts:
return datetime.utcfromtimestamp(ts)
return datetime.fromtimestamp(ts, timezone.utc)
@property
def quoted_text(self):
+1 -1
View File
@@ -14,7 +14,7 @@ class Provider(object):
:param domain: The email to get the provider info for.
"""
def __init__(self, account, addr):
def __init__(self, account, addr) -> None:
provider = ffi.gc(
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
lib.dc_provider_unref,
+25 -17
View File
@@ -9,6 +9,7 @@ import fnmatch
import time
import weakref
import tempfile
from typing import List, Dict, Callable
import pytest
import requests
@@ -126,7 +127,7 @@ def pytest_report_header(config, startdir):
class SessionLiveConfigFromFile:
def __init__(self, fn):
def __init__(self, fn) -> None:
self.fn = fn
self.configlist = []
for line in open(fn):
@@ -137,19 +138,21 @@ class SessionLiveConfigFromFile:
d[name] = value
self.configlist.append(d)
def get(self, index):
def get(self, index: int):
return self.configlist[index]
def exists(self):
def exists(self) -> bool:
return bool(self.configlist)
class SessionLiveConfigFromURL:
def __init__(self, url):
configlist: List[Dict[str, str]]
def __init__(self, url: str) -> None:
self.configlist = []
self.url = url
def get(self, index):
def get(self, index: int):
try:
return self.configlist[index]
except IndexError:
@@ -162,7 +165,7 @@ class SessionLiveConfigFromURL:
self.configlist.append(config)
return config
def exists(self):
def exists(self) -> bool:
return bool(self.configlist)
@@ -179,7 +182,7 @@ def session_liveconfig(request):
@pytest.fixture
def data(request):
class Data:
def __init__(self):
def __init__(self) -> None:
# trying to find test data heuristically
# because we are run from a dev-setup with pytest direct,
# through tox, and then maybe also from deltachat-binding
@@ -210,7 +213,10 @@ def data(request):
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
class AccountMaker:
def __init__(self):
_finalizers: List[Callable[[], None]]
_accounts: List[Account]
def __init__(self) -> None:
self.live_count = 0
self.offline_count = 0
self._finalizers = []
@@ -423,7 +429,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
pass
imap.dump_imap_structures(tmpdir, logfile=logfile)
def get_accepted_chat(self, ac1, ac2):
def get_accepted_chat(self, ac1: Account, ac2: Account):
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
@@ -451,7 +457,9 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
class BotProcess:
def __init__(self, popen, bot_cfg):
stdout_queue: queue.Queue
def __init__(self, popen, bot_cfg) -> None:
self.popen = popen
self.addr = bot_cfg["addr"]
@@ -459,10 +467,10 @@ class BotProcess:
# the (unicode) lines available for readers through a queue.
self.stdout_queue = queue.Queue()
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
t.setDaemon(1)
t.setDaemon(True)
t.start()
def _run_stdout_thread(self):
def _run_stdout_thread(self) -> None:
try:
while 1:
line = self.popen.stdout.readline()
@@ -474,10 +482,10 @@ class BotProcess:
finally:
self.stdout_queue.put(None)
def kill(self):
def kill(self) -> None:
self.popen.kill()
def wait(self, timeout=30):
def wait(self, timeout=30) -> None:
self.popen.wait(timeout=timeout)
def fnmatch_lines(self, pattern_lines):
@@ -509,14 +517,14 @@ def tmp_db_path(tmpdir):
@pytest.fixture
def lp():
class Printer:
def sec(self, msg):
def sec(self, msg: str) -> None:
print()
print("=" * 10, msg, "=" * 10)
def step(self, msg):
def step(self, msg: str) -> None:
print("-" * 5, "step " + msg, "-" * 5)
def indent(self, msg):
def indent(self, msg: str) -> None:
print(" " + msg)
return Printer()
+37 -3
View File
@@ -10,7 +10,7 @@ from deltachat.tracker import ImexTracker
from deltachat.hookspec import account_hookimpl
from deltachat.capi import ffi, lib
from deltachat.cutil import iter_array
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
@pytest.mark.parametrize("msgtext,res", [
@@ -447,7 +447,7 @@ class TestOfflineChat:
contact1.create_chat().send_text("hello")
def test_chat_message_distinctions(self, ac1, chat1):
past1s = datetime.utcnow() - timedelta(seconds=1)
past1s = datetime.now(timezone.utc) - timedelta(seconds=1)
msg = chat1.send_text("msg1")
ts = msg.time_sent
assert msg.time_received is None
@@ -1221,6 +1221,40 @@ class TestOnlineAccount:
assert not msg.is_encrypted()
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
def test_gossip_optimization(self, acfactory, lp):
"""Test that gossip timestamp is updated when someone else sends gossip,
so we don't have to send gossip ourselves.
"""
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
acfactory.introduce_each_other([ac1, ac2])
acfactory.introduce_each_other([ac2, ac3])
lp.sec("ac1 creates a group chat with ac2")
group_chat = ac1.create_group_chat("hello")
group_chat.add_contact(ac2)
msg = group_chat.send_text("hi")
# No Autocrypt gossip was sent yet.
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp == 0
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.text == "hi"
lp.sec("ac2 adds ac3 to the group")
msg.chat.add_contact(ac3)
lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
# ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
# ac1 does not need to send gossip because ac2 already did it.
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp == int(msg.time_sent.timestamp())
def test_gossip_encryption_preference(self, acfactory, lp):
"""Test that encryption preference of group members is gossiped to new members.
This is a Delta Chat extension to Autocrypt 1.1.0, which Autocrypt-Gossip headers
@@ -2163,7 +2197,7 @@ class TestOnlineAccount:
break # DC is done with reading messages
def test_send_receive_locations(self, acfactory, lp):
now = datetime.utcnow()
now = datetime.now(timezone.utc)
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")
+11
View File
@@ -1,7 +1,9 @@
[tox]
isolated_build = true
envlist =
py3
lint
mypy
auditwheels
[testenv]
@@ -42,6 +44,15 @@ commands =
flake8 tests/ examples/
rst-lint --encoding 'utf-8' README.rst
[testenv:mypy]
deps =
mypy
typing
types-setuptools
types-requests
commands =
mypy --no-incremental src/
[testenv:doc]
changedir=doc
deps =
+46 -47
View File
@@ -266,7 +266,7 @@ impl ChatId {
/// Updates chat blocked status.
///
/// Returns true if the value was modified.
async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
pub(crate) async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
if self.is_special() {
bail!("ignoring setting of Block-status for {}", self);
}
@@ -397,7 +397,7 @@ impl ChatId {
context.emit_event(EventType::ChatModified(self));
// make sure, the receivers will get all keys
reset_gossiped_timestamp(context, self).await?;
self.reset_gossiped_timestamp(context).await?;
Ok(())
}
@@ -857,6 +857,48 @@ impl ChatId {
pub fn to_u32(self) -> u32 {
self.0
}
pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
self.set_gossiped_timestamp(context, 0).await
}
/// Get timestamp of the last gossip sent in the chat.
/// Zero return value means that gossip was never sent.
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
let timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
paramsv![self],
)
.await?;
Ok(timestamp.unwrap_or_default())
}
pub(crate) async fn set_gossiped_timestamp(
self,
context: &Context,
timestamp: i64,
) -> Result<()> {
ensure!(
!self.is_special(),
"can not set gossiped timestamp for special chats"
);
info!(
context,
"set gossiped_timestamp for chat {} to {}.", self, timestamp,
);
context
.sql
.execute(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
paramsv![timestamp, self],
)
.await?;
Ok(())
}
}
impl std::fmt::Display for ChatId {
@@ -1057,10 +1099,6 @@ impl Chat {
Ok(None)
}
pub async fn get_gossiped_timestamp(&self, context: &Context) -> Result<i64> {
get_gossiped_timestamp(context, self.id).await
}
pub async fn get_color(&self, context: &Context) -> Result<u32> {
let mut color = 0;
@@ -1093,7 +1131,7 @@ impl Chat {
name: self.name.clone(),
archived: self.visibility == ChatVisibility::Archived,
param: self.param.to_string(),
gossiped_timestamp: self.get_gossiped_timestamp(context).await?,
gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
is_sending_locations: self.is_sending_locations,
color: self.get_color(context).await?,
profile_image: self
@@ -2371,7 +2409,7 @@ pub(crate) async fn add_contact_to_chat_ex(
let contact = Contact::get_by_id(context, contact_id).await?;
let mut msg = Message::default();
reset_gossiped_timestamp(context, chat_id).await?;
chat_id.reset_gossiped_timestamp(context).await?;
/*this also makes sure, not contacts are added to special or normal chats*/
let mut chat = Chat::load_from_db(context, chat_id).await?;
@@ -2453,45 +2491,6 @@ pub(crate) async fn add_contact_to_chat_ex(
Ok(true)
}
pub(crate) async fn reset_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<()> {
set_gossiped_timestamp(context, chat_id, 0).await
}
/// Get timestamp of the last gossip sent in the chat.
/// Zero return value means that gossip was never sent.
pub async fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<i64> {
let timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
paramsv![chat_id],
)
.await?;
Ok(timestamp.unwrap_or_default())
}
pub(crate) async fn set_gossiped_timestamp(
context: &Context,
chat_id: ChatId,
timestamp: i64,
) -> Result<()> {
ensure!(!chat_id.is_special(), "can not add member to special chats");
info!(
context,
"set gossiped_timestamp for chat #{} to {}.", chat_id, timestamp,
);
context
.sql
.execute(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
paramsv![timestamp, chat_id],
)
.await?;
Ok(())
}
pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool> {
// versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others.
// to avoid sending out previously set selfavatars unexpectedly we added this additional check.
+8
View File
@@ -30,6 +30,7 @@ use crate::sql::Sql;
#[derive(Clone, Debug)]
pub struct Context {
pub(crate) inner: Arc<InnerContext>,
pub disable_needs_move: bool, // TODO just added for some extra profiling info
}
impl Deref for Context {
@@ -76,6 +77,11 @@ pub struct InnerContext {
pub(crate) id: u32,
creation_time: SystemTime,
/// The text of the last error logged and emitted as an event.
/// If the ui wants to display an error after a failure,
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: RwLock<String>,
}
#[derive(Debug)]
@@ -147,10 +153,12 @@ impl Context {
quota: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
last_error: RwLock::new("".to_string()),
};
let ctx = Context {
inner: Arc::new(inner),
disable_needs_move: false,
};
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
+349 -220
View File
@@ -3,7 +3,7 @@
use std::cmp::min;
use std::convert::TryFrom;
use anyhow::{bail, ensure, Result};
use anyhow::{bail, ensure, Context as _, Result};
use itertools::join;
use mailparse::SingleInfo;
use num_traits::FromPrimitive;
@@ -187,8 +187,13 @@ pub(crate) async fn dc_receive_imf_inner(
.and_then(|value| mailparse::dateparse(value).ok())
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp));
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
let better_msg = stock_str::msg_location_enabled_by(context, from_id as u32).await;
set_better_msg(&mut mime_parser, &better_msg);
}
// Add parts
let chat_id = add_parts(
let (chat_id, msgs) = add_parts(
context,
&mut mime_parser,
imf_raw,
@@ -212,6 +217,26 @@ pub(crate) async fn dc_receive_imf_inner(
.await
.map_err(|err| err.context("add_parts error"))?;
// Update gossiped timestamp for the chat if someone else or our other device sent
// Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves
// and waste traffic.
if !chat_id.is_special()
&& mime_parser
.recipients
.iter()
.all(|recipient| mime_parser.gossiped_addr.contains(&recipient.addr))
{
info!(
context,
"Received message contains Autocrypt-Gossip for all members, updating timestamp."
);
if chat_id.get_gossiped_timestamp(context).await? < sent_timestamp {
chat_id
.set_gossiped_timestamp(context, sent_timestamp)
.await?;
}
}
let insert_msg_id = if let Some((_chat_id, msg_id)) = created_db_entries.last() {
*msg_id
} else {
@@ -298,7 +323,11 @@ pub(crate) async fn dc_receive_imf_inner(
)
.await?;
}
} else if insert_msg_id
} else if msgs
.first()
.unwrap() // TODO unwrap() (should be safe though)
.clone() // TODO unnecessary clone
.into_lazy()
.needs_move(context, server_folder.as_ref())
.await
.unwrap_or_default()
@@ -414,7 +443,7 @@ async fn add_parts(
create_event_to_send: &mut Option<CreateEvent>,
fetching_existing_messages: bool,
prevent_rename: bool,
) -> Result<ChatId> {
) -> Result<(ChatId, Vec<Message>)> {
let mut chat_id = None;
let mut chat_id_blocked = Blocked::Not;
let mut incoming_origin = incoming_origin;
@@ -488,7 +517,7 @@ async fn add_parts(
}
Err(err) => {
warn!(context, "Error in Secure-Join message handling: {}", err);
return Ok(DC_CHAT_ID_TRASH);
return Ok((DC_CHAT_ID_TRASH, Vec::new()));
}
}
} else {
@@ -531,7 +560,6 @@ async fn add_parts(
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
context,
mime_parser,
sent_timestamp,
if test_normal_chat.is_none() {
allow_creation
} else {
@@ -569,6 +597,16 @@ async fn add_parts(
}
}
}
apply_group_changes(
context,
mime_parser,
sent_timestamp,
chat_id,
from_id,
to_ids,
)
.await?;
}
if chat_id.is_none() {
@@ -644,11 +682,12 @@ async fn add_parts(
}
if let Some(chat_id) = chat_id {
if Blocked::Not != chat_id_blocked {
if Blocked::Not == create_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
} else if parent.is_some() {
if chat_id_blocked != Blocked::Not {
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
if create_blocked == Blocked::Request && parent.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
// the contact requests will pop up and this should be just fine.
Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo)
@@ -705,7 +744,7 @@ async fn add_parts(
}
Err(err) => {
warn!(context, "Error in Secure-Join watching: {}", err);
return Ok(DC_CHAT_ID_TRASH);
return Ok((DC_CHAT_ID_TRASH, Vec::new()));
}
}
} else if mime_parser.sync_items.is_some() && self_sent {
@@ -755,7 +794,6 @@ async fn add_parts(
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
context,
mime_parser,
sent_timestamp,
allow_creation,
Blocked::Not,
from_id,
@@ -786,9 +824,9 @@ async fn add_parts(
}
if let Some(chat_id) = chat_id {
if Blocked::Not != chat_id_blocked && Blocked::Not == create_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
if chat_id_blocked != Blocked::Not && chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
}
}
@@ -830,9 +868,10 @@ async fn add_parts(
DC_CHAT_ID_TRASH
});
// Extract ephemeral timer from the message.
let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer)
{
// Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
let mut ephemeral_timer = if is_partial_download.is_some() {
chat_id.get_ephemeral_timer(context).await?
} else if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) {
match value.parse::<EphemeralTimer>() {
Ok(timer) => timer,
Err(err) => {
@@ -970,7 +1009,7 @@ async fn add_parts(
sort_timestamp,
)
.await?;
return Ok(chat_id); // do not return an error as this would result in retrying the message
return Ok((chat_id, Vec::new())); // do not return an error as this would result in retrying the message
}
}
set_better_msg(
@@ -1041,6 +1080,7 @@ async fn add_parts(
let mut ids = Vec::with_capacity(parts.len());
let conn = context.sql.get_conn().await?;
let mut msgs = Vec::new();
for part in &mut parts {
let mut txt_raw = "".to_string();
@@ -1098,6 +1138,53 @@ INSERT INTO msgs
// also change `MsgId::trash()` and `delete_expired_messages()`
let trash = chat_id.is_trash();
let msg = Message {
id: MsgId::new(0),
rfc724_mid: rfc724_mid.to_string(),
server_uid: server_uid,
chat_id,
from_id: if trash { 0 } else { from_id },
to_id: if trash { 0 } else { to_id },
timestamp_sort: sort_timestamp,
timestamp_sent: sent_timestamp,
timestamp_rcvd: rcvd_timestamp,
viewtype: part.typ,
state,
is_dc_message,
text: if trash {
None
} else {
Some(part.msg.to_string())
},
subject: if trash {
"".to_string()
} else {
subject.to_string()
},
param: if trash {
Params::new()
} else {
part.param.clone()
},
in_reply_to: Some(mime_in_reply_to.to_string()), // TODO be careful with Some("") and None
mime_modified,
error: part.error.clone(),
ephemeral_timer,
ephemeral_timestamp,
download_state: if is_partial_download.is_some() {
DownloadState::Available
} else {
DownloadState::Done
},
hidden: false,
chat_blocked: chat_id_blocked, // TODO not sure if correct
location_id: 0,
server_folder: Some(server_folder.to_string()),
};
// TODO mabye reuse some of the fields from above in the query below to avoid duplicate code
msgs.push(msg);
stmt.execute(paramsv![
rfc724_mid,
server_folder,
@@ -1188,7 +1275,7 @@ INSERT INTO msgs
}
}
Ok(chat_id)
Ok((chat_id, msgs))
}
/// Saves attached locations to the database.
@@ -1288,15 +1375,6 @@ async fn lookup_chat_by_reply(
if let Some(parent) = parent {
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
if let Some(msg_grpid) = try_getting_grpid(mime_parser) {
if msg_grpid == parent_chat.grpid {
// This message will be assigned to this chat, anyway.
// But if we assigned it now, create_or_lookup_group() will not be called
// and group commands will not be executed.
return Ok(None);
}
}
if parent.error.is_some() {
// If the parent msg is undecipherable, then it may have been assigned to the wrong chat
// (undecipherable group msgs often get assigned to the 1:1 chat with the sender).
@@ -1357,42 +1435,22 @@ async fn is_probably_private_reply(
Ok(true)
}
/// This function tries to extract the group-id from the message and returns the
/// corresponding chat_id. If the chat does not exist, it is created.
/// If the message contains groups commands (name, profile image, changed members),
/// they are executed as well.
///
/// If no group-id could be extracted, message is assigned to the same chat as the
/// parent message.
///
/// If there is no parent message in the database, and there are more than two members,
/// a new ad-hoc group is created.
/// This function tries to extract the group-id from the message and returns the corresponding
/// chat_id. If the chat does not exist, it is created. If there is no group-id and there are more
/// than two members, a new ad hoc group is created.
///
/// On success the function returns the found/created (chat_id, chat_blocked) tuple.
#[allow(non_snake_case, clippy::cognitive_complexity)]
async fn create_or_lookup_group(
context: &Context,
mime_parser: &mut MimeMessage,
sent_timestamp: i64,
allow_creation: bool,
create_blocked: Blocked,
from_id: u32,
to_ids: &ContactIds,
) -> Result<Option<(ChatId, Blocked)>> {
let mut chat_id_blocked = Blocked::Not;
let mut recreate_member_list = false;
let mut send_EVENT_CHAT_MODIFIED = false;
let mut X_MrAddToGrp = None;
let mut better_msg: String = From::from("");
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
better_msg = stock_str::msg_location_enabled_by(context, from_id as u32).await;
set_better_msg(mime_parser, &better_msg);
}
let grpid = if let Some(grpid) = try_getting_grpid(mime_parser) {
grpid
} else {
} else if allow_creation {
let mut member_ids: Vec<u32> = to_ids.iter().copied().collect();
if !member_ids.contains(&(from_id as u32)) {
member_ids.push(from_id as u32);
@@ -1400,24 +1458,26 @@ async fn create_or_lookup_group(
if !member_ids.contains(&(DC_CONTACT_ID_SELF as u32)) {
member_ids.push(DC_CONTACT_ID_SELF as u32);
}
if !allow_creation {
info!(context, "creating ad-hoc group prevented from caller");
return Ok(None);
}
return create_adhoc_group(context, mime_parser, create_blocked, &member_ids)
let res = create_adhoc_group(context, mime_parser, create_blocked, &member_ids)
.await
.map(|chat_id| chat_id.map(|chat_id| (chat_id, create_blocked)))
.map_err(|err| {
info!(context, "could not create adhoc-group: {:?}", err);
err
});
.context("could not create ad hoc group")?
.map(|chat_id| (chat_id, create_blocked));
return Ok(res);
} else {
info!(context, "creating ad-hoc group prevented from caller");
return Ok(None);
};
// check, if we have a chat with this group ID
let mut chat_id = chat::get_chat_id_by_grpid(context, &grpid)
.await?
.map(|(chat_id, _protected, _blocked)| chat_id);
let mut chat_id;
let mut chat_id_blocked;
if let Some((id, _protected, blocked)) = chat::get_chat_id_by_grpid(context, &grpid).await? {
chat_id = Some(id);
chat_id_blocked = blocked;
} else {
chat_id = None;
chat_id_blocked = Default::default();
}
// For chat messages, we don't have to guess (is_*probably*_private_reply()) but we know for sure that
// they belong to the group because of the Chat-Group-Id or Message-Id header
@@ -1429,69 +1489,6 @@ async fn create_or_lookup_group(
}
}
// now we have a grpid that is non-empty
// but we might not know about this group
let grpname = mime_parser.get_header(HeaderDef::ChatGroupName).cloned();
let mut removed_id = None;
if let Some(removed_addr) = mime_parser
.get_header(HeaderDef::ChatGroupMemberRemoved)
.cloned()
{
removed_id = Contact::lookup_id_by_addr(context, &removed_addr, Origin::Unknown).await?;
match removed_id {
Some(contact_id) => {
mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup;
better_msg = if contact_id == from_id {
stock_str::msg_group_left(context, from_id).await
} else {
stock_str::msg_del_member(context, &removed_addr, from_id).await
};
}
None => warn!(context, "removed {:?} has no contact_id", removed_addr),
}
} else {
let field = mime_parser
.get_header(HeaderDef::ChatGroupMemberAdded)
.cloned();
if let Some(added_member) = field {
mime_parser.is_system_message = SystemMessage::MemberAddedToGroup;
better_msg = stock_str::msg_add_member(context, &added_member, from_id).await;
X_MrAddToGrp = Some(added_member);
} else if let Some(old_name) = mime_parser.get_header(HeaderDef::ChatGroupNameChanged) {
better_msg = stock_str::msg_grp_name(
context,
old_name,
if let Some(ref name) = grpname {
name
} else {
""
},
from_id as u32,
)
.await;
mime_parser.is_system_message = SystemMessage::GroupNameChanged;
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
mime_parser.is_system_message = SystemMessage::GroupImageChanged;
better_msg = match avatar_action {
AvatarAction::Delete => {
stock_str::msg_grp_img_deleted(context, from_id).await
}
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
}
};
}
}
}
}
set_better_msg(mime_parser, &better_msg);
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
warn!(context, "verification problem: {}", err);
@@ -1503,55 +1500,56 @@ async fn create_or_lookup_group(
ProtectionStatus::Unprotected
};
// check if the group does not exist but should be created
let group_explicitly_left = chat::is_group_explicitly_left(context, &grpid)
.await
.unwrap_or_default();
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
.context("no address configured")?;
if chat_id.is_none()
&& !mime_parser.is_mailinglist_message()
&& !grpid.is_empty()
&& grpname.is_some()
&& mime_parser.get_header(HeaderDef::ChatGroupName).is_some()
// otherwise, a pending "quit" message may pop up
&& removed_id.is_none()
&& mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved).is_none()
// re-create explicitly left groups only if ourself is re-added
&& (!group_explicitly_left
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
&& (!chat::is_group_explicitly_left(context, &grpid).await?
|| mime_parser.get_header(HeaderDef::ChatGroupMemberAdded).map_or(false, |member_addr| addr_cmp(&self_addr, member_addr)))
{
// group does not exist but should be created
// Group does not exist but should be created.
if !allow_creation {
info!(context, "creating group forbidden by caller");
return Ok(None);
}
chat_id = ChatId::create_multiuser_record(
let grpname = mime_parser.get_header(HeaderDef::ChatGroupName).unwrap();
let new_chat_id = ChatId::create_multiuser_record(
context,
Chattype::Group,
&grpid,
grpname.as_ref().unwrap(),
grpname,
create_blocked,
create_protected,
)
.await
.map(Some)
.unwrap_or_else(|err| {
warn!(
context,
"Failed to create group '{}' for grpid={}: {:?}",
grpname.as_ref().unwrap(),
grpid,
err,
);
None
});
.with_context(|| format!("Failed to create group '{}' for grpid={}", grpname, grpid))?;
chat_id = Some(new_chat_id);
chat_id_blocked = create_blocked;
recreate_member_list = true;
// Create initial member list.
chat::add_to_chat_contacts_table(context, new_chat_id, DC_CONTACT_ID_SELF).await?;
if from_id > DC_CONTACT_ID_LAST_SPECIAL
&& !chat::is_contact_in_chat(context, new_chat_id, from_id).await?
{
chat::add_to_chat_contacts_table(context, new_chat_id, from_id).await?;
}
for &to_id in to_ids.iter() {
info!(context, "adding to={:?} to chat id={}", to_id, new_chat_id);
if !Contact::addr_equals_contact(context, &self_addr, to_id).await?
&& !chat::is_contact_in_chat(context, new_chat_id, to_id).await?
{
chat::add_to_chat_contacts_table(context, new_chat_id, to_id).await?;
}
}
// once, we have protected-chats explained in UI, we can uncomment the following lines.
// ("verified groups" did not add a message anyway)
@@ -1559,25 +1557,16 @@ async fn create_or_lookup_group(
//if create_protected == ProtectionStatus::Protected {
// set from_id=0 as it is not clear that the sender of this random group message
// actually really has enabled chat-protection at some point.
//chat_id
//new_chat_id
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
// .await?;
//}
} else if let Some(chat_id) = chat_id {
if create_protected == ProtectionStatus::Protected {
let chat = Chat::load_from_db(context, chat_id).await?;
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
recreate_member_list = true;
}
}
context.emit_event(EventType::ChatModified(new_chat_id));
}
// again, check chat_id
let chat_id = if let Some(chat_id) = chat_id {
chat_id
if let Some(chat_id) = chat_id {
Ok(Some((chat_id, chat_id_blocked)))
} else if mime_parser.decrypting_failed {
// It is possible that the message was sent to a valid,
// yet unknown group, which was rejected because
@@ -1585,65 +1574,141 @@ async fn create_or_lookup_group(
// not found. We can't create a properly named group in
// this case, so assign error message to 1:1 chat with the
// sender instead.
return Ok(None);
Ok(None)
} else {
// The message was decrypted successfully, but contains a late "quit" or otherwise
// unwanted message.
info!(context, "message belongs to unwanted group (TRASH)");
return Ok(Some((DC_CHAT_ID_TRASH, chat_id_blocked)));
};
Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)))
}
}
// We have a valid chat_id > DC_CHAT_ID_LAST_SPECIAL.
/// Apply group member list, name, avatar and protection status changes from the MIME message.
async fn apply_group_changes(
context: &Context,
mime_parser: &mut MimeMessage,
sent_timestamp: i64,
chat_id: ChatId,
from_id: u32,
to_ids: &ContactIds,
) -> Result<()> {
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ != Chattype::Group {
return Ok(());
}
// execute group commands
if X_MrAddToGrp.is_some() {
recreate_member_list = true;
} else if mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.is_some()
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context("no address configured")?;
let mut recreate_member_list = false;
let mut send_event_chat_modified = false;
let removed_id;
if let Some(removed_addr) = mime_parser
.get_header(HeaderDef::ChatGroupMemberRemoved)
.cloned()
{
if let Some(ref grpname) = grpname {
if grpname.len() < 200
&& chat_id
removed_id = Contact::lookup_id_by_addr(context, &removed_addr, Origin::Unknown).await?;
match removed_id {
Some(contact_id) => {
mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup;
let better_msg = if contact_id == from_id {
stock_str::msg_group_left(context, from_id).await
} else {
stock_str::msg_del_member(context, &removed_addr, from_id).await
};
set_better_msg(mime_parser, &better_msg);
}
None => warn!(context, "removed {:?} has no contact_id", removed_addr),
}
} else {
removed_id = None;
if let Some(added_member) = mime_parser
.get_header(HeaderDef::ChatGroupMemberAdded)
.cloned()
{
mime_parser.is_system_message = SystemMessage::MemberAddedToGroup;
let better_msg = stock_str::msg_add_member(context, &added_member, from_id).await;
set_better_msg(mime_parser, &better_msg);
recreate_member_list = true;
} else if let Some(old_name) = mime_parser.get_header(HeaderDef::ChatGroupNameChanged) {
if let Some(grpname) = mime_parser
.get_header(HeaderDef::ChatGroupName)
.filter(|grpname| grpname.len() < 200)
{
if chat_id
.update_timestamp(context, Param::GroupNameTimestamp, sent_timestamp)
.await?
{
info!(context, "updating grpname for chat {}", chat_id);
if context
.sql
.execute(
"UPDATE chats SET name=? WHERE id=?;",
paramsv![grpname.to_string(), chat_id],
)
.await
.is_ok()
{
context.emit_event(EventType::ChatModified(chat_id));
info!(context, "updating grpname for chat {}", chat_id);
context
.sql
.execute(
"UPDATE chats SET name=? WHERE id=?;",
paramsv![grpname.to_string(), chat_id],
)
.await?;
send_event_chat_modified = true;
}
let better_msg =
stock_str::msg_grp_name(context, old_name, grpname, from_id as u32).await;
set_better_msg(mime_parser, &better_msg);
mime_parser.is_system_message = SystemMessage::GroupNameChanged;
}
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
mime_parser.is_system_message = SystemMessage::GroupImageChanged;
let better_msg = match avatar_action {
AvatarAction::Delete => {
stock_str::msg_grp_img_deleted(context, from_id).await
}
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
}
};
set_better_msg(mime_parser, &better_msg);
}
}
}
} else if mime_parser.is_system_message == SystemMessage::ChatProtectionEnabled {
recreate_member_list = true;
}
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(&s);
}
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
recreate_member_list = true;
}
}
if let Some(avatar_action) = &mime_parser.group_avatar {
info!(context, "group-avatar change for {}", chat_id);
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
if chat
.param
.update_timestamp(Param::AvatarTimestamp, sent_timestamp)?
{
match avatar_action {
AvatarAction::Change(profile_image) => {
chat.param.set(Param::ProfileImage, profile_image);
}
AvatarAction::Delete => {
chat.param.remove(Param::ProfileImage);
}
};
chat.update_param(context).await?;
send_EVENT_CHAT_MODIFIED = true;
}
if chat
.param
.update_timestamp(Param::AvatarTimestamp, sent_timestamp)?
{
match avatar_action {
AvatarAction::Change(profile_image) => {
chat.param.set(Param::ProfileImage, profile_image);
}
AvatarAction::Delete => {
chat.param.remove(Param::ProfileImage);
}
};
chat.update_param(context).await?;
send_event_chat_modified = true;
}
}
@@ -1663,8 +1728,7 @@ async fn create_or_lookup_group(
"DELETE FROM chats_contacts WHERE chat_id=?;",
paramsv![chat_id],
)
.await
.ok();
.await?;
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await?;
}
@@ -1682,7 +1746,7 @@ async fn create_or_lookup_group(
chat::add_to_chat_contacts_table(context, chat_id, to_id).await?;
}
}
send_EVENT_CHAT_MODIFIED = true;
send_event_chat_modified = true;
}
} else if let Some(contact_id) = removed_id {
if chat_id
@@ -1690,14 +1754,14 @@ async fn create_or_lookup_group(
.await?
{
chat::remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
send_EVENT_CHAT_MODIFIED = true;
send_event_chat_modified = true;
}
}
if send_EVENT_CHAT_MODIFIED {
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat_id));
}
Ok(Some((chat_id, chat_id_blocked)))
Ok(())
}
/// Create or lookup a mailing list chat.
@@ -2044,7 +2108,7 @@ async fn check_verified_properties(
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
// mark gossiped keys (if any) as verified
if mimeparser.gossipped_addr.contains(&to_addr) {
if mimeparser.gossiped_addr.contains(&to_addr) {
if let Some(mut peerstate) = peerstate {
// if we're here, we know the gossip key is verified:
// - use the gossip-key as verified-key if there is no verified-key
@@ -4576,6 +4640,71 @@ Reply to all"#,
}
}
/// Tests that replies to similar ad hoc groups are correctly assigned to chats.
///
/// The difficutly here is that ad hoc groups don't have unique group IDs, because both
/// messages have the same recipient lists and only differ in the subject and message contents.
/// The messages can be properly assigned to chats only using the In-Reply-To or References
/// headers.
#[async_std::test]
async fn test_chat_assignment_adhoc() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_alice().await;
alice.set_config(Config::ShowEmails, Some("2")).await?;
bob.set_config(Config::ShowEmails, Some("2")).await?;
let first_thread_mime = br#"Subject: First thread
Message-ID: first@example.org
To: Alice <alice@example.com>, Bob <bob@example.net>
From: Claire <claire@example.org>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First thread."#;
let second_thread_mime = br#"Subject: Second thread
Message-ID: second@example.org
To: Alice <alice@example.com>, Bob <bob@example.net>
From: Claire <claire@example.org>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Second thread."#;
// Alice receives two classic emails from Claire.
dc_receive_imf(&alice, first_thread_mime, "Inbox", 1, false).await?;
let alice_first_msg = alice.get_last_msg().await;
dc_receive_imf(&alice, second_thread_mime, "Inbox", 2, false).await?;
let alice_second_msg = alice.get_last_msg().await;
// Bob receives the same two emails.
dc_receive_imf(&bob, first_thread_mime, "Inbox", 1, false).await?;
let bob_first_msg = bob.get_last_msg().await;
dc_receive_imf(&bob, second_thread_mime, "Inbox", 2, false).await?;
let bob_second_msg = bob.get_last_msg().await;
// Messages go to separate chats both for Alice and Bob.
assert!(alice_first_msg.chat_id != alice_second_msg.chat_id);
assert!(bob_first_msg.chat_id != bob_second_msg.chat_id);
// Alice replies to both chats. Bob receives two messages and assigns them to corresponding
// chats.
alice_first_msg.chat_id.accept(&alice).await?;
let alice_first_reply = alice
.send_text(alice_first_msg.chat_id, "First reply")
.await;
bob.recv_msg(&alice_first_reply).await;
let bob_first_reply = bob.get_last_msg().await;
assert_eq!(bob_first_reply.chat_id, bob_first_msg.chat_id);
alice_second_msg.chat_id.accept(&alice).await?;
let alice_second_reply = alice
.send_text(alice_second_msg.chat_id, "Second reply")
.await;
bob.recv_msg(&alice_second_reply).await;
let bob_second_reply = bob.get_last_msg().await;
assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id);
Ok(())
}
/// Test that read receipts don't create chats.
#[async_std::test]
async fn test_read_receipts_dont_create_chats() -> Result<()> {
+37
View File
@@ -227,6 +227,7 @@ mod tests {
use crate::chat::send_msg;
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf_inner;
use crate::ephemeral::Timer;
use crate::test_utils::TestContext;
use num_traits::FromPrimitive;
@@ -343,4 +344,40 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_partial_download_and_ephemeral() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = t
.create_chat_with_contact("bob", "bob@example.org")
.await
.id;
chat_id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
.await?;
// download message from bob partially, this must not change the ephemeral timer
dc_receive_imf_inner(
&t,
b"From: Bob <bob@example.org>\n\
To: Alice <alice@example.com>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain",
"INBOX",
1,
false,
Some(100000),
false,
)
.await?;
assert_eq!(
chat_id.get_ephemeral_timer(&t).await?,
Timer::Enabled { duration: 60 }
);
Ok(())
}
}
+2
View File
@@ -361,6 +361,7 @@ impl Imap {
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& err.to_string().to_lowercase().contains("authentication")
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
@@ -1597,6 +1598,7 @@ async fn precheck_imf(
if delete_server_after != Some(0) {
if msg_id
.to_lazy()
.needs_move(context, server_folder)
.await
.unwrap_or_default()
+9 -4
View File
@@ -561,11 +561,16 @@ impl Job {
}
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let server_folder = &job_try!(msg
let server_folder = job_try!(msg
.server_folder
.as_ref()
.context("Can't move message out of folder if we don't know the current folder"));
let move_res = msg.id.needs_move(context, server_folder).await;
let move_res = msg
.clone() // TODO avoid clone()?
.into_lazy()
.needs_move(context, &server_folder)
.await;
let dest_folder = match move_res {
Err(e) => {
warn!(context, "could not load dest folder: {}", e);
@@ -589,7 +594,7 @@ impl Job {
if let Some(dest_folder) = dest_folder {
match imap
.mv(context, server_folder, msg.server_uid, &dest_folder)
.mv(context, &server_folder, msg.server_uid, &dest_folder)
.await
{
ImapActionResult::RetryLater => Status::RetryLater,
@@ -991,7 +996,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
}
if rendered_msg.is_gossiped {
chat::set_gossiped_timestamp(context, msg.chat_id, time()).await?;
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
}
if 0 != rendered_msg.last_added_location_id {
+47
View File
@@ -1,6 +1,7 @@
//! # Logging.
use crate::context::Context;
use async_std::task::block_on;
#[macro_export]
macro_rules! info {
@@ -39,10 +40,28 @@ macro_rules! error {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
$ctx.set_last_error(&formatted);
$ctx.emit_event($crate::EventType::Error(formatted));
}};
}
impl Context {
/// Set last error string.
/// Implemented as blocking as used from macros in different, not always async blocks.
pub fn set_last_error(&self, error: &str) {
block_on(async move {
let mut last_error = self.last_error.write().await;
*last_error = error.to_string();
});
}
/// Get last error string.
pub async fn get_last_error(&self) -> String {
let last_error = &*self.last_error.read().await;
last_error.clone()
}
}
pub trait LogExt<T, E>
where
Self: std::marker::Sized,
@@ -134,3 +153,31 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
self
}
}
#[cfg(test)]
mod tests {
use crate::test_utils::TestContext;
use anyhow::Result;
#[async_std::test]
async fn test_get_last_error() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.get_last_error().await, "");
error!(t, "foo-error");
assert_eq!(t.get_last_error().await, "foo-error");
warn!(t, "foo-warning");
assert_eq!(t.get_last_error().await, "foo-error");
info!(t, "foo-info");
assert_eq!(t.get_last_error().await, "foo-error");
error!(t, "bar-error");
error!(t, "baz-error");
assert_eq!(t.get_last_error().await, "baz-error");
Ok(())
}
}
+105 -64
View File
@@ -6,6 +6,7 @@ use std::convert::TryInto;
use anyhow::{ensure, format_err, Context as _, Result};
use async_std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::OnceCell;
use rusqlite::types::ValueRef;
use serde::{Deserialize, Serialize};
@@ -53,6 +54,13 @@ impl MsgId {
MsgId(0)
}
pub fn to_lazy(&self) -> LazyMsg {
LazyMsg {
id: *self,
message: OnceCell::new(),
}
}
/// Whether the message ID signifies a special message.
///
/// This kind of message ID can not be used for real messages.
@@ -83,65 +91,6 @@ impl MsgId {
Ok(result)
}
/// Returns Some if the message needs to be moved from `folder`.
/// If yes, returns `ConfiguredInboxFolder`, `ConfiguredMvboxFolder` or `ConfiguredSentboxFolder`,
/// depending on where the message should be moved
pub async fn needs_move(self, context: &Context, folder: &str) -> Result<Option<Config>> {
use Config::*;
if context.is_mvbox(folder).await? {
return Ok(None);
}
let msg = Message::load_from_db(context, self).await?;
if context.is_spam_folder(folder).await? {
let msg_unblocked = msg.chat_id != DC_CHAT_ID_TRASH && msg.chat_blocked == Blocked::Not;
return if msg_unblocked {
if self.needs_move_to_mvbox(context, &msg).await? {
Ok(Some(ConfiguredMvboxFolder))
} else {
Ok(Some(ConfiguredInboxFolder))
}
} else {
// Blocked or contact request message in the spam folder, leave it there
Ok(None)
};
}
if self.needs_move_to_mvbox(context, &msg).await? {
Ok(Some(ConfiguredMvboxFolder))
} else if msg.state.is_outgoing()
&& msg.is_dc_message == MessengerMessage::Yes
&& !msg.is_setupmessage()
&& msg.to_id != DC_CONTACT_ID_SELF // Leave self-chat-messages in the inbox, not sure about this
&& context.is_inbox(folder).await?
&& context.get_config_bool(SentboxMove).await?
&& context.get_config(ConfiguredSentboxFolder).await?.is_some()
{
Ok(Some(ConfiguredSentboxFolder))
} else {
Ok(None)
}
}
async fn needs_move_to_mvbox(self, context: &Context, msg: &Message) -> Result<bool> {
if !context.get_config_bool(Config::MvboxMove).await? {
return Ok(false);
}
if msg.is_setupmessage() {
// do not move setup messages;
// there may be a non-delta device that wants to handle it
return Ok(false);
}
match msg.is_dc_message {
MessengerMessage::No => Ok(false),
MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
}
}
/// Put message into trash chat and delete message text.
///
/// It means the message is deleted locally, but not on the server.
@@ -282,6 +231,90 @@ impl Default for MessengerMessage {
}
}
#[derive(Debug)]
pub struct LazyMsg {
id: MsgId,
message: OnceCell<Message>,
}
impl LazyMsg {
// TODO should get() require &mut self? Or lock a mutex during computation?
async fn get(&self, context: &Context) -> Result<&Message> {
if let Some(m) = self.message.get() {
Ok(m)
} else {
let m = Message::load_from_db(context, self.id).await?;
self.message.set(m).ok();
self.message.get().context("message.set() failed???")
}
}
/// Returns Some if the message needs to be moved from `folder`.
/// If yes, returns `ConfiguredInboxFolder`, `ConfiguredMvboxFolder` or `ConfiguredSentboxFolder`,
/// depending on where the message should be moved
pub async fn needs_move(&self, context: &Context, folder: &str) -> Result<Option<Config>> {
use Config::*;
if context.disable_needs_move {
return Ok(None);
}
if context.is_mvbox(folder).await? {
return Ok(None);
}
let msg = self.get(context).await?;
if context.is_spam_folder(folder).await? {
let msg_unblocked = msg.chat_id != DC_CHAT_ID_TRASH && msg.chat_blocked == Blocked::Not;
return if msg_unblocked {
if self.needs_move_to_mvbox(context).await? {
Ok(Some(ConfiguredMvboxFolder))
} else {
Ok(Some(ConfiguredInboxFolder))
}
} else {
// Blocked or contact request message in the spam folder, leave it there
Ok(None)
};
}
if self.needs_move_to_mvbox(context).await? {
Ok(Some(ConfiguredMvboxFolder))
} else if msg.state.is_outgoing()
&& msg.is_dc_message == MessengerMessage::Yes
&& !msg.is_setupmessage()
&& msg.to_id != DC_CONTACT_ID_SELF // Leave self-chat-messages in the inbox, not sure about this
&& context.is_inbox(folder).await?
&& context.get_config_bool(SentboxMove).await?
&& context.get_config(ConfiguredSentboxFolder).await?.is_some()
{
Ok(Some(ConfiguredSentboxFolder))
} else {
Ok(None)
}
}
async fn needs_move_to_mvbox(&self, context: &Context) -> Result<bool> {
if !context.get_config_bool(Config::MvboxMove).await? {
return Ok(false);
}
let msg = self.get(context).await?;
if msg.is_setupmessage() {
// do not move setup messages;
// there may be a non-delta device that wants to handle it
return Ok(false);
}
match msg.is_dc_message {
MessengerMessage::No => Ok(false),
MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
}
}
}
/// An object representing a single message in memory.
/// The message object is not updated.
/// If you want an update, you have to recreate the object.
@@ -426,6 +459,13 @@ impl Message {
Ok(msg)
}
pub fn into_lazy(self) -> LazyMsg {
LazyMsg {
id: self.id,
message: self.into(),
}
}
pub fn get_filemime(&self) -> Option<String> {
if let Some(m) = self.param.get(Param::MimeType) {
return Some(m.to_string());
@@ -1990,11 +2030,12 @@ mod tests {
let exists = rfc724_mid_exists(&t, "abc@example.com").await.unwrap();
let (folder_1, _, msg_id) = exists.unwrap();
assert_eq!(folder, folder_1);
let actual = if let Some(config) = msg_id.needs_move(&t.ctx, folder).await.unwrap() {
t.ctx.get_config(config).await.unwrap()
} else {
None
};
let actual =
if let Some(config) = msg_id.to_lazy().needs_move(&t.ctx, folder).await.unwrap() {
t.ctx.get_config(config).await.unwrap()
} else {
None
};
let expected = if expected_destination == folder {
None
} else {
+1 -1
View File
@@ -361,7 +361,7 @@ impl<'a> MimeFactory<'a> {
match &self.loaded {
Loaded::Message { chat } => {
// beside key- and member-changes, force re-gossip every 48 hours
let gossiped_timestamp = chat.get_gossiped_timestamp(context).await?;
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
Ok(true)
} else {
+43 -34
View File
@@ -57,7 +57,9 @@ pub struct MimeMessage {
/// this set is empty.
pub signatures: HashSet<Fingerprint>,
pub gossipped_addr: HashSet<String>,
/// The set of mail recipient addresses for which gossip headers were applied, regardless of
/// whether they modified any peerstates.
pub gossiped_addr: HashSet<String>,
pub is_forwarded: bool,
pub is_system_message: SystemMessage,
pub location_kml: Option<location::Kml>,
@@ -198,7 +200,7 @@ impl MimeMessage {
// Memory location for a possible decrypted message.
let mut mail_raw = Vec::new();
let mut gossipped_addr = Default::default();
let mut gossiped_addr = Default::default();
let (mail, signatures, warn_empty_signature) =
match e2ee::try_decrypt(context, &mail, message_time).await {
@@ -221,7 +223,7 @@ impl MimeMessage {
if !signatures.is_empty() {
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossipped_addr = update_gossip_peerstates(
gossiped_addr = update_gossip_peerstates(
context,
message_time,
&mail,
@@ -279,7 +281,7 @@ impl MimeMessage {
// only non-empty if it was a valid autocrypt message
signatures,
gossipped_addr,
gossiped_addr,
is_forwarded: false,
mdn_reports: Vec::new(),
is_system_message: SystemMessage::Unknown,
@@ -1380,6 +1382,9 @@ impl MimeMessage {
}
}
/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
///
/// Returns the set of mail recipient addresses for which valid gossip headers were found.
async fn update_gossip_peerstates(
context: &Context,
message_time: i64,
@@ -1387,42 +1392,46 @@ async fn update_gossip_peerstates(
gossip_headers: Vec<String>,
) -> Result<HashSet<String>> {
// XXX split the parsing from the modification part
let mut gossipped_addr: HashSet<String> = Default::default();
let mut gossiped_addr: HashSet<String> = Default::default();
for value in &gossip_headers {
let gossip_header = value.parse::<Aheader>();
if let Ok(ref header) = gossip_header {
if get_recipients(&mail.headers)
.iter()
.any(|info| info.addr == header.addr.to_lowercase())
{
let mut peerstate = Peerstate::from_addr(context, &header.addr).await?;
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_gossip(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_gossip(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
if let Some(peerstate) = peerstate {
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
}
gossipped_addr.insert(header.addr.clone());
} else {
warn!(
context,
"Ignoring gossipped \"{}\" as the address is not in To/Cc list.", &header.addr,
);
let header = match value.parse::<Aheader>() {
Ok(header) => header,
Err(err) => {
warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
continue;
}
};
if !get_recipients(&mail.headers)
.iter()
.any(|info| info.addr == header.addr.to_lowercase())
{
warn!(
context,
"Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
);
continue;
}
let peerstate;
if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
p.apply_gossip(&header, message_time);
p.save_to_db(&context.sql, false).await?;
peerstate = p;
} else {
let p = Peerstate::from_gossip(&header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = p;
};
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
gossiped_addr.insert(header.addr.clone());
}
Ok(gossipped_addr)
Ok(gossiped_addr)
}
#[derive(Debug)]
+41 -10
View File
@@ -499,6 +499,12 @@ pub(crate) async fn handle_securejoin_handshake(
inviter_progress!(context, contact_id, 300);
// for setup-contact, make Alice's one-to-one chat with Bob visible
// (secure-join-information are shown in the group chat)
if !join_vg {
ChatId::create_for_contact(context, contact_id).await?;
}
// Alice -> Bob
send_alice_handshake_msg(
context,
@@ -527,9 +533,18 @@ pub(crate) async fn handle_securejoin_handshake(
Ok(HandshakeMessage::Done)
}
Some(_stage) => {
let msg = stock_str::secure_join_replies(context, contact_id).await;
chat::add_info_msg(context, bobstate.chat_id(context).await?, msg, time())
if join_vg {
// the message reads "Alice replied, waiting for being added to the group…";
// show it only on secure-join and not on setup-contact therefore.
let msg = stock_str::secure_join_replies(context, contact_id).await;
chat::add_info_msg(
context,
bobstate.chat_id(context).await?,
msg,
time(),
)
.await?;
}
joiner_progress!(context, bobstate.invite().contact_id(), 400);
Ok(HandshakeMessage::Done)
}
@@ -858,7 +873,7 @@ async fn could_not_establish_secure_connection(
)
.await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
error!(
warn!(
context,
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
);
@@ -936,6 +951,8 @@ mod tests {
async fn test_setup_contact() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
// Setup JoinerProgress sinks.
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
@@ -954,6 +971,7 @@ mod tests {
// Step 2: Bob scans QR-code, sends vc-request
dc_join_securejoin(&bob.ctx, &qr).await?;
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
let sent = bob.pop_sent_msg().await;
assert!(!bob.ctx.has_ongoing().await);
@@ -965,6 +983,7 @@ mod tests {
// Step 3: Alice receives vc-request, sends vc-auth-required
alice.recv_msg(&sent).await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
@@ -1041,6 +1060,11 @@ mod tests {
VerifiedStatus::BidirectVerified
);
// exactly one one-to-one chat should be visible for both now
// (check this before calling alice.create_chat() explicitly below)
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
// Check Alice got the verified message in her 1:1 chat.
{
let chat = alice.create_chat(&bob).await;
@@ -1302,6 +1326,8 @@ mod tests {
async fn test_secure_join() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
// Setup JoinerProgress sinks.
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
@@ -1323,12 +1349,9 @@ mod tests {
.await
.unwrap();
// Step 2: Bob scans QR-code, sends vg-request; blocks on ongoing process
let joiner = {
let qr = qr.clone();
let ctx = bob.ctx.clone();
async_std::task::spawn(async move { dc_join_securejoin(&ctx, &qr).await.unwrap() })
};
// Step 2: Bob scans QR-code, sends vg-request
let bob_chatid = dc_join_securejoin(&bob.ctx, &qr).await?;
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
let sent = bob.pop_sent_msg().await;
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
@@ -1444,9 +1467,9 @@ mod tests {
"vg-member-added-received"
);
let bob_chatid = joiner.await;
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
assert!(bob_chat.is_protected());
assert!(bob_chat.typ == Chattype::Group);
assert!(!bob.ctx.has_ongoing().await);
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
@@ -1454,6 +1477,14 @@ mod tests {
// however, should not be visible in the UIs.
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
let bobs_chat_with_alice = bob.create_chat(&alice).await;
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
alice.recv_msg(&sent).await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
Ok(())
}
}
+29 -9
View File
@@ -3,7 +3,7 @@
use async_std::path::Path;
use async_std::sync::RwLock;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::time::Duration;
@@ -39,12 +39,14 @@ mod migrations;
#[derive(Debug)]
pub struct Sql {
pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>,
config_cache: RwLock<HashMap<String, String>>, // TODO should be <String, Option<String>> in order to save that an option is None?
}
impl Default for Sql {
fn default() -> Self {
Self {
pool: RwLock::new(None),
config_cache: RwLock::new(HashMap::new()),
}
}
}
@@ -387,6 +389,8 @@ impl Sql {
/// will already have been logged.
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> {
let key = key.as_ref();
// TODO could be on another thread
if let Some(value) = value {
let exists = self
.exists(
@@ -413,20 +417,36 @@ impl Sql {
.await?;
}
let mut lock = self.config_cache.write().await;
if let Some(v) = value {
lock.insert(key.to_string(), v.to_string());
} else {
lock.remove(key);
}
drop(lock);
Ok(())
}
/// Get configuration options from the database.
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Result<Option<String>> {
let value = self
.query_get_value(
"SELECT value FROM config WHERE keyname=?;",
paramsv![key.as_ref()],
)
.await
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
let lock = self.config_cache.read().await;
let cached = lock.get(key.as_ref()).cloned();
drop(lock);
Ok(value)
if let Some(c) = cached {
Ok(Some(c))
} else {
let value = self
.query_get_value(
"SELECT value FROM config WHERE keyname=?;",
paramsv![key.as_ref()],
)
.await
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
Ok(value)
}
}
pub async fn set_raw_config_int(&self, key: impl AsRef<str>, value: i32) -> Result<()> {
+4
View File
@@ -526,6 +526,10 @@ impl Sql {
})
.await?;
let mut lock = self.config_cache.write().await;
lock.insert(VERSION_CFG.to_string(), format!("{}", version));
drop(lock);
Ok(())
}
}