Compare commits

..

82 Commits

Author SHA1 Message Date
Sebastian Klähn
bf52a77f98 typos 2023-01-30 18:47:07 +01:00
Sebastian Klähn
d850813ef0 Revert "fix test by expecting CHAT_MODIFIED another time"
This reverts commit 57ba2387e8.
2023-01-30 17:53:56 +01:00
Sebastian Klähn
7f4cebc08b fmt 2023-01-30 17:53:13 +01:00
Sebastian Klähn
d422ab8105 add tests 2023-01-30 17:50:38 +01:00
Sebastian Klähn
6529cfff03 properly handle mua users 2023-01-30 13:06:32 +01:00
Sebastian Klähn
061fd1dd2d move function to better place 2023-01-30 12:30:51 +01:00
septias
41f45bd680 fmt 2023-01-25 18:02:08 +01:00
septias
18dce04608 improvements 2023-01-25 17:55:47 +01:00
Sebastian Klähn
fe11198dbc Merge branch 'master' into fix3782 2023-01-23 11:10:41 +01:00
link2xt
fac7b064b4 Refine Python CI
Add lint environment to `deltachat-rpc-client/`
and set line length to 120, same as in `python/`.

Switch from flake8 to ruff.

Fix ruff warnings.
2023-01-20 16:53:21 +00:00
link2xt
ef6f252842 Introduce DNS cache for IMAP connections
DNS cache is used as a fallback if TCP connection
to all IP addresses returned by DNS failed.
If strict TLS checks are disabled,
DNS cache results are stored, but not used.

GitHub Pull Request: <https://github.com/deltachat/deltachat-core-rust/pull/3970>
2023-01-20 10:21:38 +00:00
link2xt
b8da19e49f Upgrade async-smtp to v0.6 2023-01-19 22:10:40 +00:00
link2xt
a483df8b20 Add all resolver results with the same timestamp 2023-01-19 21:29:17 +00:00
link2xt
41ccc13394 Extend lookup_host_with_cache comment 2023-01-19 21:06:31 +00:00
link2xt
0978357c5f Do not cache IP addresses which resolve into themselves 2023-01-19 20:43:53 +00:00
link2xt
7935085e74 Remove port number from DNS cache table 2023-01-19 20:26:11 +00:00
link2xt
7a47c9e38b Adapt nicer_configuration_error to new error message 2023-01-19 18:29:18 +00:00
link2xt
20124bfca0 Add DNS lookup timeout 2023-01-19 17:33:59 +00:00
link2xt
eaeaa297c7 Maximize priority of the cached address on successful connection 2023-01-19 16:55:43 +00:00
link2xt
9adb9ab5f4 Return last connection error from connect_tcp 2023-01-19 16:50:50 +00:00
link2xt
c4c4c977a6 Changelog 2023-01-19 16:50:50 +00:00
link2xt
7d508dcb52 Log DNS resolution errors instead of failing directly 2023-01-19 16:50:50 +00:00
link2xt
773754d74f Introduce DNS cache table
Only used for IMAP connections currently.
2023-01-19 16:50:50 +00:00
link2xt
ed20a23297 Resolve IP addresses explicitly 2023-01-19 16:10:26 +00:00
link2xt
4615c84f31 Automatically group imports using nightly rustfmt 2023-01-19 13:13:25 +00:00
link2xt
677136f4ab Use SMTP pipelining 2023-01-19 01:30:32 +00:00
iequidoo
3c3710420b Don't unpin chat on sending / receiving not fresh messages (#3967)
This bug was introduced by 3cf78749df - there are three visibility
states: Archived, Pinned and Normal - in the database, this "visibility" is named historically
"archived" ... the original code has an AND archived=1 therefore.
2023-01-18 16:57:38 -03:00
link2xt
de268b8225 Terminate recently seen loop if cannot receive interrupts
It seems .abort() does not work on the recently seen loop
in some cases, e.g. if it is busy looping in a separate thread.

In my case after account reconfiguration recently seen loop
kept running and issuing warnings about closed interrupt channel.

Exit from recently seen loop on errors to avoid using 100% CPU
in such cases.
2023-01-18 10:48:54 +00:00
link2xt
42c709e7b1 Fix SOCKS5 usage for IMAP
Connect to SOCKS5 server rather than target server
and send TCP connect command.
2023-01-18 10:13:17 +00:00
link2xt
cf0349acc8 configure: log SOCKS5 configuration for IMAP like we do for SMTP 2023-01-18 09:32:29 +00:00
link2xt
e43b36b61f Peerstate.verifier fixes
Derive Debug, PartialEq and Eq for Peerstate,
so `verifier` is included in Debug output and compared.

Store verifier as empty string
instead of NULL in the database.
2023-01-17 14:22:22 +00:00
Simon Laux
ed24867309 fix verifier-by addr was empty string instead of None (#3961)
fix verifier-by addr was empty string intead of None
2023-01-17 13:06:57 +00:00
iequidoo
3cf78749df Emit DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived chats with
unread messages increases (#3940)
2023-01-16 16:05:47 -03:00
iequidoo
badbf766bb Move event emitting for a new message to a separate function 2023-01-16 16:05:47 -03:00
holger krekel
57ba2387e8 fix test by expecting CHAT_MODIFIED another time 2023-01-16 15:27:23 +01:00
bjoern
5b265dbc1c remove comma from unit in message-details (#3954)
it shoud read "filename.ext, 123 bytes" and not "filename.ext, 123, bytes"
2023-01-13 18:25:20 +01:00
link2xt
0053e143e7 Do not emit ChatModified event when user avatar is updated 2023-01-12 20:52:47 +00:00
link2xt
1c44135b41 Remove deprecated attach_selfavatar config
According to the comment it was added in Dec 2019
with an intention to remove it "after some time".
2023-01-12 20:52:47 +00:00
iequidoo
5f883a4445 Prepare to remove "vc-contact-confirm-received", "vg-member-added-received" messages from Securejoin
protocol
2023-01-12 15:13:30 -03:00
iequidoo
8dc6ff268d check_verified_properties(): Don't ignore fails of Peerstate::set_verified()
- Return Result from set_verified() so that it can't be missed.
- Pass Fingerprint to set_verified() by value to avoid cloning it inside. This optimises out an
  extra clone() if we already have a value that can be moved at the caller side. However, this may
  add an extra clone() if set_verified() fails, but let's not optimise the fail scenario.
2023-01-12 15:13:30 -03:00
iequidoo
57d7df530b Add a test that a new verified member is seen on the second device going online (#3836)
- Alice has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
- Bob joins the group and sends a message there. Alice sees it.
- Alice's second devices goes online, but doesn't see Bob in the group.
2023-01-12 15:13:30 -03:00
iequidoo
13b2fe8d30 import_self_keys(): Log set_self_key() error as DC_EVENT_INFO
It isn't an error actually since we just skip the file.
2023-01-12 15:13:30 -03:00
iequidoo
d644988845 Securejoin: Fix adding and handling Autocrypt-Gossip headers (#3836)
- If bcc_self is set, gossip headers must be added despite of the number of group members.
- If another device observes Secure-Join, instead of looking for Secure-Join-Fingerprint in
  "vg-member-added"/"vc-contact-confirm" messages it must use keys from Autocrypt-Gossip headers as
  described in the Countermitm doc
  (https://countermitm.readthedocs.io/en/latest/new.html#joining-a-verified-group-secure-join).
2023-01-12 15:13:30 -03:00
iequidoo
27c6cfc958 Log messages in info!() if DCC_MIME_DEBUG is set
Using println!() leads to reordered output on terminal. Moreover, println!() prints to stdout which
is not for logging.
2023-01-12 15:13:30 -03:00
link2xt
3b9a48ff5f python: remove "data1=0" from INFO/WARNING/ERROR events display 2023-01-12 18:12:05 +00:00
link2xt
a5354ded3f ci: disable fail-fast
This setting is true by default and causes
Windows build to cancel when Linux fails
due to flaky test and vice versa.
Cancelled test then has to be restarted
from scratch even though it was not going
to fail.
2023-01-12 17:51:58 +00:00
Simon Laux
0b07dafe77 add verified-by api to jsonrpc (#3946)
also refactor it so that it is not a static method anymore
(would have resulted in two load-Contact-from-db-calls in jsonrpc)
2023-01-12 16:13:27 +00:00
link2xt
f0e900b885 Cleanup constants module 2023-01-12 13:19:16 +00:00
link2xt
f460043e87 Add missing documentation to webxdc module 2023-01-12 13:18:30 +00:00
link2xt
8c6b804d73 Merge "Add ContactAddress type" (#3947) 2023-01-11 23:19:48 +00:00
link2xt
790512d52a Reduce code indentation 2023-01-11 23:19:07 +00:00
link2xt
89c8d26968 Add ContactAddress type 2023-01-11 23:07:47 +00:00
iequidoo
6d9d31cad1 Add timeouts to HTTP requests (#3908) 2023-01-11 14:29:49 -03:00
link2xt
6642083f52 Clippy fix 2023-01-10 21:17:30 +00:00
link2xt
554090b03e Prepare 1.106.0 2023-01-10 20:57:14 +00:00
link2xt
1cde09c312 Add missing documentation to the message module 2023-01-10 20:52:22 +00:00
link2xt
e215b4d919 Return Option from Contact::add_or_lookup()
This allows to distinguish exceptions,
such as database errors, from invalid user input.
For example, if the From: field of the message
does not look like an email address, the mail
should be ignored. But if there is a database
failure while writing a new contact for the address,
this error should be bubbled up.
2023-01-10 20:43:20 +00:00
link2xt
5ae6c25394 Remove aheader module dependency on mailparse 2023-01-10 14:53:48 +00:00
Hocuri
68cd8dbc3e Only send IncomingMsgBunch if there are more than 0 new messages (#3941) 2023-01-10 13:18:40 +00:00
link2xt
01fe88e337 Save modified .toml if absolute paths were replaced with relative
Previously this required adding or removing an account.
2023-01-10 01:37:54 +00:00
link2xt
2b8923931e Add more context to IMAP errors 2023-01-10 00:00:23 +00:00
Hocuri
8d119713bc Print chats when a test failed (#3937)
* Print chats after a test failed again

E.g.
```
========== Chats of bob: ==========
Single#Chat#10: alice@example.org [alice@example.org]
--------------------------------------------------------------------------------
Msg#10:  (Contact#Contact#10): hellooo [FRESH]
Msg#11:  (Contact#Contact#10): hellooo without mailing list [FRESH]
--------------------------------------------------------------------------------

========== Chats of alice: ==========
Single#Chat#10: bob@example.net [bob@example.net]
--------------------------------------------------------------------------------
Msg#10: Me (Contact#Contact#Self): hellooo  √
Msg#11: Me (Contact#Contact#Self): hellooo without mailing list  √
--------------------------------------------------------------------------------
```

I found this very useful sometimes, so, let's try to re-introduce it (it
was removed in #3449)

* Add failing test for TestContext::drop()

* Do not panic in TestContext::drop() if runtime is dropped

Co-authored-by: link2xt <link2xt@testrun.org>
2023-01-09 22:46:32 +01:00
Simon Laux
07f2e28eca fix: only send contact changed event for recently seen if it is relevant (#3938)
(no events for contacts that are already offline again)

This dramatically speeds up replay after backup import on desktop.
2023-01-09 21:12:33 +00:00
link2xt
0e849609f4 ci: python: do not try to upload source distributions
They are not useful because you still need a statically
linked rust library besides the C FFI module, and
they are not built by tox 4 without changes.
2023-01-09 16:20:56 +00:00
Sebastian Klähn
8e0626e995 Merge branch 'master' into fix3782 2023-01-05 18:55:47 +01:00
Sebastian Klähn
7273c917a6 remove logs 2022-12-27 18:08:20 +01:00
Sebastian Klähn
d0871d3bd7 fmt + changelog 2022-12-27 17:41:21 +01:00
Sebastian Klähn
c8fe830c33 Revert "Fix test_synchronize_member_list_on_group_rejoin"
This reverts commit 0215046c76.
2022-12-27 17:24:24 +01:00
Sebastian Klähn
12dbf41116 Merge branch 'fix3782' of https://github.com/deltachat/deltachat-core-rust into fix3782 2022-12-27 17:23:15 +01:00
Sebastian Klähn
f7bf31dbea recteate memberlist on groupjoin 2022-12-27 17:23:04 +01:00
link2xt
0215046c76 Fix test_synchronize_member_list_on_group_rejoin 2022-12-27 16:07:29 +00:00
Sebastian Klähn
b5fc9aedc5 Update src/mimeparser.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2022-12-27 14:58:06 +01:00
Sebastian Klähn
0250b6f421 Update src/chat.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2022-12-27 14:58:06 +01:00
Septias
7415eadb78 fix suggested changes 2022-12-27 14:58:06 +01:00
Sebastian Klähn
8d077b964c Update src/chat.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2022-12-27 14:57:25 +01:00
Sebastian Klähn
99923fd816 Update src/chat.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2022-12-27 14:57:25 +01:00
Sebastian Klähn
f3614536d9 debug 2022-12-27 14:57:25 +01:00
Sebastian Klähn
a9a485d19e recreate test 2022-12-27 14:57:25 +01:00
Sebastian Klähn
32d54c8b93 clippy fix 2022-12-27 14:57:09 +01:00
Sebastian Klähn
f9a5cf9b11 use correct method & handle message deletions correctly 2022-12-27 14:56:24 +01:00
Sebastian Klähn
71ada54703 documentation 2022-12-27 14:56:24 +01:00
Sebastian Klähn
81695d6b80 don't always build new contact list 2022-12-27 14:56:24 +01:00
130 changed files with 2211 additions and 1115 deletions

View File

@@ -67,6 +67,7 @@ jobs:
build_and_test:
name: Build and test
strategy:
fail-fast: false
matrix:
include:
# Currently used Rust version.
@@ -143,7 +144,7 @@ jobs:
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
working-directory: deltachat-rpc-client
run: tox -e py3
run: tox -e py3,lint
- name: install pypy
if: ${{ matrix.python }}

View File

@@ -3,10 +3,34 @@
## Unreleased
### Changes
### API-Changes
- Pipeline SMTP commands #3924
- Cache DNS results #3970
### Fixes
- Securejoin: Fix adding and handling Autocrypt-Gossip headers #3914
- fix verifier-by addr was empty string intead of None #3961
- Emit DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived chats with
unread messages increases #3959
- Fix Peerstate comparison #3962
- Log SOCKS5 configuration for IMAP like already done for SMTP #3964
- Fix SOCKS5 usage for IMAP #3965
- Exit from recently seen loop on interrupt channel errors to avoid busy looping #3966
### API-Changes
- jsonrpc: add verified-by information to `Contact`-Object
- Remove `attach_selfavatar` config #3951
## 1.106.0
### Changes
- Only send IncomingMsgBunch if there are more than 0 new messages #3941
### Fixes
- fix: only send contact changed event for recently seen if it is relevant (not too old to matter) #3938
- Immediately save `accounts.toml` if it was modified by a migration from absolute paths to relative paths #3943
- Do not treat invalid email addresses as an exception #3942
- Add timeouts to HTTP requests #3948
## 1.105.0
@@ -33,13 +57,13 @@
### Fixes
- Do not add an error if the message is encrypted but not signed #3860
- Do not strip leading spaces from message lines #3867
- Don't always rebuild group member lists #3872
- Fix uncaught exception in JSON-RPC tests #3884
- Fix STARTTLS connection and add a test for it #3907
- Trigger reconnection when failing to fetch existing messages #3911
- Do not retry fetching existing messages after failure, prevents infinite reconnection loop #3913
- Ensure format=flowed formatting is always reversible on the receiver side #3880
## 1.104.0
### Changes

12
Cargo.lock generated
View File

@@ -165,9 +165,9 @@ dependencies = [
[[package]]
name = "async-smtp"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6da21e1dd19fbad3e095ad519fb1558ab77fd82e5c4778dca8f9be0464589e1e"
checksum = "2ade89127f9e0d44f9e83cf574d499060005cd45b7dc76be89c0167487fe8edd"
dependencies = [
"async-native-tls",
"async-trait",
@@ -873,7 +873,7 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
[[package]]
name = "deltachat"
version = "1.105.0"
version = "1.106.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -947,7 +947,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.105.0"
version = "1.106.0"
dependencies = [
"anyhow",
"async-channel",
@@ -969,7 +969,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.105.0"
version = "1.106.0"
dependencies = [
"anyhow",
"deltachat-jsonrpc",
@@ -992,7 +992,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.105.0"
version = "1.106.0"
dependencies = [
"anyhow",
"deltachat",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.105.0"
version = "1.106.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.63"
@@ -25,7 +25,7 @@ ansi_term = { version = "0.12.1", optional = true }
anyhow = "1"
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
async-smtp = { version = "0.6", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
trust-dns-resolver = "0.22"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar

View File

@@ -1,6 +1,7 @@
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::accounts::Accounts;
use std::path::PathBuf;
use tempfile::tempdir;
async fn create_accounts(n: u32) {

View File

@@ -1,7 +1,6 @@
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;

View File

@@ -1,7 +1,6 @@
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;

View File

@@ -1,8 +1,9 @@
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
use std::path::Path;
async fn search_benchmark(dbfile: impl AsRef<Path>) {
let id = 100;

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.105.0"
version = "1.106.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -5811,7 +5811,7 @@ void dc_event_unref(dc_event_t* event);
* @param data2 (int) The progress as:
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
* 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
* 1000=Protocol finished for this contact.
*/
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060

View File

@@ -23,13 +23,6 @@ use std::sync::Arc;
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use num_traits::{FromPrimitive, ToPrimitive};
use once_cell::sync::Lazy;
use rand::Rng;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, ContactId, Origin};
@@ -37,21 +30,28 @@ use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
use num_traits::{FromPrimitive, ToPrimitive};
use once_cell::sync::Lazy;
use rand::Rng;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
mod dc_array;
mod lot;
mod string;
use self::string::*;
use deltachat::chatlist::Chatlist;
use self::string::*;
// as C lacks a good and portable error handling,
// in general, the C Interface is forgiving wrt to bad parameters.
// - objects returned by some functions
@@ -3975,13 +3975,10 @@ pub unsafe extern "C" fn dc_contact_get_verifier_addr(
}
let ffi_contact = &*contact;
let ctx = &*ffi_contact.context;
block_on(Contact::get_verifier_addr(
ctx,
&ffi_contact.contact.get_id(),
))
.log_err(ctx, "failed to get verifier for contact")
.unwrap_or_default()
.strdup()
block_on(ffi_contact.contact.get_verifier_addr(ctx))
.log_err(ctx, "failed to get verifier for contact")
.unwrap_or_default()
.strdup()
}
#[no_mangle]
@@ -3992,12 +3989,12 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t)
}
let ffi_contact = &*contact;
let ctx = &*ffi_contact.context;
let contact_id = block_on(Contact::get_verifier_id(ctx, &ffi_contact.contact.get_id()))
let verifier_contact_id = block_on(ffi_contact.contact.get_verifier_id(ctx))
.log_err(ctx, "failed to get verifier")
.unwrap_or_default()
.unwrap_or_default();
contact_id.to_u32()
verifier_contact_id.to_u32()
}
// dc_lot_t
@@ -4580,11 +4577,12 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use super::*;
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use super::*;
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,

View File

@@ -1,10 +1,12 @@
//! # Legacy generic return values for C API.
use std::borrow::Cow;
use anyhow::Error;
use crate::message::MessageState;
use crate::qr::Qr;
use crate::summary::{Summary, SummaryPrefix};
use anyhow::Error;
use std::borrow::Cow;
/// An object containing a set of values.
/// The meaning of the values is defined by the function returning the object.

View File

@@ -287,9 +287,10 @@ fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
#[cfg(test)]
mod tests {
use super::*;
use libc::{free, strcmp};
use super::*;
#[test]
fn test_os_str_to_c_string_cwd() {
let some_dir = std::env::current_dir().unwrap();

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.105.0"
version = "1.106.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"

View File

@@ -1,4 +1,9 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::{
chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, marknoticed_chat,
@@ -23,21 +28,14 @@ use deltachat::{
webxdc::StatusUpdateSerial,
};
use sanitize_filename::is_sanitized;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{collections::HashMap, str::FromStr};
use tokio::{fs, sync::RwLock};
use walkdir::WalkDir;
use yerpc::rpc;
pub use deltachat::accounts::Accounts;
pub mod events;
pub mod types;
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::qr::QrObject;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::chat_list::ChatListEntry;
@@ -53,8 +51,8 @@ use self::types::{
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
},
};
use num_traits::FromPrimitive;
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::qr::QrObject;
#[derive(Clone, Debug)]
pub struct CommandApi {

View File

@@ -20,6 +20,10 @@ pub struct ContactObject {
name_and_addr: String,
is_blocked: bool,
is_verified: bool,
/// the address that verified this contact
verifier_addr: Option<String>,
/// the id of the contact that verified this contact
verifier_id: Option<u32>,
/// the contact's last seen timestamp
last_seen: i64,
was_seen_recently: bool,
@@ -36,6 +40,18 @@ impl ContactObject {
};
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
let (verifier_addr, verifier_id) = if is_verified {
(
contact.get_verifier_addr(context).await?,
contact
.get_verifier_id(context)
.await?
.map(|contact_id| contact_id.to_u32()),
)
} else {
(None, None)
};
Ok(ContactObject {
address: contact.get_addr().to_owned(),
color: color_int_to_hex_string(contact.get_color()),
@@ -48,6 +64,8 @@ impl ContactObject {
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_verified,
verifier_addr,
verifier_id,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),
})

View File

@@ -4,12 +4,13 @@ pub use yerpc;
#[cfg(test)]
mod tests {
use super::api::{Accounts, CommandApi};
use async_channel::unbounded;
use futures::StreamExt;
use tempfile::TempDir;
use yerpc::{RpcClient, RpcSession};
use super::api::{Accounts, CommandApi};
#[tokio::test(flavor = "multi_thread")]
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();

View File

@@ -1,6 +1,7 @@
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
use std::net::SocketAddr;
use std::path::PathBuf;
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
use yerpc::axum::handle_ws_rpc;
use yerpc::{RpcClient, RpcSession};

View File

@@ -48,5 +48,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.105.0"
"version": "1.106.0"
}

View File

@@ -27,9 +27,7 @@ async def log_error(event):
@hooks.on(events.MemberListChanged)
async def on_memberlist_changed(event):
logging.info(
"member %s was %s", event.member, "added" if event.member_added else "removed"
)
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
@hooks.on(events.GroupImageChanged)

View File

@@ -27,3 +27,13 @@ deltachat_rpc_client = [
[project.entry-points.pytest11]
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
[tool.black]
line-length = 120
[tool.ruff]
select = ["E", "F", "W", "N", "YTT", "B", "C4", "ISC", "ICN", "PT", "RET", "SIM", "TID", "ARG", "DTZ", "ERA", "PLC", "PLE", "PLW", "PIE", "COM"]
line-length = 120
[tool.isort]
profile = "black"

View File

@@ -8,3 +8,18 @@ from .contact import Contact
from .deltachat import DeltaChat
from .message import Message
from .rpc import Rpc
__all__ = [
"Account",
"AttrDict",
"Bot",
"Chat",
"Client",
"Contact",
"DeltaChat",
"EventType",
"Message",
"Rpc",
"run_bot_cli",
"run_client_cli",
]

View File

@@ -30,12 +30,7 @@ class AttrDict(dict):
"""Dictionary that allows accessing values usin the "dot notation" as attributes."""
def __init__(self, *args, **kwargs) -> None:
super().__init__(
{
_camel_to_snake(key): _to_attrdict(value)
for key, value in dict(*args, **kwargs).items()
}
)
super().__init__({_camel_to_snake(key): _to_attrdict(value) for key, value in dict(*args, **kwargs).items()})
def __getattr__(self, attr):
if attr in self:
@@ -51,7 +46,7 @@ class AttrDict(dict):
async def run_client_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs
**kwargs,
) -> None:
"""Run a simple command line app, using the given hooks.
@@ -65,7 +60,7 @@ async def run_client_cli(
async def run_bot_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs
**kwargs,
) -> None:
"""Run a simple bot command line using the given hooks.
@@ -80,7 +75,7 @@ async def _run_cli(
client_type: Type["Client"],
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs
**kwargs,
) -> None:
from .deltachat import DeltaChat
from .rpc import Rpc
@@ -107,12 +102,9 @@ async def _run_cli(
client = client_type(account, hooks)
client.logger.debug("Running deltachat core %s", core_version)
if not await client.is_configured():
assert (
args.email and args.password
), "Account is not configured and email and password must be provided"
asyncio.create_task(
client.configure(email=args.email, password=args.password)
)
assert args.email, "Account is not configured and email must be provided"
assert args.password, "Account is not configured and password must be provided"
asyncio.create_task(client.configure(email=args.email, password=args.password))
await client.run_forever()

View File

@@ -89,9 +89,7 @@ class Account:
"""Configure an account."""
await self._rpc.configure(self.id)
async def create_contact(
self, obj: Union[int, str, Contact], name: Optional[str] = None
) -> Contact:
async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
Calling this method will always result in the same
@@ -120,10 +118,7 @@ class Account:
async def get_blocked_contacts(self) -> List[AttrDict]:
"""Return a list with snapshots of all blocked contacts."""
contacts = await self._rpc.get_blocked_contacts(self.id)
return [
AttrDict(contact=Contact(self, contact["id"]), **contact)
for contact in contacts
]
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
async def get_contacts(
self,
@@ -148,10 +143,7 @@ class Account:
if snapshot:
contacts = await self._rpc.get_contacts(self.id, flags, query)
return [
AttrDict(contact=Contact(self, contact["id"]), **contact)
for contact in contacts
]
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
return [Contact(self, contact_id) for contact_id in contacts]
@@ -192,9 +184,7 @@ class Account:
if alldone_hint:
flags |= ChatlistFlag.ADD_ALLDONE_HINT
entries = await self._rpc.get_chatlist_entries(
self.id, flags, query, contact and contact.id
)
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
if not snapshot:
return [Chat(self, entry[0]) for entry in entries]

View File

@@ -63,7 +63,7 @@ class Chat:
"""
if duration is not None:
assert duration > 0, "Invalid duration"
dur: Union[str, dict] = dict(Until=duration)
dur: Union[str, dict] = {"Until": duration}
else:
dur = "Forever"
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
@@ -74,27 +74,19 @@ class Chat:
async def pin(self) -> None:
"""Pin this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.PINNED
)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
async def unpin(self) -> None:
"""Unpin this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.NORMAL
)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
async def archive(self) -> None:
"""Archive this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.ARCHIVED
)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
async def unarchive(self) -> None:
"""Unarchive this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.NORMAL
)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
async def set_name(self, name: str) -> None:
"""Set name of this chat."""
@@ -133,9 +125,7 @@ class Chat:
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
msg_id, _ = await self._rpc.misc_send_msg(
self.account.id, self.id, text, file, location, quoted_msg
)
msg_id, _ = await self._rpc.misc_send_msg(self.account.id, self.id, text, file, location, quoted_msg)
return Message(self.account, msg_id)
async def send_text(self, text: str) -> Message:
@@ -241,23 +231,17 @@ class Chat:
timestamp_to: Optional[datetime] = None,
) -> List[AttrDict]:
"""Get list of location snapshots for the given contact in the given timespan."""
time_from = (
calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
)
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
contact_id = contact.id if contact else 0
result = await self._rpc.get_locations(
self.account.id, self.id, contact_id, time_from, time_to
)
result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
locations = []
contacts: Dict[int, Contact] = {}
for loc in result:
loc = AttrDict(loc)
loc["chat"] = self
loc["contact"] = contacts.setdefault(
loc.contact_id, Contact(self.account, loc.contact_id)
)
loc["contact"] = contacts.setdefault(loc.contact_id, Contact(self.account, loc.contact_id))
loc["message"] = Message(self.account, loc.msg_id)
locations.append(loc)
return locations

View File

@@ -47,15 +47,11 @@ class Client:
self._should_process_messages = 0
self.add_hooks(hooks or [])
def add_hooks(
self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]
) -> None:
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
for hook, event in hooks:
self.add_hook(hook, event)
def add_hook(
self, hook: Callable, event: Union[type, EventFilter] = RawEvent
) -> None:
def add_hook(self, hook: Callable, event: Union[type, EventFilter] = RawEvent) -> None:
"""Register hook for the given event filter."""
if isinstance(event, type):
event = event()
@@ -64,7 +60,7 @@ class Client:
isinstance(
event,
(NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged),
)
),
)
self._hooks.setdefault(type(event), set()).add((hook, event))
@@ -76,7 +72,7 @@ class Client:
isinstance(
event,
(NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged),
)
),
)
self._hooks.get(type(event), set()).remove((hook, event))
@@ -95,9 +91,7 @@ class Client:
"""Process events forever."""
await self.run_until(lambda _: False)
async def run_until(
self, func: Callable[[AttrDict], Union[bool, Coroutine]]
) -> AttrDict:
async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
"""Process events until the given callable evaluates to True.
The callable should accept an AttrDict object representing the
@@ -122,9 +116,7 @@ class Client:
if stop:
return event
async def _on_event(
self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent
) -> None:
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
for hook, evfilter in self._hooks.get(filter_type, []):
if await evfilter.filter(event):
try:
@@ -133,11 +125,7 @@ class Client:
self.logger.exception(ex)
async def _parse_command(self, event: AttrDict) -> None:
cmds = [
hook[1].command
for hook in self._hooks.get(NewMessage, [])
if hook[1].command
]
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
parts = event.message_snapshot.text.split(maxsplit=1)
payload = parts[1] if len(parts) > 1 else ""
cmd = parts.pop(0)
@@ -202,11 +190,7 @@ class Client:
for message in await self.account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot()
await self._on_new_msg(snapshot)
if (
snapshot.is_info
and snapshot.system_message_type
!= SystemMessageType.WEBXDC_INFO_MESSAGE
):
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
await self._handle_info_msg(snapshot)
await snapshot.message.mark_seen()

View File

@@ -10,7 +10,7 @@ from .const import EventType
def _tuple_of(obj, type_: type) -> tuple:
if not obj:
return tuple()
return ()
if isinstance(obj, type_):
obj = (obj,)
@@ -39,7 +39,7 @@ class EventFilter(ABC):
"""Return True if two event filters are equal."""
def __ne__(self, other):
return not self.__eq__(other)
return not self == other
async def _call_func(self, event) -> bool:
if not self.func:
@@ -65,9 +65,7 @@ class RawEvent(EventFilter):
should be dispatched or not.
"""
def __init__(
self, types: Union[None, EventType, Iterable[EventType]] = None, **kwargs
):
def __init__(self, types: Union[None, EventType, Iterable[EventType]] = None, **kwargs):
super().__init__(**kwargs)
try:
self.types = _tuple_of(types, EventType)

View File

@@ -49,22 +49,14 @@ class Message:
"""Mark the message as seen."""
await self._rpc.markseen_msgs(self.account.id, [self.id])
async def send_webxdc_status_update(
self, update: Union[dict, str], description: str
) -> None:
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
"""Send a webxdc status update. This message must be a webxdc."""
if not isinstance(update, str):
update = json.dumps(update)
await self._rpc.send_webxdc_status_update(
self.account.id, self.id, update, description
)
await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
return json.loads(
await self._rpc.get_webxdc_status_updates(
self.account.id, self.id, last_known_serial
)
)
return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
async def get_webxdc_info(self) -> dict:
return await self._rpc.get_webxdc_info(self.account.id, self.id)

View File

@@ -67,9 +67,7 @@ class ACFactory:
) -> Message:
if not from_account:
from_account = (await self.get_online_accounts(1))[0]
to_contact = await from_account.create_contact(
await to_account.get_config("addr")
)
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
if group:
to_chat = await from_account.create_group(group)
await to_chat.add_contact(to_contact)

View File

@@ -30,7 +30,7 @@ class Rpc:
"deltachat-rpc-server",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
**self._kwargs
**self._kwargs,
)
self.id = 0
self.event_queues = {}
@@ -46,7 +46,7 @@ class Rpc:
await self.start()
return self
async def __aexit__(self, exc_type, exc, tb):
async def __aexit__(self, _exc_type, _exc, _tb):
await self.close()
async def reader_loop(self) -> None:
@@ -97,5 +97,6 @@ class Rpc:
raise JsonRpcError(response["error"])
if "result" in response:
return response["result"]
return None
return method

View File

@@ -6,14 +6,14 @@ from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError
@pytest.mark.asyncio
@pytest.mark.asyncio()
async def test_system_info(rpc) -> None:
system_info = await rpc.get_system_info()
assert "arch" in system_info
assert "deltachat_core_version" in system_info
@pytest.mark.asyncio
@pytest.mark.asyncio()
async def test_email_address_validity(rpc) -> None:
valid_addresses = [
"email@example.com",
@@ -27,7 +27,7 @@ async def test_email_address_validity(rpc) -> None:
assert not await rpc.check_email_validity(addr)
@pytest.mark.asyncio
@pytest.mark.asyncio()
async def test_acfactory(acfactory) -> None:
account = await acfactory.new_configured_account()
while True:
@@ -41,7 +41,7 @@ async def test_acfactory(acfactory) -> None:
print("Successful configuration")
@pytest.mark.asyncio
@pytest.mark.asyncio()
async def test_configure_starttls(acfactory) -> None:
account = await acfactory.new_preconfigured_account()
@@ -51,7 +51,7 @@ async def test_configure_starttls(acfactory) -> None:
assert await account.is_configured()
@pytest.mark.asyncio
@pytest.mark.asyncio()
async def test_account(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
@@ -111,7 +111,7 @@ async def test_account(acfactory) -> None:
await alice.stop_io()
@pytest.mark.asyncio
@pytest.mark.asyncio()
async def test_chat(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
@@ -177,7 +177,7 @@ async def test_chat(acfactory) -> None:
await group.get_locations()
@pytest.mark.asyncio
@pytest.mark.asyncio()
async def test_contact(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
@@ -195,7 +195,7 @@ async def test_contact(acfactory) -> None:
await alice_contact_bob.create_chat()
@pytest.mark.asyncio
@pytest.mark.asyncio()
async def test_message(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
@@ -226,7 +226,7 @@ async def test_message(acfactory) -> None:
await message.send_reaction("😎")
@pytest.mark.asyncio
@pytest.mark.asyncio()
async def test_bot(acfactory) -> None:
mock = MagicMock()
user = (await acfactory.get_online_accounts(1))[0]
@@ -237,25 +237,20 @@ async def test_bot(acfactory) -> None:
hook = lambda e: mock.hook(e.msg_id), events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook)
event = await acfactory.process_message(
from_account=user, to_client=bot, text="Hello!"
)
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
mock.hook.assert_called_once_with(event.msg_id)
bot.remove_hook(*hook)
track = lambda e: mock.hook(e.message_snapshot.id)
def track(e):
mock.hook(e.message_snapshot.id)
mock.hook.reset_mock()
hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help"))
event = await acfactory.process_message(
from_account=user, to_client=bot, text="hello"
)
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
mock.hook.assert_called_with(event.msg_id)
event = await acfactory.process_message(
from_account=user, to_client=bot, text="hello!"
)
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
mock.hook.assert_called_with(event.msg_id)
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
assert len(mock.hook.mock_calls) == 2
@@ -263,7 +258,5 @@ async def test_bot(acfactory) -> None:
mock.hook.reset_mock()
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = await acfactory.process_message(
from_account=user, to_client=bot, text="/help"
)
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
mock.hook.assert_called_once_with(event.msg_id)

View File

@@ -3,16 +3,14 @@ import pytest
from deltachat_rpc_client import EventType
@pytest.mark.asyncio
@pytest.mark.asyncio()
async def test_webxdc(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_message(
text="Let's play chess!", file="../test-data/webxdc/chess.xdc"
)
await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
while True:
event = await bob.wait_for_event()

View File

@@ -2,6 +2,7 @@
isolated_build = true
envlist =
py3
lint
[testenv]
commands =
@@ -16,3 +17,13 @@ deps =
pytest-asyncio
aiohttp
aiodns
[testenv:lint]
skipsdist = True
skip_install = True
deps =
ruff
black
commands =
black --check src/ examples/ tests/
ruff src/ examples/ tests/

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.105.0"
version = "1.106.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -1,9 +1,10 @@
#![recursion_limit = "128"]
extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
use crate::proc_macro::TokenStream;
// For now, assume (not check) that these macroses are applied to enum without
// data. If this assumption is violated, compiler error will point to
// generated code, which is not very user-friendly.

View File

@@ -1,5 +1,3 @@
use tempfile::tempdir;
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::*;
use deltachat::config;
@@ -8,6 +6,7 @@ use deltachat::context::*;
use deltachat::message::Message;
use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use tempfile::tempdir;
fn cb(event: EventType) {
match event {

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.105.0"
"version": "1.106.0"
}

View File

@@ -11,7 +11,8 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
import sys
import os
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the

View File

@@ -34,8 +34,10 @@ class GroupTrackingPlugin:
def ac_member_added(self, chat, contact, actor, message):
print(
"ac_member_added {} to chat {} from {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr
)
contact.addr,
chat.id,
actor or message.get_sender_contact().addr,
),
)
for member in chat.get_contacts():
print("chat member: {}".format(member.addr))
@@ -44,8 +46,10 @@ class GroupTrackingPlugin:
def ac_member_removed(self, chat, contact, actor, message):
print(
"ac_member_removed {} from chat {} by {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr
)
contact.addr,
chat.id,
actor or message.get_sender_contact().addr,
),
)

View File

@@ -13,8 +13,8 @@ def datadir():
datadir = path.join("test-data")
if datadir.isdir():
return datadir
else:
pytest.skip("test-data directory not found")
pytest.skip("test-data directory not found")
return None
def test_echo_quit_plugin(acfactory, lp):
@@ -47,7 +47,7 @@ def test_group_tracking_plugin(acfactory, lp):
botproc.fnmatch_lines(
"""
*ac_configure_completed*
"""
""",
)
ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2))
@@ -61,7 +61,7 @@ def test_group_tracking_plugin(acfactory, lp):
botproc.fnmatch_lines(
"""
*ac_chat_modified*bot test group*
"""
""",
)
lp.sec("adding third member {}".format(ac2.get_config("addr")))
@@ -76,8 +76,9 @@ def test_group_tracking_plugin(acfactory, lp):
"""
*ac_member_added {}*from*{}*
""".format(
contact3.addr, ac1.get_config("addr")
)
contact3.addr,
ac1.get_config("addr"),
),
)
lp.sec("contact successfully added, now removing")
@@ -86,6 +87,7 @@ def test_group_tracking_plugin(acfactory, lp):
"""
*ac_member_removed {}*from*{}*
""".format(
contact3.addr, ac1.get_config("addr")
)
contact3.addr,
ac1.get_config("addr"),
),
)

View File

@@ -44,5 +44,9 @@ git_describe_command = "git describe --dirty --tags --long --match py-*.*"
[tool.black]
line-length = 120
[tool.ruff]
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM"]
line-length = 120
[tool.isort]
profile = "black"
profile = "black"

View File

@@ -36,7 +36,8 @@ register_global_plugin(events)
def run_cmdline(argv=None, account_plugins=None):
"""Run a simple default command line app, registering the specified
account plugins."""
account plugins.
"""
import argparse
if argv is None:

View File

@@ -102,8 +102,8 @@ def find_header(flags):
printf("%s", _dc_header_file_location());
return 0;
}
"""
)
""",
),
)
cwd = os.getcwd()
try:
@@ -198,7 +198,7 @@ def ffibuilder():
typedef int... time_t;
void free(void *ptr);
extern int dc_event_has_string_data(int);
"""
""",
)
function_defs = extract_functions(flags)
defines = extract_defines(flags)

View File

@@ -1,4 +1,4 @@
""" Account class implementation. """
"""Account class implementation."""
from __future__ import print_function
@@ -39,7 +39,7 @@ def get_core_info():
ffi.gc(
lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
lib.dc_context_unref,
)
),
)
@@ -172,10 +172,7 @@ class Account(object):
namebytes = name.encode("utf8")
if isinstance(value, (int, bool)):
value = str(int(value))
if value is not None:
valuebytes = value.encode("utf8")
else:
valuebytes = ffi.NULL
valuebytes = value.encode("utf8") if value is not None else ffi.NULL
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
def get_config(self, name: str) -> str:
@@ -225,9 +222,10 @@ class Account(object):
return bool(lib.dc_is_configured(self._dc_context))
def is_open(self) -> bool:
"""Determine if account is open
"""Determine if account is open.
:returns True if account is open."""
:returns True if account is open.
"""
return bool(lib.dc_context_is_open(self._dc_context))
def set_avatar(self, img_path: Optional[str]) -> None:
@@ -543,7 +541,7 @@ class Account(object):
return from_dc_charpointer(res)
def check_qr(self, qr):
"""check qr code and return :class:`ScannedQRCode` instance representing the result"""
"""check qr code and return :class:`ScannedQRCode` instance representing the result."""
res = ffi.gc(lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), lib.dc_lot_unref)
lot = DCLot(res)
if lot.state() == const.DC_QR_ERROR:
@@ -662,7 +660,7 @@ class Account(object):
return lib.dc_all_work_done(self._dc_context)
def start_io(self):
"""start this account's IO scheduling (Rust-core async scheduler)
"""start this account's IO scheduling (Rust-core async scheduler).
If this account is not configured an Exception is raised.
You need to call account.configure() and account.wait_configure_finish()
@@ -705,12 +703,10 @@ class Account(object):
"""
lib.dc_maybe_network(self._dc_context)
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
def configure(self) -> 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.
:param reconfigure: deprecated, doesn't need to be checked anymore.
"""
if not self.get_config("addr") or not self.get_config("mail_pw"):
raise MissingCredentials("addr or mail_pwd not set in config")
@@ -733,7 +729,8 @@ class Account(object):
def shutdown(self) -> None:
"""shutdown and destroy account (stop callback thread, close and remove
underlying dc_context)."""
underlying dc_context).
"""
if self._dc_context is None:
return

View File

@@ -1,4 +1,4 @@
""" Chat and Location related API. """
"""Chat and Location related API."""
import calendar
import json
@@ -37,7 +37,7 @@ class Chat(object):
return self.id == getattr(other, "id", None) and self.account._dc_context == other.account._dc_context
def __ne__(self, other) -> bool:
return not (self == other)
return not self == other
def __repr__(self) -> str:
return "<Chat id={} name={}>".format(self.id, self.get_name())
@@ -74,19 +74,19 @@ class Chat(object):
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
def is_single(self) -> bool:
"""Return True if this chat is a single/direct chat, False otherwise"""
"""Return True if this chat is a single/direct chat, False otherwise."""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_SINGLE
def is_mailinglist(self) -> bool:
"""Return True if this chat is a mailing list, False otherwise"""
"""Return True if this chat is a mailing list, False otherwise."""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_MAILINGLIST
def is_broadcast(self) -> bool:
"""Return True if this chat is a broadcast list, False otherwise"""
"""Return True if this chat is a broadcast list, False otherwise."""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_BROADCAST
def is_multiuser(self) -> bool:
"""Return True if this chat is a multi-user chat (group, mailing list or broadcast), False otherwise"""
"""Return True if this chat is a multi-user chat (group, mailing list or broadcast), False otherwise."""
return lib.dc_chat_get_type(self._dc_chat) in (
const.DC_CHAT_TYPE_GROUP,
const.DC_CHAT_TYPE_MAILINGLIST,
@@ -94,11 +94,11 @@ class Chat(object):
)
def is_self_talk(self) -> bool:
"""Return True if this chat is the self-chat (a.k.a. "Saved Messages"), False otherwise"""
"""Return True if this chat is the self-chat (a.k.a. "Saved Messages"), False otherwise."""
return bool(lib.dc_chat_is_self_talk(self._dc_chat))
def is_device_talk(self) -> bool:
"""Returns True if this chat is the "Device Messages" chat, False otherwise"""
"""Returns True if this chat is the "Device Messages" chat, False otherwise."""
return bool(lib.dc_chat_is_device_talk(self._dc_chat))
def is_muted(self) -> bool:
@@ -109,12 +109,12 @@ class Chat(object):
return bool(lib.dc_chat_is_muted(self._dc_chat))
def is_pinned(self) -> bool:
"""Return True if this chat is pinned, False otherwise"""
"""Return True if this chat is pinned, False otherwise."""
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_PINNED
def is_archived(self) -> bool:
"""Return True if this chat is archived, False otherwise.
:returns: True if archived, False otherwise
:returns: True if archived, False otherwise.
"""
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
@@ -136,7 +136,7 @@ class Chat(object):
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
This is not true eg. for the contact requests or for the device-talk.
:returns: True if the chat is writable, False otherwise
"""
@@ -167,7 +167,7 @@ class Chat(object):
def get_color(self):
"""return the color of the chat.
:returns: color as 0x00rrggbb
:returns: color as 0x00rrggbb.
"""
return lib.dc_chat_get_color(self._dc_chat)
@@ -178,21 +178,18 @@ class Chat(object):
return json.loads(s)
def mute(self, duration: Optional[int] = None) -> None:
"""mutes the chat
"""mutes the chat.
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
:returns: None
"""
if duration is None:
mute_duration = -1
else:
mute_duration = duration
mute_duration = -1 if duration is None else duration
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration)
if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self) -> None:
"""unmutes the chat
"""unmutes the chat.
:returns: None
"""
@@ -252,7 +249,8 @@ class Chat(object):
def get_encryption_info(self) -> Optional[str]:
"""Return encryption info for this chat.
:returns: a string with encryption preferences of all chat members"""
: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)
@@ -463,7 +461,7 @@ class Chat(object):
def get_contacts(self):
"""get all contacts for this chat.
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
:returns: list of :class:`deltachat.contact.Contact` objects for this chat.
"""
from .contact import Contact
@@ -547,19 +545,10 @@ class Chat(object):
:param timespan_to: a datetime object or None (indicating up till now)
:returns: list of :class:`deltachat.chat.Location` objects.
"""
if timestamp_from is None:
time_from = 0
else:
time_from = calendar.timegm(timestamp_from.utctimetuple())
if timestamp_to is None:
time_to = 0
else:
time_to = calendar.timegm(timestamp_to.utctimetuple())
time_from = 0 if timestamp_from is None else calendar.timegm(timestamp_from.utctimetuple())
time_to = 0 if timestamp_to is None else calendar.timegm(timestamp_to.utctimetuple())
if contact is None:
contact_id = 0
else:
contact_id = contact.id
contact_id = 0 if contact is None else contact.id
dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to)
return [

View File

@@ -1,4 +1,4 @@
""" Contact object. """
"""Contact object."""
from datetime import date, datetime, timezone
from typing import Optional
@@ -28,7 +28,7 @@ class Contact(object):
return self.account._dc_context == other.account._dc_context and self.id == other.id
def __ne__(self, other):
return not (self == other)
return not self == other
def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context)
@@ -76,7 +76,7 @@ class Contact(object):
return lib.dc_contact_is_verified(self._dc_contact)
def get_verifier(self, contact):
"""Return the address of the contact that verified the contact"""
"""Return the address of the contact that verified the contact."""
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
def get_profile_image(self) -> Optional[str]:

View File

@@ -79,15 +79,17 @@ class DirectImap:
def select_config_folder(self, config_name: str):
"""Return info about selected folder if it is
configured, otherwise None."""
configured, otherwise None.
"""
if "_" not in config_name:
config_name = "configured_{}_folder".format(config_name)
foldername = self.account.get_config(config_name)
if foldername:
return self.select_folder(foldername)
return None
def list_folders(self) -> List[str]:
"""return list of all existing folder names"""
"""return list of all existing folder names."""
assert not self._idling
return [folder.name for folder in self.conn.folder.list()]
@@ -103,7 +105,7 @@ class DirectImap:
def get_all_messages(self) -> List[MailMessage]:
assert not self._idling
return [mail for mail in self.conn.fetch()]
return list(self.conn.fetch())
def get_unread_messages(self) -> List[str]:
assert not self._idling
@@ -221,5 +223,4 @@ class IdleManager:
def done(self):
"""send idle-done to server if we are currently in idle mode."""
res = self.direct_imap.conn.idle.stop()
return res
return self.direct_imap.conn.idle.stop()

View File

@@ -30,6 +30,12 @@ class FFIEvent:
self.data2 = data2
def __str__(self):
if self.name == "DC_EVENT_INFO":
return "INFO {data2}".format(data2=self.data2)
if self.name == "DC_EVENT_WARNING":
return "WARNING {data2}".format(data2=self.data2)
if self.name == "DC_EVENT_ERROR":
return "ERROR {data2}".format(data2=self.data2)
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
@@ -128,7 +134,8 @@ class FFIEventTracker:
def wait_for_connectivity(self, connectivity):
"""Wait for the specified connectivity.
This only works reliably if the connectivity doesn't change
again too quickly, otherwise we might miss it."""
again too quickly, otherwise we might miss it.
"""
while 1:
if self.account.get_connectivity() == connectivity:
return
@@ -136,12 +143,13 @@ class FFIEventTracker:
def wait_for_connectivity_change(self, previous, expected_next):
"""Wait until the connectivity changes to `expected_next`.
Fails the test if it changes to something else."""
Fails the test if it changes to something else.
"""
while 1:
current = self.account.get_connectivity()
if current == expected_next:
return
elif current != previous:
if current != previous:
raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current))
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -176,7 +184,8 @@ class FFIEventTracker:
- ac1 and ac2 are created
- ac1 sends a message to ac2
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
- therefore no DC_EVENT_INCOMING_MSG is sent"""
- therefore no DC_EVENT_INCOMING_MSG is sent
"""
self.get_info_contains("INBOX: Idle entering")
def wait_next_incoming_message(self):
@@ -186,14 +195,15 @@ class FFIEventTracker:
def wait_next_messages_changed(self):
"""wait for and return next message-changed message or None
if the event contains no msgid"""
if the event contains no msgid
"""
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_next_reactions_changed(self):
"""wait for and return next reactions-changed message"""
"""wait for and return next reactions-changed message."""
ev = self.get_matching("DC_EVENT_REACTIONS_CHANGED")
assert ev.data1 > 0
return self.account.get_message_by_id(ev.data2)
@@ -285,10 +295,10 @@ class EventThread(threading.Thread):
if data1 == 0 or data1 == 1000:
success = data1 == 1000
comment = ffi_event.data2
yield "ac_configure_completed", dict(success=success, comment=comment)
yield "ac_configure_completed", {"success": success, "comment": comment}
elif name == "DC_EVENT_INCOMING_MSG":
msg = account.get_message_by_id(ffi_event.data2)
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
elif name == "DC_EVENT_MSGS_CHANGED":
if ffi_event.data2 != 0:
msg = account.get_message_by_id(ffi_event.data2)
@@ -296,19 +306,19 @@ class EventThread(threading.Thread):
res = map_system_message(msg)
if res and res[0].startswith("ac_member"):
yield res
yield "ac_outgoing_message", dict(message=msg)
yield "ac_outgoing_message", {"message": msg}
elif msg.is_in_fresh():
yield map_system_message(msg) or (
"ac_incoming_message",
dict(message=msg),
{"message": msg},
)
elif name == "DC_EVENT_REACTIONS_CHANGED":
assert ffi_event.data1 > 0
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_reactions_changed", dict(message=msg)
yield "ac_reactions_changed", {"message": msg}
elif name == "DC_EVENT_MSG_DELIVERED":
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", dict(message=msg)
yield "ac_message_delivered", {"message": msg}
elif name == "DC_EVENT_CHAT_MODIFIED":
chat = account.get_chat_by_id(ffi_event.data1)
yield "ac_chat_modified", dict(chat=chat)
yield "ac_chat_modified", {"chat": chat}

View File

@@ -1,4 +1,4 @@
""" Hooks for Python bindings to Delta Chat Core Rust CFFI"""
"""Hooks for Python bindings to Delta Chat Core Rust CFFI."""
import pluggy

View File

@@ -1,4 +1,4 @@
""" The Message object. """
"""The Message object."""
import json
import os
@@ -59,10 +59,7 @@ class Message(object):
:param view_type: the message type code or one of the strings:
"text", "audio", "video", "file", "sticker", "videochat", "webxdc"
"""
if isinstance(view_type, int):
view_type_code = view_type
else:
view_type_code = get_viewtype_code_from_name(view_type)
view_type_code = view_type if isinstance(view_type, int) else get_viewtype_code_from_name(view_type)
return Message(
account,
ffi.gc(lib.dc_msg_new(account._dc_context, view_type_code), lib.dc_msg_unref),
@@ -129,7 +126,7 @@ class Message(object):
@props.with_doc
def filemime(self) -> str:
"""mime type of the file (if it exists)"""
"""mime type of the file (if it exists)."""
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
def get_status_updates(self, serial: int = 0) -> list:
@@ -141,7 +138,7 @@ class Message(object):
:param serial: The last known serial. Pass 0 if there are no known serials to receive all updates.
"""
return json.loads(
from_dc_charpointer(lib.dc_get_webxdc_status_updates(self.account._dc_context, self.id, serial))
from_dc_charpointer(lib.dc_get_webxdc_status_updates(self.account._dc_context, self.id, serial)),
)
def send_status_update(self, json_data: Union[str, dict], description: str) -> bool:
@@ -158,8 +155,11 @@ class Message(object):
json_data = json.dumps(json_data, default=str)
return bool(
lib.dc_send_webxdc_status_update(
self.account._dc_context, self.id, as_dc_charpointer(json_data), as_dc_charpointer(description)
)
self.account._dc_context,
self.id,
as_dc_charpointer(json_data),
as_dc_charpointer(description),
),
)
def send_reaction(self, reaction: str):
@@ -232,16 +232,18 @@ class Message(object):
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
if ts:
return datetime.fromtimestamp(ts, timezone.utc)
return None
@props.with_doc
def ephemeral_timer(self):
"""Ephemeral timer in seconds
"""Ephemeral timer in seconds.
:returns: timer in seconds or None if there is no timer
"""
timer = lib.dc_msg_get_ephemeral_timer(self._dc_msg)
if timer:
return timer
return None
@props.with_doc
def ephemeral_timestamp(self):
@@ -255,23 +257,25 @@ class Message(object):
@property
def quoted_text(self) -> Optional[str]:
"""Text inside the quote
"""Text inside the quote.
:returns: Quoted text"""
:returns: Quoted text
"""
return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
@property
def quote(self):
"""Quote getter
"""Quote getter.
:returns: Quoted message, if found in the database"""
:returns: Quoted message, if found in the database
"""
msg = lib.dc_msg_get_quoted_msg(self._dc_msg)
if msg:
return Message(self.account, ffi.gc(msg, lib.dc_msg_unref))
@quote.setter
def quote(self, quoted_message):
"""Quote setter"""
"""Quote setter."""
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
def force_plaintext(self) -> None:
@@ -286,7 +290,7 @@ class Message(object):
:returns: email-mime message object (with headers only, no body).
"""
import email.parser
import email
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
if mime_headers:
@@ -297,7 +301,7 @@ class Message(object):
@property
def error(self) -> Optional[str]:
"""Error message"""
"""Error message."""
return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
@property
@@ -493,7 +497,8 @@ def get_viewtype_code_from_name(view_type_name):
if code is not None:
return code
raise ValueError(
"message typecode not found for {!r}, " "available {!r}".format(view_type_name, list(_view_type_mapping.keys()))
"message typecode not found for {!r}, "
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())),
)
@@ -506,14 +511,11 @@ def map_system_message(msg):
if msg.is_system_message():
res = parse_system_add_remove(msg.text)
if not res:
return
return None
action, affected, actor = res
affected = msg.account.get_contact_by_addr(affected)
if actor == "me":
actor = None
else:
actor = msg.account.get_contact_by_addr(actor)
d = dict(chat=msg.chat, contact=affected, actor=actor, message=msg)
actor = None if actor == "me" else msg.account.get_contact_by_addr(actor)
d = {"chat": msg.chat, "contact": affected, "actor": actor, "message": msg}
return "ac_member_" + res[0], d
@@ -528,8 +530,8 @@ def extract_addr(text):
def parse_system_add_remove(text):
"""return add/remove info from parsing the given system message text.
returns a (action, affected, actor) triple"""
returns a (action, affected, actor) triple
"""
# You removed member a@b.
# You added member a@b.
# Member Me (x@y) removed by a@b.

View File

@@ -8,7 +8,7 @@ def with_doc(f):
# copied over unmodified from
# https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py
def cached(f):
"""returns a cached property that is calculated by function f"""
"""returns a cached property that is calculated by function f."""
def get(self):
try:
@@ -17,8 +17,9 @@ def cached(f):
self._property_cache = {}
except KeyError:
pass
x = self._property_cache[f] = f(self)
return x
res = f(self)
self._property_cache[f] = res
return res
def set(self, val):
propcache = self.__dict__.setdefault("_property_cache", {})

View File

@@ -9,7 +9,8 @@ class ProviderNotFoundError(Exception):
class Provider(object):
"""Provider information.
"""
Provider information.
:param domain: The email to get the provider info for.
"""

View File

@@ -1,4 +1,4 @@
""" The Reactions object. """
"""The Reactions object."""
from .capi import ffi, lib
from .cutil import from_dc_charpointer, iter_array

View File

@@ -29,7 +29,7 @@ def pytest_addoption(parser):
"--liveconfig",
action="store",
default=None,
help="a file with >=2 lines where each line " "contains NAME=VALUE config settings for one account",
help="a file with >=2 lines where each line contains NAME=VALUE config settings for one account",
)
group.addoption(
"--ignored",
@@ -124,7 +124,7 @@ def pytest_report_header(config, startdir):
info["deltachat_core_version"],
info["sqlite_version"],
info["journal_mode"],
)
),
]
cfg = config.option.liveconfig
@@ -180,7 +180,7 @@ class TestProcess:
if res.status_code != 200:
pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
config = {"addr": d["email"], "mail_pw": d["password"]}
print("newtmpuser {}: addr={}".format(index, config["addr"]))
self._configlist.append(config)
yield config
@@ -229,7 +229,7 @@ def write_dict_to_dir(dic, target_dir):
path.write_bytes(content)
@pytest.fixture
@pytest.fixture()
def data(request):
class Data:
def __init__(self) -> None:
@@ -253,6 +253,7 @@ def data(request):
if os.path.exists(fn):
return fn
print("WARNING: path does not exist: {!r}".format(fn))
return None
def read_path(self, bn, mode="r"):
fn = self.get_path(bn)
@@ -264,8 +265,11 @@ def data(request):
class ACSetup:
"""accounts setup helper to deal with multiple configure-process
and io & imap initialization phases. From tests, use the higher level
"""
Accounts setup helper to deal with multiple configure-process
and io & imap initialization phases.
From tests, use the higher level
public ACFactory methods instead of its private helper class.
"""
@@ -289,7 +293,7 @@ class ACSetup:
self._account2state[account] = self.CONFIGURED
self.log("added already configured account", account, account.get_config("addr"))
def start_configure(self, account, reconfigure=False):
def start_configure(self, account):
"""add an account and start its configure process."""
class PendingTracker:
@@ -299,7 +303,7 @@ class ACSetup:
account.add_account_plugin(PendingTracker(), name="pending_tracker")
self._account2state[account] = self.CONFIGURING
account.configure(reconfigure=reconfigure)
account.configure()
self.log("started configure on", account)
def wait_one_configured(self, account):
@@ -411,7 +415,8 @@ class ACFactory:
acc.disable_logging()
def get_next_liveconfig(self):
"""Base function to get functional online configurations
"""
Base function to get functional online configurations
where we can make valid SMTP and IMAP connections with.
"""
configdict = next(self._liveconfig_producer).copy()
@@ -465,8 +470,7 @@ class ACFactory:
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True
else:
print("WARN: could not use preconfigured keys for {!r}".format(addr))
print("WARN: could not use preconfigured keys for {!r}".format(addr))
def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Account:
# do a pseudo-configured account
@@ -476,14 +480,14 @@ class ACFactory:
acname = ac._logid
addr = "{}@offline.org".format(acname)
ac.update_config(
dict(
addr=addr,
displayname=acname,
mail_pw="123",
configured_addr=addr,
configured_mail_pw="123",
configured="1",
)
{
"addr": addr,
"displayname": acname,
"mail_pw": "123",
"configured_addr": addr,
"configured_mail_pw": "123",
"configured": "1",
},
)
self._preconfigure_key(ac, addr)
self._acsetup.init_logging(ac)
@@ -494,12 +498,12 @@ class ACFactory:
configdict = self.get_next_liveconfig()
else:
# XXX we might want to transfer the key to the new account
configdict = dict(
addr=cloned_from.get_config("addr"),
mail_pw=cloned_from.get_config("mail_pw"),
imap_certificate_checks=cloned_from.get_config("imap_certificate_checks"),
smtp_certificate_checks=cloned_from.get_config("smtp_certificate_checks"),
)
configdict = {
"addr": cloned_from.get_config("addr"),
"mail_pw": cloned_from.get_config("mail_pw"),
"imap_certificate_checks": cloned_from.get_config("imap_certificate_checks"),
"smtp_certificate_checks": cloned_from.get_config("smtp_certificate_checks"),
}
configdict.update(kwargs)
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
if ac is not None:
@@ -600,7 +604,7 @@ class ACFactory:
acc._evtracker.wait_next_incoming_message()
@pytest.fixture
@pytest.fixture()
def acfactory(request, tmpdir, testprocess, data):
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
yield am
@@ -665,12 +669,12 @@ class BotProcess:
ignored.append(line)
@pytest.fixture
@pytest.fixture()
def tmp_db_path(tmpdir):
return tmpdir.join("test.db").strpath
@pytest.fixture
@pytest.fixture()
def lp():
class Printer:
def sec(self, msg: str) -> None:

View File

@@ -77,11 +77,11 @@ class ConfigureTracker:
self.account.remove_account_plugin(self)
def wait_smtp_connected(self):
"""wait until smtp is configured."""
"""Wait until SMTP is configured."""
self._smtp_finished.wait()
def wait_imap_connected(self):
"""wait until smtp is configured."""
"""Wait until IMAP is configured."""
self._imap_finished.wait()
def wait_progress(self, data1=None):
@@ -91,7 +91,8 @@ class ConfigureTracker:
break
def wait_finish(self, timeout=None):
"""wait until configure is completed.
"""
Wait until configure is completed.
Raise Exception if Configure failed
"""

View File

@@ -15,5 +15,5 @@ if __name__ == "__main__":
p,
"-w",
workspacedir,
]
],
)

View File

@@ -216,7 +216,7 @@ def test_fetch_existing(acfactory, lp, mvbox_move):
# would also find the "Sent" folder, but it would be too late:
# The sentbox thread, started by `start_io()`, would have seen that there is no
# ConfiguredSentboxFolder and do nothing.
acfactory._acsetup.start_configure(ac1, reconfigure=True)
acfactory._acsetup.start_configure(ac1)
acfactory.bring_accounts_online()
assert_folders_configured(ac1)
@@ -492,3 +492,48 @@ def test_multidevice_sync_seen(acfactory, lp):
assert ac1_clone_message.is_in_seen
# Test that the timer is started on the second device after synchronizing the seen status.
assert "Expires: " in ac1_clone_message.get_message_info()
def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
"""The test for the bug #3836:
- Alice has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
- Bob joins the group and sends a message there. Alice sees it.
- Alice's second devices goes online, but doesn't see Bob in the group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac2_addr = ac2.get_config("addr")
ac1_offl = acfactory.new_online_configuring_account(cloned_from=ac1)
for ac in [ac1, ac1_offl]:
ac.set_config("bcc_self", "1")
acfactory.bring_accounts_online()
dir = tmpdir.mkdir("exportdir")
ac1.export_self_keys(dir.strpath)
ac1_offl.import_self_keys(dir.strpath)
ac1_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("ac2: sending message")
# Message can be sent only after a receipt of "vg-member-added" message. Just wait for
# "Member Me (<addr>) added by <addr>." message.
ac2._evtracker.wait_next_incoming_message()
msg_out = chat2.send_text("hello")
lp.sec("ac1: receiving message")
msg_in = ac1._evtracker.wait_next_incoming_message()
assert msg_in.text == msg_out.text
assert msg_in.get_sender_contact().addr == ac2_addr
lp.sec("ac1_offl: going online, receiving message")
ac1_offl.start_io()
ac1_offl._evtracker.wait_securejoin_inviter_progress(1000)
msg_in = ac1_offl._evtracker.wait_next_incoming_message()
assert msg_in.text == msg_out.text
assert msg_in.get_sender_contact().addr == ac2_addr

View File

@@ -32,7 +32,7 @@ def test_basic_imap_api(acfactory, tmpdir):
imap2.shutdown()
@pytest.mark.ignored
@pytest.mark.ignored()
def test_configure_generate_key(acfactory, lp):
# A slow test which will generate new keys.
acfactory.remove_preconfigured_keys()
@@ -88,7 +88,7 @@ def test_export_import_self_keys(acfactory, tmpdir, lp):
lp.indent(dir.strpath + os.sep + name)
lp.sec("importing into existing account")
ac2.import_self_keys(dir.strpath)
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*", check_error=False)
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*")
assert key_id2 == key_id
@@ -510,7 +510,7 @@ def test_send_and_receive_message_markseen(acfactory, lp):
idle2.wait_for_seen()
lp.step("1")
for i in range(2):
for _i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
@@ -529,7 +529,7 @@ def test_send_and_receive_message_markseen(acfactory, lp):
pass # mark_seen_messages() has generated events before it returns
def test_moved_markseen(acfactory, lp):
def test_moved_markseen(acfactory):
"""Test that message already moved to DeltaChat folder is marked as seen."""
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
@@ -553,7 +553,7 @@ def test_moved_markseen(acfactory, lp):
ac2.mark_seen_messages([msg])
uid = idle2.wait_for_seen()
assert len([a for a in ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*")))]) == 1
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*"))))) == 1
def test_message_override_sender_name(acfactory, lp):
@@ -832,7 +832,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
lp.sec("sending multi-line non-unicode message from ac1 to ac2")
text1 = (
"hello\nworld\nthis is a very long message that should be"
+ " wrapped using format=flowed and unwrapped on the receiver"
" wrapped using format=flowed and unwrapped on the receiver"
)
msg_out = chat.send_text(text1)
assert not msg_out.is_encrypted()
@@ -894,7 +894,7 @@ def test_dont_show_emails(acfactory, lp):
message in Drafts that is moved to Sent later
""".format(
ac1.get_config("configured_addr")
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
@@ -908,7 +908,7 @@ def test_dont_show_emails(acfactory, lp):
message in Sent
""".format(
ac1.get_config("configured_addr")
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
@@ -922,7 +922,7 @@ def test_dont_show_emails(acfactory, lp):
Unknown message in Spam
""".format(
ac1.get_config("configured_addr")
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
@@ -936,7 +936,21 @@ def test_dont_show_emails(acfactory, lp):
Unknown & malformed message in Spam
""".format(
ac1.get_config("configured_addr")
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
"Spam",
"""
From: delta<address: inbox@nhroy.com>
Subject: subj
To: {}
Message-ID: <spam.message99@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown & malformed message in Spam
""".format(
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
@@ -950,7 +964,7 @@ def test_dont_show_emails(acfactory, lp):
Actually interesting message in Spam
""".format(
ac1.get_config("configured_addr")
ac1.get_config("configured_addr"),
),
)
ac1.direct_imap.append(
@@ -964,7 +978,7 @@ def test_dont_show_emails(acfactory, lp):
Unknown message in Junk
""".format(
ac1.get_config("configured_addr")
ac1.get_config("configured_addr"),
),
)
@@ -1696,7 +1710,7 @@ def test_system_group_msg_from_blocked_user(acfactory, lp):
assert contact.is_blocked()
chat_on_ac2.remove_contact(ac1)
ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
assert not ac1.get_self_contact() in chat_on_ac1.get_contacts()
assert ac1.get_self_contact() not in chat_on_ac1.get_contacts()
def test_set_get_group_image(acfactory, data, lp):
@@ -1770,7 +1784,7 @@ def test_connectivity(acfactory, lp):
lp.sec(
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
+ "all messages are fetched"
"all messages are fetched",
)
ac1.direct_imap.select_config_folder("inbox")
@@ -2132,7 +2146,7 @@ def test_group_quote(acfactory, lp):
@pytest.mark.parametrize(
"folder,move,expected_destination,",
("folder", "move", "expected_destination"),
[
(
"xyz",
@@ -2247,11 +2261,44 @@ def test_aeap_flow_verified(acfactory, lp):
assert ac1new.get_config("addr") in [contact.addr for contact in msg_in_2.chat.get_contacts()]
def test_archived_muted_chat(acfactory, lp):
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: send message to ac2")
chat.send_text("message0")
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message0"
msg2.mark_seen()
chat2 = msg2.chat
chat2.archive()
chat2.mute()
lp.sec("ac1: send another message to ac2")
chat.send_text("message1")
lp.sec("wait for ac2 to receive DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK")
while 1:
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data1 == const.DC_CHAT_ID_ARCHIVED_LINK:
assert ev.data2 == 0
archive = ac2.get_chat_by_id(const.DC_CHAT_ID_ARCHIVED_LINK)
assert archive.count_fresh_messages() == 1
assert chat2.count_fresh_messages() == 1
break
class TestOnlineConfigureFails:
def test_invalid_password(self, acfactory):
configdict = acfactory.get_next_liveconfig()
ac1 = acfactory.get_unconfigured_account()
ac1.update_config(dict(addr=configdict["addr"], mail_pw="123"))
ac1.update_config({"addr": configdict["addr"], "mail_pw": "123"})
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)

View File

@@ -15,7 +15,7 @@ from deltachat.tracker import ImexFailed
@pytest.mark.parametrize(
"msgtext,res",
("msgtext", "res"),
[
(
"Member Me (tmp1@x.org) removed by tmp2@x.org.",
@@ -108,7 +108,7 @@ class TestOfflineAccountBasic:
def test_update_config(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
ac1.update_config(dict(mvbox_move=False))
ac1.update_config({"mvbox_move": False})
assert ac1.get_config("mvbox_move") == "0"
def test_has_savemime(self, acfactory):
@@ -229,11 +229,11 @@ class TestOfflineContact:
class TestOfflineChat:
@pytest.fixture
@pytest.fixture()
def ac1(self, acfactory):
return acfactory.get_pseudo_configured_account()
@pytest.fixture
@pytest.fixture()
def chat1(self, ac1):
return ac1.create_contact("some1@example.org", name="some1").create_chat()
@@ -257,7 +257,7 @@ class TestOfflineChat:
assert chat2.id == chat1.id
assert chat2.get_name() == chat1.get_name()
assert chat1 == chat2
assert not (chat1 != chat2)
assert not chat1.__ne__(chat2)
assert chat1 != chat3
for ichat in ac1.get_chats():
@@ -450,7 +450,7 @@ class TestOfflineChat:
assert msg.filemime == "image/png"
@pytest.mark.parametrize(
"fn,typein,typeout",
("fn", "typein", "typeout"),
[
("r", None, "application/octet-stream"),
("r.txt", None, "text/plain"),
@@ -458,7 +458,7 @@ class TestOfflineChat:
("r.txt", "image/png", "image/png"),
],
)
def test_message_file(self, ac1, chat1, data, lp, fn, typein, typeout):
def test_message_file(self, chat1, data, lp, fn, typein, typeout):
lp.sec("sending file")
fp = data.get_path(fn)
msg = chat1.send_file(fp, typein)
@@ -694,7 +694,7 @@ class TestOfflineChat:
chat1.set_draft(None)
assert chat1.get_draft() is None
def test_qr_setup_contact(self, acfactory, lp):
def test_qr_setup_contact(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
qr = ac1.get_setup_contact_qr()

View File

@@ -93,7 +93,7 @@ def test_empty_context():
capi.lib.dc_context_unref(ctx)
def test_dc_close_events(tmpdir, acfactory):
def test_dc_close_events(acfactory):
ac1 = acfactory.get_unconfigured_account()
# register after_shutdown function

View File

@@ -50,18 +50,14 @@ commands =
skipsdist = True
skip_install = True
deps =
flake8
# isort 5.11.0 is broken: https://github.com/PyCQA/isort/issues/2031
isort<5.11.0
ruff
black
# pygments required by rst-lint
pygments
restructuredtext_lint
commands =
isort --check setup.py install_python_bindings.py src/deltachat examples/ tests/
black --check setup.py install_python_bindings.py src/deltachat examples/ tests/
flake8 src/deltachat
flake8 tests/ examples/
ruff src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst
[testenv:mypy]
@@ -102,7 +98,3 @@ timeout = 150
timeout_func_only = True
markers =
ignored: ignore this test in default test runs, use --ignored to run.
[flake8]
max-line-length = 120
ignore = E203, E266, E501, W503

View File

@@ -2,12 +2,13 @@
Remove old "dc" indices except for master which always stays.
"""
from requests import Session
import datetime
import sys
import subprocess
import sys
MAXDAYS=7
from requests import Session
MAXDAYS = 7
session = Session()
session.headers["Accept"] = "application/json"
@@ -54,7 +55,8 @@ def run():
if not dates:
print(
"%s has no releases" % (baseurl + username + "/" + indexname),
file=sys.stderr)
file=sys.stderr,
)
date = datetime.datetime.now()
else:
date = datetime.datetime(*max(dates))
@@ -67,6 +69,5 @@ def run():
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
if __name__ == '__main__':
if __name__ == "__main__":
run()

View File

@@ -104,9 +104,6 @@ jobs:
outputs:
- name: py-docs
path: ./python/doc/_build/
# Source packages
- name: py-dist
path: ./python/.docker-tox/dist/
# Binary wheels
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
@@ -145,7 +142,6 @@ jobs:
config:
inputs:
- name: py-wheels
- name: py-dist
image_resource:
type: registry-image
source:
@@ -162,7 +158,6 @@ jobs:
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*manylinux201*
devpi upload py-dist/*
- name: python-aarch64
plan:

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env python3
import os
import json
import re
import os
import pathlib
import re
import subprocess
from argparse import ArgumentParser
@@ -23,7 +23,7 @@ def read_toml_version(relpath):
res = regex_matches(relpath, rex)
if res is not None:
return res.group(1)
raise ValueError("no version found in {}".format(relpath))
raise ValueError(f"no version found in {relpath}")
def replace_toml_version(relpath, newversion):
@@ -34,8 +34,8 @@ def replace_toml_version(relpath, newversion):
for line in open(str(p)):
m = rex.match(line)
if m is not None:
print("{}: set version={}".format(relpath, newversion))
f.write('version = "{}"\n'.format(newversion))
print(f"{relpath}: set version={newversion}")
f.write(f'version = "{newversion}"\n')
else:
f.write(line)
os.rename(tmp_path, str(p))
@@ -44,7 +44,7 @@ def replace_toml_version(relpath, newversion):
def read_json_version(relpath):
p = pathlib.Path(relpath)
assert p.exists()
with open(p, "r") as f:
with open(p) as f:
json_data = json.loads(f.read())
return json_data["version"]
@@ -52,7 +52,7 @@ def read_json_version(relpath):
def update_package_json(relpath, newversion):
p = pathlib.Path(relpath)
assert p.exists()
with open(p, "r") as f:
with open(p) as f:
json_data = json.loads(f.read())
json_data["version"] = newversion
with open(p, "w") as f:
@@ -63,7 +63,7 @@ def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
toml_list = [
"Cargo.toml",
"deltachat-ffi/Cargo.toml",
@@ -75,9 +75,9 @@ def main():
except SystemExit:
print()
for x in toml_list:
print("{}: {}".format(x, read_toml_version(x)))
print(f"{x}: {read_toml_version(x)}")
for x in json_list:
print("{}: {}".format(x, read_json_version(x)))
print(f"{x}: {read_json_version(x)}")
print()
raise SystemExit("need argument: new version, example: 1.25.0")
@@ -92,19 +92,19 @@ def main():
if "alpha" not in newversion:
for line in open("CHANGELOG.md"):
## 1.25.0
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break
if line.startswith("## ") and line[2:].strip().startswith(newversion):
break
else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
raise SystemExit(
f"CHANGELOG.md contains no entry for version: {newversion}"
)
for toml_filename in toml_list:
replace_toml_version(toml_filename, newversion)
for json_filename in json_list:
update_package_json(json_filename, newversion)
print("running cargo check")
subprocess.call(["cargo", "check"])
@@ -114,13 +114,12 @@ def main():
print("after commit, on master make sure to: ")
print("")
print(" git tag -a {}".format(newversion))
print(" git push origin {}".format(newversion))
print(" git tag -a py-{}".format(newversion))
print(" git push origin py-{}".format(newversion))
print(f" git tag -a {newversion}")
print(f" git push origin {newversion}")
print(f" git tag -a py-{newversion}")
print(f" git push origin py-{newversion}")
print("")
if __name__ == "__main__":
main()

View File

@@ -367,13 +367,20 @@ impl Config {
// Previous versions of the core stored absolute paths in account config.
// Convert them to relative paths.
let mut modified = false;
for account in &mut inner.accounts {
if let Ok(new_dir) = account.dir.strip_prefix(dir) {
account.dir = new_dir.to_path_buf();
modified = true;
}
}
Ok(Config { file, inner })
let config = Self { file, inner };
if modified {
config.sync().await?;
}
Ok(config)
}
/// Loads all accounts defined in the configuration file.
@@ -502,7 +509,6 @@ impl AccountConfig {
#[cfg(test)]
mod tests {
use super::*;
use crate::stock_str::{self, StockMessage};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -2,13 +2,12 @@
//!
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
use anyhow::{bail, Context as _, Error, Result};
use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use std::{fmt, str};
use crate::contact::addr_cmp;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use anyhow::{bail, Context as _, Error, Result};
use crate::key::{DcKey, SignedPublicKey};
/// Possible values for encryption preference
@@ -36,7 +35,7 @@ impl fmt::Display for EncryptPreference {
}
}
impl str::FromStr for EncryptPreference {
impl FromStr for EncryptPreference {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
@@ -69,29 +68,6 @@ impl Aheader {
prefer_encrypt,
}
}
/// Tries to parse Autocrypt header.
///
/// If there is none, returns None. If the header is present but cannot be parsed, returns an
/// error.
pub fn from_headers(
wanted_from: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Self>> {
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
let header = Self::from_str(&value)?;
if !addr_cmp(&header.addr, wanted_from) {
bail!(
"Autocrypt header address {:?} is not {:?}",
header.addr,
wanted_from
);
}
Ok(Some(header))
} else {
Ok(None)
}
}
}
impl fmt::Display for Aheader {
@@ -118,7 +94,7 @@ impl fmt::Display for Aheader {
}
}
impl str::FromStr for Aheader {
impl FromStr for Aheader {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {

View File

@@ -355,7 +355,6 @@ mod tests {
use tokio::io::AsyncReadExt;
use super::*;
use crate::aheader::EncryptPreference;
use crate::e2ee;
use crate::message;

View File

@@ -499,17 +499,15 @@ fn encoded_img_exceeds_bytes(
#[cfg(test)]
mod tests {
use fs::File;
use anyhow::Result;
use fs::File;
use image::{GenericImageView, Pixel};
use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
use crate::message::Message;
use crate::test_utils::{self, TestContext};
use super::*;
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
tokio::task::block_in_place(move || {
let img = image::open(path).expect("failed to open image");

View File

@@ -531,20 +531,68 @@ impl ChatId {
Ok(())
}
// Unarchives a chat that is archived and not muted.
// Needed when a message is added to a chat so that the chat gets a normal visibility again.
// Sending an appropriate event is up to the caller.
pub async fn unarchive_if_not_muted(self, context: &Context) -> Result<()> {
/// Unarchives a chat that is archived and not muted.
/// Needed after a message is added to a chat so that the chat gets a normal visibility again.
/// `msg_state` is the state of the message. Matters only for incoming messages currently. For
/// multiple outgoing messages the function may be called once with MessageState::Undefined.
/// Sending an appropriate event is up to the caller.
/// Also emits DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived
/// chats with unread messages increases (which is possible if the chat is muted).
pub async fn unarchive_if_not_muted(
self,
context: &Context,
msg_state: MessageState,
) -> Result<()> {
if msg_state != MessageState::InFresh {
context
.sql
.execute(
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
AND NOT(muted_until=-1 OR muted_until>?)",
paramsv![self, time()],
)
.await?;
return Ok(());
}
let chat = Chat::load_from_db(context, self).await?;
if chat.visibility != ChatVisibility::Archived {
return Ok(());
}
if chat.is_muted() {
let unread_cnt = context
.sql
.count(
"SELECT COUNT(*)
FROM msgs
WHERE state=?
AND hidden=0
AND chat_id=?",
paramsv![MessageState::InFresh, self],
)
.await?;
if unread_cnt == 1 {
// Added the first unread message in the chat.
context.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
}
return Ok(());
}
context
.sql
.execute(
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 AND NOT(muted_until=-1 OR muted_until>?)",
paramsv![self, time()],
)
.execute("UPDATE chats SET archived=0 WHERE id=?", paramsv![self])
.await?;
Ok(())
}
/// Emits an appropriate event for a message. `important` is whether a notification should be
/// shown.
pub(crate) fn emit_msg_event(self, context: &Context, msg_id: MsgId, important: bool) {
if important {
context.emit_incoming_msg(self, msg_id);
} else {
context.emit_msgs_changed(self, msg_id);
}
}
/// Deletes a chat.
pub async fn delete(self, context: &Context) -> Result<()> {
ensure!(
@@ -1315,6 +1363,8 @@ impl Chat {
self.param.get_bool(Param::Unpromoted).unwrap_or_default()
}
/// Returns whether the chat is promoted which means that a message has been
/// send to it and it not only exists on the users device.
pub fn is_promoted(&self) -> bool {
!self.is_unpromoted()
}
@@ -2009,7 +2059,9 @@ async fn prepare_msg_common(
msg.state = change_state_to;
prepare_msg_blob(context, msg).await?;
chat_id.unarchive_if_not_muted(context).await?;
if !msg.hidden {
chat_id.unarchive_if_not_muted(context, msg.state).await?;
}
msg.id = chat
.prepare_msg_raw(
context,
@@ -2805,6 +2857,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
}
/// Adds a contact to the chat.
/// If the group is promoted, also sends out a system message to all group members
pub async fn add_contact_to_chat(
context: &Context,
chat_id: ChatId,
@@ -2826,7 +2879,7 @@ pub(crate) async fn add_contact_to_chat_ex(
chat_id.reset_gossiped_timestamp(context).await?;
/*this also makes sure, not contacts are added to special or normal chats*/
// this also makes sure, no contacts are added to special or normal chats
let mut chat = Chat::load_from_db(context, chat_id).await?;
ensure!(
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
@@ -2848,7 +2901,7 @@ pub(crate) async fn add_contact_to_chat_ex(
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot add contact to group; self not in group.".into(),
));
bail!("can not add contact because our account is not part of it");
bail!("can not add contact because the account is not part of the group/broadcast");
}
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
@@ -2902,14 +2955,12 @@ pub(crate) async fn add_contact_to_chat_ex(
Ok(true)
}
/// Returns true if an avatar should be attached in the given chat.
///
/// This function does not check if the avatar is set.
/// If avatar is not set and this function returns `true`,
/// a `Chat-User-Avatar: 0` header should be sent to reset the avatar.
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.
// it can be removed after some time.
if !context.sql.get_raw_config_bool("attach_selfavatar").await? {
return Ok(false);
}
let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60;
let needs_attach = context
.sql
@@ -3203,7 +3254,9 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
let mut created_msgs: Vec<MsgId> = Vec::new();
let mut curr_timestamp: i64;
chat_id.unarchive_if_not_muted(context).await?;
chat_id
.unarchive_if_not_muted(context, MessageState::Undefined)
.await?;
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
@@ -3406,7 +3459,6 @@ pub async fn add_device_msg_with_importance(
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
msg.try_calc_and_set_dimensions(context).await.ok();
prepare_msg_blob(context, msg).await?;
chat_id.unarchive_if_not_muted(context).await?;
let timestamp_sent = create_smeared_timestamp(context).await;
@@ -3426,6 +3478,7 @@ pub async fn add_device_msg_with_importance(
}
}
let state = MessageState::InFresh;
let row_id = context
.sql
.insert(
@@ -3449,7 +3502,7 @@ pub async fn add_device_msg_with_importance(
timestamp_sent,
timestamp_sent, // timestamp_sent equals timestamp_rcvd
msg.viewtype,
MessageState::InFresh,
state,
msg.text.as_ref().cloned().unwrap_or_default(),
msg.param.to_string(),
rfc724_mid,
@@ -3458,6 +3511,9 @@ pub async fn add_device_msg_with_importance(
.await?;
msg_id = MsgId::new(u32::try_from(row_id)?);
if !msg.hidden {
chat_id.unarchive_if_not_muted(context, state).await?;
}
}
if let Some(label) = label {
@@ -3471,11 +3527,7 @@ pub async fn add_device_msg_with_importance(
}
if !msg_id.is_unset() {
if important {
context.emit_incoming_msg(chat_id, msg_id);
} else {
context.emit_msgs_changed(chat_id, msg_id);
}
chat_id.emit_msg_event(context, msg_id, important);
}
Ok(msg_id)
@@ -3626,13 +3678,11 @@ pub(crate) async fn update_msg_text_and_timestamp(
#[cfg(test)]
mod tests {
use super::*;
use crate::chatlist::{get_archived_cnt, Chatlist};
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::contact::Contact;
use crate::contact::{Contact, ContactAddress};
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use crate::test_utils::{TestContext, TestContextManager};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_info() {
@@ -4027,6 +4077,45 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_member_list_on_rejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "", "claire@example.de").await?;
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let add = alice.pop_sent_msg().await;
let bob = tcm.bob().await;
bob.recv_msg(&add).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3);
// remove bob from chat
remove_contact_from_chat(&alice, alice_chat_id, bob_id).await?;
let remove_bob = alice.pop_sent_msg().await;
bob.recv_msg(&remove_bob).await;
// remove any other member
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
alice.pop_sent_msg().await;
// readd bob
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
let add2 = alice.pop_sent_msg().await;
bob.recv_msg(&add2).await;
// number of members in chat should have updated
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_leave_group() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -4645,6 +4734,46 @@ mod tests {
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pinned_after_new_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = alice.create_chat(&bob).await.id;
let bob_chat_id = bob.create_chat(&alice).await.id;
assert!(alice_chat_id
.set_visibility(&alice, ChatVisibility::Pinned)
.await
.is_ok());
assert_eq!(
Chat::load_from_db(&alice, alice_chat_id)
.await?
.get_visibility(),
ChatVisibility::Pinned,
);
send_text_msg(&alice, alice_chat_id, "hi!".into()).await?;
assert_eq!(
Chat::load_from_db(&alice, alice_chat_id)
.await?
.get_visibility(),
ChatVisibility::Pinned,
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hi!".into()));
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, alice_chat_id);
assert_eq!(
Chat::load_from_db(&alice, alice_chat_id)
.await?
.get_visibility(),
ChatVisibility::Pinned,
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_chat_name() {
let t = TestContext::new().await;
@@ -4692,15 +4821,21 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
assert!(!shall_attach_selfavatar(&t, chat_id).await?);
let (contact_id, _) =
Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::IncomingUnknownTo).await?;
let (contact_id, _) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("foo@bar.org")?,
Origin::IncomingUnknownTo,
)
.await?;
add_contact_to_chat(&t, chat_id, contact_id).await?;
assert!(!shall_attach_selfavatar(&t, chat_id).await?);
t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending
assert!(shall_attach_selfavatar(&t, chat_id).await?);
chat_id.set_selfavatar_timestamp(&t, time()).await?;
assert!(!shall_attach_selfavatar(&t, chat_id).await?);
t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending
assert!(shall_attach_selfavatar(&t, chat_id).await?);
Ok(())
}
@@ -4939,8 +5074,8 @@ mod tests {
alice.set_config(Config::ShowEmails, Some("2")).await?;
bob.set_config(Config::ShowEmails, Some("2")).await?;
let (contact_id, _) =
Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated).await?;
let alice_bob_contact = alice.add_or_lookup_contact(&bob).await;
let contact_id = alice_bob_contact.id;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?;
@@ -5650,8 +5785,13 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_for_contact_with_blocked() -> Result<()> {
let t = TestContext::new().await;
let (contact_id, _) =
Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::ManuallyCreated).await?;
let (contact_id, _) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("foo@bar.org")?,
Origin::ManuallyCreated,
)
.await?;
// create a blocked chat
let chat_id_orig =

View File

@@ -365,7 +365,6 @@ pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::message::Viewtype;
use crate::receive_imf::receive_imf;

View File

@@ -292,9 +292,6 @@ impl Context {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
.await?;
self.sql
.set_raw_config_bool("attach_selfavatar", true)
.await?;
match value {
Some(value) => {
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
@@ -443,16 +440,15 @@ fn get_config_keys_string() -> String {
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use std::string::ToString;
use num_traits::FromPrimitive;
use super::*;
use crate::constants;
use crate::test_utils::TestContext;
use num_traits::FromPrimitive;
#[test]
fn test_to_string() {
assert_eq!(Config::MailServer.to_string(), "mail_server");

View File

@@ -6,9 +6,12 @@ mod read_url;
mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use futures::FutureExt;
use futures_lite::FutureExt as _;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use server_params::{expand_param_vector, ServerParams};
use tokio::task;
use crate::config::Config;
@@ -28,10 +31,6 @@ use crate::stock_str;
use crate::tools::{time, EmailAddress};
use crate::{chat, e2ee, provider};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use server_params::{expand_param_vector, ServerParams};
macro_rules! progress {
($context:tt, $progress:expr, $comment:expr) => {
assert!(
@@ -565,13 +564,18 @@ async fn try_imap_one_param(
provider_strict_tls: bool,
) -> Result<Imap, ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
param.user,
param.server,
param.port,
param.security,
param.certificate_checks,
param.oauth2
param.oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
} else {
"None".to_string()
}
);
info!(context, "Trying: {}", inf);
@@ -661,6 +665,7 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
if errors.iter().all(|e| {
e.msg.to_lowercase().contains("could not resolve")
|| e.msg.to_lowercase().contains("no dns resolution results")
|| e.msg
.to_lowercase()
.contains("temporary failure in name resolution")

View File

@@ -1,17 +1,16 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: <https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
use quick_xml::events::{BytesStart, Event};
use std::io::BufRead;
use std::str::FromStr;
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::provider::{Protocol, Socket};
use quick_xml::events::{BytesStart, Event};
use super::read_url::read_url;
use super::{Error, ServerParams};
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::provider::{Protocol, Socket};
#[derive(Debug)]
struct Server {

View File

@@ -3,15 +3,14 @@
//! This module implements autoconfiguration via POX (Plain Old XML) interface to Autodiscover
//! Service. Newer SOAP interface, introduced in Exchange 2010, is not used.
use quick_xml::events::Event;
use std::io::BufRead;
use crate::context::Context;
use crate::provider::{Protocol, Socket};
use quick_xml::events::Event;
use super::read_url::read_url;
use super::{Error, ServerParams};
use crate::context::Context;
use crate::provider::{Protocol, Socket};
/// Result of parsing a single `Protocol` tag.
///

View File

@@ -16,7 +16,7 @@ pub async fn read_url(context: &Context, url: &str) -> anyhow::Result<String> {
}
pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result<String> {
let client = reqwest::Client::new();
let client = crate::http::get_client()?;
let mut url = url.to_string();
// Follow up to 10 http-redirects

View File

@@ -68,6 +68,7 @@ impl Default for MediaQuality {
}
}
/// Type of the key to generate.
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
@@ -118,13 +119,13 @@ pub const DC_GCL_VERIFIED_ONLY: u32 = 0x01;
pub const DC_GCL_ADD_SELF: u32 = 0x02;
// unchanged user avatars are resent to the recipients every some days
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
pub(crate) const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
// warn about an outdated app after a given number of days.
// as we use the "provider-db generation date" as reference (that might not be updated very often)
// and as not all system get speedy updates,
// do not use too small value that will annoy users checking for nonexistant updates.
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 365;
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
@@ -169,7 +170,7 @@ pub const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// String that indicates that something is left out or truncated.
pub const DC_ELLIPSIS: &str = "[...]";
pub(crate) const DC_ELLIPSIS: &str = "[...]";
// how many lines desktop can display when fullscreen (fullscreen at zoomlevel 1x)
// (taken from "subjective" testing what looks ok)
pub const DC_DESIRED_TEXT_LINES: usize = 38;
@@ -186,11 +187,6 @@ pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100;
/// `char`s), not Unicode Grapheme Clusters.
pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES;
// Flags for empty server job
pub const DC_EMPTY_MVBOX: u32 = 0x01;
pub const DC_EMPTY_INBOX: u32 = 0x02;
// Flags for configuring IMAP and SMTP servers.
// These flags are optional
// and may be set together with the username, password etc.
@@ -220,21 +216,7 @@ pub const BALANCED_IMAGE_SIZE: u32 = 1280;
pub const WORSE_IMAGE_SIZE: u32 = 640;
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks.
// this does not affect MIME'e `To:` header.
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum KeyType {
Public = 0,
Private = 1,
}
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
#[cfg(test)]
mod tests {
@@ -262,13 +244,6 @@ mod tests {
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
}
#[test]
fn test_keytype_values() {
// values may be written to disk and must not change
assert_eq!(KeyType::Public, KeyType::from_i32(0).unwrap());
assert_eq!(KeyType::Private, KeyType::from_i32(1).unwrap());
}
#[test]
fn test_showemails_values() {
// values may be written to disk and must not change

View File

@@ -4,6 +4,7 @@ use std::cmp::Reverse;
use std::collections::BinaryHeap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::ops::Deref;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -36,6 +37,51 @@ use crate::{chat, stock_str};
/// Time during which a contact is considered as seen recently.
const SEEN_RECENTLY_SECONDS: i64 = 600;
/// Valid contact address.
#[derive(Debug, Clone, Copy)]
pub(crate) struct ContactAddress<'a>(&'a str);
impl Deref for ContactAddress<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
}
}
impl AsRef<str> for ContactAddress<'_> {
fn as_ref(&self) -> &str {
self.0
}
}
impl fmt::Display for ContactAddress<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'a> ContactAddress<'a> {
/// Constructs a new contact address from string,
/// normalizing and validating it.
pub fn new(s: &'a str) -> Result<Self> {
let addr = addr_normalize(s);
if !may_be_valid_addr(addr) {
bail!("invalid address {:?}", s);
}
Ok(Self(addr))
}
}
/// Allow converting [`ContactAddress`] to an SQLite type.
impl rusqlite::types::ToSql for ContactAddress<'_> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.0.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Contact ID, including reserved IDs.
///
/// Some contact IDs are reserved to identify special contacts. This
@@ -376,12 +422,14 @@ impl Contact {
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<ContactId> {
let name = improve_single_line_input(name);
ensure!(!addr.is_empty(), "Cannot create contact with empty address");
let (name, addr) = sanitize_name_and_addr(&name, addr);
let addr = ContactAddress::new(&addr)?;
let (contact_id, sth_modified) =
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated).await?;
Contact::add_or_lookup(context, &name, addr, Origin::ManuallyCreated)
.await
.context("add_or_lookup")?;
let blocked = Contact::is_blocked_load(context, contact_id).await?;
match sth_modified {
Modifier::None => {}
@@ -466,10 +514,12 @@ impl Contact {
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
///
/// Returns None if the contact with such address cannot exist.
pub(crate) async fn add_or_lookup(
context: &Context,
name: &str,
addr: &str,
addr: ContactAddress<'_>,
mut origin: Origin,
) -> Result<(ContactId, Modifier)> {
let mut sth_modified = Modifier::None;
@@ -477,22 +527,10 @@ impl Contact {
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
ensure!(origin != Origin::Unknown, "Missing valid origin");
let addr = addr_normalize(addr).to_string();
if context.is_self_addr(&addr).await? {
return Ok((ContactId::SELF, sth_modified));
}
if !may_be_valid_addr(&addr) {
warn!(
context,
"Bad address \"{}\" for contact \"{}\".",
addr,
if !name.is_empty() { name } else { "<unset>" },
);
bail!("Bad address supplied: {:?}", addr);
}
let mut name = name;
#[allow(clippy::collapsible_if)]
if origin <= Origin::OutgoingTo {
@@ -551,7 +589,7 @@ impl Contact {
|| row_authname.is_empty());
row_id = u32::try_from(id)?;
if origin as i32 >= row_origin as i32 && addr != row_addr {
if origin >= row_origin && addr.as_ref() != row_addr {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {
@@ -679,18 +717,25 @@ impl Contact {
for (name, addr) in split_address_book(addr_book).into_iter() {
let (name, addr) = sanitize_name_and_addr(name, addr);
let name = normalize_name(&name);
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
Err(err) => {
warn!(
context,
"Failed to add address {} from address book: {}", addr, err
);
}
Ok((_, modified)) => {
if modified != Modifier::None {
modify_cnt += 1
match ContactAddress::new(&addr) {
Ok(addr) => {
match Contact::add_or_lookup(context, &name, addr, Origin::AddressBook).await {
Ok((_, modified)) => {
if modified != Modifier::None {
modify_cnt += 1
}
}
Err(err) => {
warn!(
context,
"Failed to add address {} from address book: {}", addr, err
);
}
}
}
Err(err) => {
warn!(context, "{:#}.", err);
}
}
}
if modify_cnt > 0 {
@@ -1147,24 +1192,16 @@ impl Contact {
Ok(VerifiedStatus::Unverified)
}
/// Returns the address that verified the given contact.
pub async fn get_verifier_addr(
context: &Context,
contact_id: &ContactId,
) -> Result<Option<String>> {
let contact = Contact::load_from_db(context, *contact_id).await?;
Ok(Peerstate::from_addr(context, contact.get_addr())
/// Returns the address that verified the contact.
pub async fn get_verifier_addr(&self, context: &Context) -> Result<Option<String>> {
Ok(Peerstate::from_addr(context, self.get_addr())
.await?
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned())))
}
/// Returns the ContactId that verified the given contact.
pub async fn get_verifier_id(
context: &Context,
contact_id: &ContactId,
) -> Result<Option<ContactId>> {
let verifier_addr = Contact::get_verifier_addr(context, contact_id).await?;
/// Returns the ContactId that verified the contact.
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
let verifier_addr = self.get_verifier_addr(context).await?;
if let Some(addr) = verifier_addr {
Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?)
} else {
@@ -1416,6 +1453,7 @@ pub(crate) async fn update_last_seen(
)
.await?
> 0
&& timestamp > time() - SEEN_RECENTLY_SECONDS
{
context.interrupt_recently_seen(contact_id, timestamp).await;
}
@@ -1574,6 +1612,9 @@ impl RecentlySeenLoop {
context,
"Error receiving an interruption in recently seen loop: {}", err
);
// Maybe the sender side is closed.
// Terminate the loop to avoid looping indefinitely.
return;
}
Ok(Ok(RecentlySeenInterrupt {
contact_id,
@@ -1615,7 +1656,6 @@ impl RecentlySeenLoop {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
@@ -1693,7 +1733,7 @@ mod tests {
let (id, _modified) = Contact::add_or_lookup(
&context.ctx,
"bob",
"user@example.org",
ContactAddress::new("user@example.org")?,
Origin::IncomingReplyTo,
)
.await?;
@@ -1721,7 +1761,7 @@ mod tests {
let (contact_bob_id, modified) = Contact::add_or_lookup(
&context.ctx,
"someone",
"user@example.org",
ContactAddress::new("user@example.org")?,
Origin::ManuallyCreated,
)
.await?;
@@ -1756,6 +1796,18 @@ mod tests {
Ok(())
}
#[test]
fn test_contact_address() -> Result<()> {
let alice_addr = "alice@example.org";
let contact_address = ContactAddress::new(alice_addr)?;
assert_eq!(contact_address.as_ref(), alice_addr);
let invalid_addr = "<> foobar";
assert!(ContactAddress::new(invalid_addr).is_err());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_or_lookup() {
// add some contacts, this also tests add_address_book()
@@ -1771,10 +1823,14 @@ mod tests {
assert_eq!(Contact::add_address_book(&t, book).await.unwrap(), 4);
// check first added contact, this modifies authname because it is empty
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "bla foo", "one@eins.org", Origin::IncomingUnknownTo)
.await
.unwrap();
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bla foo",
ContactAddress::new("one@eins.org").unwrap(),
Origin::IncomingUnknownTo,
)
.await
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
@@ -1786,10 +1842,14 @@ mod tests {
assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)");
// modify first added contact
let (contact_id_test, sth_modified) =
Contact::add_or_lookup(&t, "Real one", " one@eins.org ", Origin::ManuallyCreated)
.await
.unwrap();
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"Real one",
ContactAddress::new(" one@eins.org ").unwrap(),
Origin::ManuallyCreated,
)
.await
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
@@ -1798,10 +1858,14 @@ mod tests {
assert!(!contact.is_blocked());
// check third added contact (contact without name)
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "", "three@drei.sam", Origin::IncomingUnknownTo)
.await
.unwrap();
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("three@drei.sam").unwrap(),
Origin::IncomingUnknownTo,
)
.await
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
@@ -1814,7 +1878,7 @@ mod tests {
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"m. serious",
"three@drei.sam",
ContactAddress::new("three@drei.sam").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -1826,10 +1890,14 @@ mod tests {
assert!(!contact.is_blocked());
// manually edit name of third contact (does not changed authorized name)
let (contact_id_test, sth_modified) =
Contact::add_or_lookup(&t, "schnucki", "three@drei.sam", Origin::ManuallyCreated)
.await
.unwrap();
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"schnucki",
ContactAddress::new("three@drei.sam").unwrap(),
Origin::ManuallyCreated,
)
.await
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
@@ -1838,10 +1906,14 @@ mod tests {
assert!(!contact.is_blocked());
// Fourth contact:
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "", "alice@w.de", Origin::IncomingUnknownTo)
.await
.unwrap();
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("alice@w.de").unwrap(),
Origin::IncomingUnknownTo,
)
.await
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
@@ -1976,9 +2048,13 @@ mod tests {
assert!(Contact::delete(&alice, ContactId::SELF).await.is_err());
// Create Bob contact
let (contact_id, _) =
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
.await?;
let (contact_id, _) = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
@@ -2051,10 +2127,14 @@ mod tests {
let t = TestContext::new().await;
// incoming mail `From: bob1 <bob@example.org>` - this should init authname
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "bob1", "bob@example.org", Origin::IncomingUnknownFrom)
.await
.unwrap();
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob1",
ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Created);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
@@ -2063,10 +2143,14 @@ mod tests {
assert_eq!(contact.get_display_name(), "bob1");
// incoming mail `From: bob2 <bob@example.org>` - this should update authname
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "bob2", "bob@example.org", Origin::IncomingUnknownFrom)
.await
.unwrap();
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob2",
ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
@@ -2085,10 +2169,14 @@ mod tests {
assert_eq!(contact.get_display_name(), "bob3");
// incoming mail `From: bob4 <bob@example.org>` - this should update authname, manually given name is still "bob3"
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "bob4", "bob@example.org", Origin::IncomingUnknownFrom)
.await
.unwrap();
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob4",
ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
@@ -2113,7 +2201,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"claire1",
"claire@example.org",
ContactAddress::new("claire@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2129,7 +2217,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"claire2",
"claire@example.org",
ContactAddress::new("claire@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2151,26 +2239,38 @@ mod tests {
let t = TestContext::new().await;
// Incoming message from Bob.
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "Bob", "bob@example.org", Origin::IncomingUnknownFrom)
.await?;
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"Bob",
ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownFrom,
)
.await?;
assert_eq!(sth_modified, Modifier::Created);
let contact = Contact::load_from_db(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Bob");
// Incoming message from someone else with "Not Bob" <bob@example.org> in the "To:" field.
let (contact_id_same, sth_modified) =
Contact::add_or_lookup(&t, "Not Bob", "bob@example.org", Origin::IncomingUnknownTo)
.await?;
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"Not Bob",
ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownTo,
)
.await?;
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Not Bob");
// Incoming message from Bob, changing the name back.
let (contact_id_same, sth_modified) =
Contact::add_or_lookup(&t, "Bob", "bob@example.org", Origin::IncomingUnknownFrom)
.await?;
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"Bob",
ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownFrom,
)
.await?;
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix
let contact = Contact::load_from_db(&t, contact_id).await?;
@@ -2193,9 +2293,14 @@ mod tests {
assert_eq!(contact.get_display_name(), "dave1");
// incoming mail `From: dave2 <dave@example.org>` - this should update authname
Contact::add_or_lookup(&t, "dave2", "dave@example.org", Origin::IncomingUnknownFrom)
.await
.unwrap();
Contact::add_or_lookup(
&t,
"dave2",
ContactAddress::new("dave@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave1");
@@ -2309,9 +2414,13 @@ mod tests {
let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await;
assert!(encrinfo.is_err());
let (contact_bob_id, _modified) =
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
.await?;
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
assert_eq!(encrinfo, "No encryption");
@@ -2468,9 +2577,13 @@ CCCB 5AA9 F6E1 141C 9431
async fn test_last_seen() -> Result<()> {
let alice = TestContext::new_alice().await;
let (contact_id, _) =
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
.await?;
let (contact_id, _) = Contact::add_or_lookup(
&alice,
"Bob",
ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
let contact = Contact::load_from_db(&alice, contact_id).await?;
assert_eq!(contact.last_seen(), 0);
@@ -2517,4 +2630,27 @@ Hi."#;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_by_none() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert!(contact.get_verifier_addr(&alice).await?.is_none());
assert!(contact.get_verifier_id(&alice).await?.is_none());
// Receive a message from Bob to create a peerstate.
let chat = bob.create_chat(&alice).await;
let sent_msg = bob.send_text(chat.id, "moin").await;
alice.recv_msg(&sent_msg).await;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert!(contact.get_verifier_addr(&alice).await?.is_none());
assert!(contact.get_verifier_id(&alice).await?.is_none());
Ok(())
}
}

View File

@@ -861,8 +861,13 @@ pub fn get_version_str() -> &'static str {
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use anyhow::Context as _;
use strum::IntoEnumIterator;
use tempfile::tempdir;
use super::*;
use crate::chat::{
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
@@ -873,10 +878,6 @@ mod tests {
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use crate::tools::create_outgoing_rfc724_mid;
use anyhow::Context as _;
use std::time::Duration;
use strum::IntoEnumIterator;
use tempfile::tempdir;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_wrong_db() -> Result<()> {

View File

@@ -1,6 +1,7 @@
//! End-to-end decryption support.
use std::collections::HashSet;
use std::str::FromStr;
use anyhow::Result;
use mailparse::ParsedMail;
@@ -13,7 +14,6 @@ use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::log::LogExt;
use crate::peerstate::Peerstate;
use crate::pgp;
@@ -72,9 +72,25 @@ pub(crate) async fn prepare_decryption(
});
}
let autocrypt_header = Aheader::from_headers(from, &mail.headers)
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
.flatten();
let autocrypt_header =
if let Some(autocrypt_header_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
match Aheader::from_str(&autocrypt_header_value) {
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
} else {
None
};
let dkim_results = handle_authres(context, mail, from, message_time).await?;
@@ -328,11 +344,10 @@ pub(crate) async fn get_autocrypt_peerstate(
#[cfg(test)]
mod tests {
use super::*;
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use super::*;
#[test]
fn test_has_decrypted_pgp_armor() {
let data = b" -----BEGIN PGP MESSAGE-----";

View File

@@ -1,9 +1,11 @@
//! # Download large messages manually.
use std::cmp::max;
use std::collections::BTreeMap;
use anyhow::{anyhow, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::config::Config;
use crate::context::Context;
@@ -14,7 +16,6 @@ use crate::mimeparser::{MimeMessage, Part};
use crate::param::Params;
use crate::tools::time;
use crate::{job_try, stock_str, EventType};
use std::cmp::max;
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
///
@@ -264,14 +265,13 @@ impl MimeMessage {
mod tests {
use num_traits::FromPrimitive;
use super::*;
use crate::chat::{get_chat_msgs, send_msg};
use crate::ephemeral::Timer;
use crate::message::Viewtype;
use crate::receive_imf::receive_imf_inner;
use crate::test_utils::TestContext;
use super::*;
#[test]
fn test_downloadstate_values() {
// values may be written to disk and must not change

View File

@@ -144,13 +144,12 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::test_utils::{bob_keypair, TestContext};
use super::*;
mod ensure_secret_key_exists {
use super::*;

View File

@@ -64,6 +64,7 @@
#![allow(missing_docs)]
use std::cmp::max;
use std::convert::{TryFrom, TryInto};
use std::num::ParseIntError;
use std::str::FromStr;
@@ -86,7 +87,6 @@ use crate::mimeparser::SystemMessage;
use crate::sql::{self, params_iter};
use crate::stock_str;
use crate::tools::{duration_to_str, time};
use std::cmp::max;
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
pub enum Timer {

View File

@@ -283,7 +283,7 @@ pub enum EventType {
/// @param data2 (int) Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
SecurejoinInviterProgress {
contact_id: ContactId,

View File

@@ -7,12 +7,14 @@
//! `MsgId.get_html()` will return HTML -
//! this allows nice quoting, handling linebreaks properly etc.
use futures::future::FutureExt;
use std::future::Future;
use std::pin::Pin;
use anyhow::{Context as _, Result};
use futures::future::FutureExt;
use lettre_email::mime::{self, Mime};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{Message, MsgId};
@@ -20,8 +22,6 @@ use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
use crate::plaintext::PlainText;
use crate::{context::Context, message};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
impl Message {
/// Check if the message can be retrieved as HTML.

13
src/http.rs Normal file
View File

@@ -0,0 +1,13 @@
//! # HTTP module.
use std::time::Duration;
use anyhow::Result;
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) fn get_client() -> Result<reqwest::Client> {
Ok(reqwest::ClientBuilder::new()
.timeout(HTTP_TIMEOUT)
.build()?)
}

View File

@@ -22,7 +22,7 @@ use crate::config::Config;
use crate::constants::{
Blocked, Chattype, ShowEmails, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION,
};
use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin};
use crate::contact::{normalize_name, Contact, ContactAddress, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
@@ -308,6 +308,7 @@ impl Imap {
if let Some(socks5_config) = &config.socks5_config {
if config.lp.security == Socket::Starttls {
Client::connect_starttls_socks5(
context,
imap_server,
imap_port,
socks5_config.clone(),
@@ -315,13 +316,18 @@ impl Imap {
)
.await
} else {
Client::connect_insecure_socks5((imap_server, imap_port), socks5_config.clone())
.await
Client::connect_insecure_socks5(
context,
imap_server,
imap_port,
socks5_config.clone(),
)
.await
}
} else if config.lp.security == Socket::Starttls {
Client::connect_starttls(imap_server, imap_port, config.strict_tls).await
Client::connect_starttls(context, imap_server, imap_port, config.strict_tls).await
} else {
Client::connect_insecure((imap_server, imap_port)).await
Client::connect_insecure(context, imap_server, imap_port).await
}
} else {
let config = &self.config;
@@ -330,6 +336,7 @@ impl Imap {
if let Some(socks5_config) = &config.socks5_config {
Client::connect_secure_socks5(
context,
imap_server,
imap_port,
config.strict_tls,
@@ -337,7 +344,7 @@ impl Imap {
)
.await
} else {
Client::connect_secure(imap_server, imap_port, config.strict_tls).await
Client::connect_secure(context, imap_server, imap_port, config.strict_tls).await
}
};
@@ -698,9 +705,11 @@ impl Imap {
let old_uid_next = get_uid_next(context, folder).await?;
let msgs = if fetch_existing_msgs {
self.prefetch_existing_msgs().await?
self.prefetch_existing_msgs()
.await
.context("prefetch_existing_msgs")?
} else {
self.prefetch(old_uid_next).await?
self.prefetch(old_uid_next).await.context("prefetch")?
};
let read_cnt = msgs.len();
@@ -763,7 +772,7 @@ impl Imap {
fetch_response.flags(),
show_emails,
)
.await?
.await.context("prefetch_should_download")?
{
match download_limit {
Some(download_limit) => uids_fetch.push((
@@ -799,7 +808,8 @@ impl Imap {
fetch_partially,
fetch_existing_msgs,
)
.await?;
.await
.context("fetch_many_msgs")?;
received_msgs.extend(received_msgs_in_batch);
largest_uid_fetched = max(
largest_uid_fetched,
@@ -825,11 +835,13 @@ impl Imap {
info!(context, "{} mails read from \"{}\".", read_cnt, folder);
let msg_ids = received_msgs
let msg_ids: Vec<MsgId> = received_msgs
.iter()
.flat_map(|m| m.msg_ids.clone())
.collect();
context.emit_event(EventType::IncomingMsgBunch { msg_ids });
if !msg_ids.is_empty() {
context.emit_event(EventType::IncomingMsgBunch { msg_ids });
}
chat::mark_old_messages_as_noticed(context, received_msgs).await?;
@@ -1732,7 +1744,19 @@ async fn should_move_out_of_spam(
};
// No chat found.
let (from_id, blocked_contact, _origin) =
from_field_to_contact_id(context, &from, true).await?;
match from_field_to_contact_id(context, &from, true)
.await
.context("from_field_to_contact_id")?
{
Some(res) => res,
None => {
warn!(
context,
"Contact with From address {:?} cannot exist, not moving out of spam", from
);
return Ok(false);
}
};
if blocked_contact {
// Contact is blocked, leave the message in spam.
return Ok(false);
@@ -2022,7 +2046,10 @@ pub(crate) async fn prefetch_should_download(
None => return Ok(false),
};
let (_from_id, blocked_contact, origin) =
from_field_to_contact_id(context, &from, true).await?;
match from_field_to_contact_id(context, &from, true).await? {
Some(res) => res,
None => return Ok(false),
};
// prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact.
// (prevent_rename is the last argument of from_field_to_contact_id())
@@ -2342,33 +2369,41 @@ async fn add_all_recipients_as_contacts(
.await
.with_context(|| format!("could not select {}", mailbox))?;
let contacts = imap
let recipients = imap
.get_all_recipients(context)
.await
.context("could not get recipients")?;
let mut any_modified = false;
for contact in contacts {
let display_name_normalized = contact
for recipient in recipients {
let display_name_normalized = recipient
.display_name
.as_ref()
.map(|s| normalize_name(s))
.unwrap_or_default();
match Contact::add_or_lookup(
let recipient_addr = match ContactAddress::new(&recipient.addr) {
Err(err) => {
warn!(
context,
"Could not add contact for recipient with address {:?}: {:#}",
recipient.addr,
err
);
continue;
}
Ok(recipient_addr) => recipient_addr,
};
let (_, modified) = Contact::add_or_lookup(
context,
&display_name_normalized,
&contact.addr,
recipient_addr,
Origin::OutgoingTo,
)
.await
{
Ok((_, modified)) => {
if modified != Modifier::None {
any_modified = true;
}
}
Err(e) => warn!(context, "Could not add recipient: {}", e),
.await?;
if modified != Modifier::None {
any_modified = true;
}
}
if any_modified {

View File

@@ -4,21 +4,18 @@ use std::{
};
use anyhow::{Context as _, Result};
use async_imap::Client as ImapClient;
use async_imap::Session as ImapSession;
use tokio::io::BufWriter;
use tokio::net::ToSocketAddrs;
use super::capabilities::Capabilities;
use super::session::Session;
use super::session::SessionStream;
use crate::context::Context;
use crate::login_param::build_tls;
use crate::net::connect_tcp;
use crate::socks::Socks5Config;
use super::session::SessionStream;
/// IMAP write and read timeout in seconds.
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30);
@@ -91,8 +88,13 @@ impl Client {
Ok(Session::new(session, capabilities))
}
pub async fn connect_secure(hostname: &str, port: u16, strict_tls: bool) -> Result<Self> {
let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?;
pub async fn connect_secure(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
let tls = build_tls(strict_tls);
let tls_stream = tls.connect(hostname, tcp_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
@@ -107,8 +109,8 @@ impl Client {
Ok(Client { inner: client })
}
pub async fn connect_insecure(addr: impl ToSocketAddrs) -> Result<Self> {
let tcp_stream = connect_tcp(addr, IMAP_TIMEOUT).await?;
pub async fn connect_insecure(context: &Context, hostname: &str, port: u16) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, false).await?;
let buffered_stream = BufWriter::new(tcp_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = ImapClient::new(session_stream);
@@ -120,12 +122,16 @@ impl Client {
Ok(Client { inner: client })
}
pub async fn connect_starttls(hostname: &str, port: u16, strict_tls: bool) -> Result<Self> {
let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?;
pub async fn connect_starttls(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
// Run STARTTLS command and convert the client back into a stream.
let session_stream: Box<dyn SessionStream> = Box::new(tcp_stream);
let mut client = ImapClient::new(session_stream);
let mut client = ImapClient::new(tcp_stream);
let _greeting = client
.read_response()
.await
@@ -150,12 +156,15 @@ impl Client {
}
pub async fn connect_secure_socks5(
context: &Context,
domain: &str,
port: u16,
strict_tls: bool,
socks5_config: Socks5Config,
) -> Result<Self> {
let socks5_stream = socks5_config.connect((domain, port), IMAP_TIMEOUT).await?;
let socks5_stream = socks5_config
.connect(context, domain, port, IMAP_TIMEOUT, strict_tls)
.await?;
let tls = build_tls(strict_tls);
let tls_stream = tls.connect(domain, socks5_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
@@ -170,10 +179,14 @@ impl Client {
}
pub async fn connect_insecure_socks5(
target_addr: impl ToSocketAddrs,
context: &Context,
domain: &str,
port: u16,
socks5_config: Socks5Config,
) -> Result<Self> {
let socks5_stream = socks5_config.connect(target_addr, IMAP_TIMEOUT).await?;
let socks5_stream = socks5_config
.connect(context, domain, port, IMAP_TIMEOUT, false)
.await?;
let buffered_stream = BufWriter::new(socks5_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = ImapClient::new(session_stream);
@@ -186,18 +199,18 @@ impl Client {
}
pub async fn connect_starttls_socks5(
context: &Context,
hostname: &str,
port: u16,
socks5_config: Socks5Config,
strict_tls: bool,
) -> Result<Self> {
let socks5_stream = socks5_config
.connect((hostname, port), IMAP_TIMEOUT)
.connect(context, hostname, port, IMAP_TIMEOUT, strict_tls)
.await?;
// Run STARTTLS command and convert the client back into a stream.
let session_stream: Box<dyn SessionStream> = Box::new(socks5_stream);
let mut client = ImapClient::new(session_stream);
let mut client = ImapClient::new(socks5_stream);
let _greeting = client
.read_response()
.await

View File

@@ -1,12 +1,12 @@
use super::Imap;
use std::time::{Duration, SystemTime};
use anyhow::{bail, Context as _, Result};
use async_channel::Receiver;
use async_imap::extensions::idle::IdleResponse;
use futures_lite::FutureExt;
use std::time::{Duration, SystemTime};
use super::session::Session;
use super::Imap;
use crate::imap::client::IMAP_TIMEOUT;
use crate::{context::Context, scheduler::InterruptInfo};

View File

@@ -3,13 +3,12 @@ use std::{collections::BTreeMap, time::Instant};
use anyhow::{Context as _, Result};
use futures::stream::StreamExt;
use super::{get_folder_meaning, get_folder_meaning_by_name};
use crate::config::Config;
use crate::imap::Imap;
use crate::log::LogExt;
use crate::{context::Context, imap::FolderMeaning};
use super::{get_folder_meaning, get_folder_meaning_by_name};
impl Imap {
/// Returns true if folders were scanned, false if scanning was postponed.
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<bool> {

View File

@@ -1,8 +1,8 @@
use super::session::Session as ImapSession;
use crate::context::Context;
use anyhow::Context as _;
use super::session::Session as ImapSession;
use crate::context::Context;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]

View File

@@ -656,7 +656,7 @@ async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
if let Err(err) = set_self_key(context, &armored, set_default, false).await {
error!(context, "set_self_key: {}", err);
info!(context, "set_self_key: {}", err);
continue;
}
}
@@ -769,14 +769,13 @@ where
#[cfg(test)]
mod tests {
use super::*;
use ::pgp::armor::BlockType;
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::stock_str::StockMessage;
use crate::test_utils::{alice_keypair, TestContext};
use ::pgp::armor::BlockType;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_setup_file() {
let t = TestContext::new_alice().await;

View File

@@ -424,7 +424,6 @@ LIMIT 1;
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
async fn insert_job(context: &Context, foreign_id: i64, valid: bool) {

View File

@@ -11,6 +11,7 @@ use anyhow::{ensure, Context as _, Result};
use futures::Future;
use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use tokio::runtime::Handle;
@@ -18,11 +19,9 @@ use tokio::runtime::Handle;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::context::Context;
use crate::tools::{time, EmailAddress};
// Re-export key types
pub use crate::pgp::KeyPair;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use crate::tools::{time, EmailAddress};
/// Convenience trait for working with keys.
///
@@ -390,11 +389,12 @@ impl std::str::FromStr for Fingerprint {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{alice_keypair, TestContext};
use std::sync::Arc;
use once_cell::sync::Lazy;
use std::sync::Arc;
use super::*;
use crate::test_utils::{alice_keypair, TestContext};
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);

View File

@@ -66,6 +66,7 @@ mod decrypt;
pub mod download;
mod e2ee;
pub mod ephemeral;
mod http;
mod imap;
pub mod imex;
mod scheduler;

View File

@@ -155,9 +155,10 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
#[cfg(test)]
mod tests {
use crate::test_utils::TestContext;
use anyhow::Result;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_last_error() -> Result<()> {
let t = TestContext::new().await;

View File

@@ -332,7 +332,6 @@ pub fn build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
#[test]

View File

@@ -1,7 +1,5 @@
//! # Messages and their identifiers.
#![allow(missing_docs)]
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
@@ -237,11 +235,18 @@ impl Default for MessengerMessage {
/// If you want an update, you have to recreate the object.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Message {
/// Message ID.
pub(crate) id: MsgId,
/// `From:` contact ID.
pub(crate) from_id: ContactId,
/// ID of the first contact in the `To:` header.
pub(crate) to_id: ContactId,
pub(crate) chat_id: ChatId,
pub(crate) viewtype: Viewtype,
/// State of the message.
pub(crate) state: MessageState,
pub(crate) download_state: DownloadState,
pub(crate) hidden: bool,
@@ -263,6 +268,7 @@ pub struct Message {
}
impl Message {
/// Creates a new message with given view type.
pub fn new(viewtype: Viewtype) -> Self {
Message {
viewtype,
@@ -270,6 +276,7 @@ impl Message {
}
}
/// Loads message with given ID from the database.
pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
ensure!(
!id.is_special(),
@@ -366,6 +373,12 @@ impl Message {
Ok(msg)
}
/// Returns the MIME type of an attached file if it exists.
///
/// If the MIME type is not known, the function guesses the MIME type
/// from the extension. `application/octet-stream` is used as a fallback
/// if MIME type is not known, but `None` is only returned if no file
/// is attached.
pub fn get_filemime(&self) -> Option<String> {
if let Some(m) = self.param.get(Param::MimeType) {
return Some(m.to_string());
@@ -380,11 +393,12 @@ impl Message {
None
}
/// Returns the full path to the file associated with a message.
pub fn get_file(&self, context: &Context) -> Option<PathBuf> {
self.param.get_path(Param::File, context).unwrap_or(None)
}
pub async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
if self.viewtype.has_file() {
let file_param = self.param.get_path(Param::File, context)?;
if let Some(path_and_filename) = file_param {
@@ -442,6 +456,8 @@ impl Message {
self.param.set_float(Param::SetLongitude, longitude);
}
/// Returns the message timestamp for display in the UI
/// as a unix timestamp in seconds.
pub fn get_timestamp(&self) -> i64 {
if 0 != self.timestamp_sent {
self.timestamp_sent
@@ -450,10 +466,12 @@ impl Message {
}
}
/// Returns the message ID.
pub fn get_id(&self) -> MsgId {
self.id
}
/// Returns the ID of the contact who wrote the message.
pub fn get_from_id(&self) -> ContactId {
self.from_id
}
@@ -463,30 +481,40 @@ impl Message {
self.chat_id
}
/// Returns the type of the message.
pub fn get_viewtype(&self) -> Viewtype {
self.viewtype
}
/// Returns the state of the message.
pub fn get_state(&self) -> MessageState {
self.state
}
/// Returns the message receive time as a unix timestamp in seconds.
pub fn get_received_timestamp(&self) -> i64 {
self.timestamp_rcvd
}
/// Returns the timestamp of the message for sorting.
pub fn get_sort_timestamp(&self) -> i64 {
self.timestamp_sort
}
/// Returns the text of the message.
pub fn get_text(&self) -> Option<String> {
self.text.as_ref().map(|s| s.to_string())
}
/// Returns message subject.
pub fn get_subject(&self) -> &str {
&self.subject
}
/// Returns base file name without the path.
/// The base file name includes the extension.
///
/// To get the full path, use [`Self::get_file()`].
pub fn get_filename(&self) -> Option<String> {
self.param
.get(Param::File)
@@ -503,18 +531,22 @@ impl Message {
}
}
/// Returns width of associated image or video file.
pub fn get_width(&self) -> i32 {
self.param.get_int(Param::Width).unwrap_or_default()
}
/// Returns height of associated image or video file.
pub fn get_height(&self) -> i32 {
self.param.get_int(Param::Height).unwrap_or_default()
}
/// Returns duration of associated audio or video file.
pub fn get_duration(&self) -> i32 {
self.param.get_int(Param::Duration).unwrap_or_default()
}
/// Returns true if padlock indicating message encryption should be displayed in the UI.
pub fn get_showpadlock(&self) -> bool {
self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
}
@@ -524,10 +556,12 @@ impl Message {
self.param.get_bool(Param::Bot).unwrap_or_default()
}
/// Return the ephemeral timer duration for a message.
pub fn get_ephemeral_timer(&self) -> EphemeralTimer {
self.ephemeral_timer
}
/// Returns the timestamp of the epehemeral message removal.
pub fn get_ephemeral_timestamp(&self) -> i64 {
self.ephemeral_timestamp
}
@@ -565,6 +599,7 @@ impl Message {
// C-data in the Java code (i.e. a `long` storing a C pointer)
// - We can't make a param `SenderDisplayname` for messages as sometimes the display name of a contact changes, and we want to show
// the same display name over all messages from the same sender.
/// Returns the name that should be shown over the message instead of the contact display ame.
pub fn get_override_sender_name(&self) -> Option<String> {
self.param
.get(Param::OverrideSenderDisplayname)
@@ -573,11 +608,15 @@ impl Message {
// Exposing this function over the ffi instead of get_override_sender_name() would mean that at least Android Java code has
// to handle raw C-data (as it is done for msg_get_summary())
pub fn get_sender_name(&self, contact: &Contact) -> String {
pub(crate) fn get_sender_name(&self, contact: &Contact) -> String {
self.get_override_sender_name()
.unwrap_or_else(|| contact.get_display_name().to_string())
}
/// Returns true if a message has a deviating timestamp.
///
/// A message has a deviating timestamp when it is sent on
/// another day as received/sorted by.
pub fn has_deviating_timestamp(&self) -> bool {
let cnv_to_local = gm2local_offset();
let sort_timestamp = self.get_sort_timestamp() + cnv_to_local;
@@ -586,14 +625,18 @@ impl Message {
sort_timestamp / 86400 != send_timestamp / 86400
}
/// Returns true if the message was successfully delivered to the outgoing server or even
/// received a read receipt.
pub fn is_sent(&self) -> bool {
self.state >= MessageState::OutDelivered
}
/// Returns true if the message is a forwarded message.
pub fn is_forwarded(&self) -> bool {
0 != self.param.get_int(Param::Forwarded).unwrap_or_default()
}
/// Returns true if the message is an informational message.
pub fn is_info(&self) -> bool {
let cmd = self.param.get_cmd();
self.from_id == ContactId::INFO
@@ -601,10 +644,12 @@ impl Message {
|| cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
}
/// Returns the type of an informational message.
pub fn get_info_type(&self) -> SystemMessage {
self.param.get_cmd()
}
/// Returns true if the message is a system message.
pub fn is_system_message(&self) -> bool {
let cmd = self.param.get_cmd();
cmd != SystemMessage::Unknown
@@ -622,6 +667,7 @@ impl Message {
self.viewtype.has_file() && self.state == MessageState::OutPreparing
}
/// Returns true if the message is an Autocrypt Setup Message.
pub fn is_setupmessage(&self) -> bool {
if self.viewtype != Viewtype::File {
return false;
@@ -630,6 +676,9 @@ impl Message {
self.param.get_cmd() == SystemMessage::AutocryptSetupMessage
}
/// Returns the first characters of the setup code.
///
/// This is used to pre-fill the first entry field of the setup code.
pub async fn get_setupcodebegin(&self, context: &Context) -> Option<String> {
if !self.is_setupmessage() {
return None;
@@ -650,7 +699,7 @@ impl Message {
// add room to a webrtc_instance as defined by the corresponding config-value;
// the result may still be prefixed by the type
pub fn create_webrtc_instance(instance: &str, room: &str) -> String {
pub(crate) fn create_webrtc_instance(instance: &str, room: &str) -> String {
let (videochat_type, mut url) = Message::parse_webrtc_instance(instance);
// make sure, there is a scheme in the url
@@ -707,6 +756,7 @@ impl Message {
}
}
/// Returns videochat URL if the message is a videochat invitation.
pub fn get_videochat_url(&self) -> Option<String> {
if self.viewtype == Viewtype::VideochatInvitation {
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
@@ -716,6 +766,7 @@ impl Message {
None
}
/// Returns videochat type if the message is a videochat invitation.
pub fn get_videochat_type(&self) -> Option<VideochatType> {
if self.viewtype == Viewtype::VideochatInvitation {
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
@@ -725,10 +776,16 @@ impl Message {
None
}
/// Sets or unsets message text.
pub fn set_text(&mut self, text: Option<String>) {
self.text = text;
}
/// Sets the file associated with a message.
///
/// This function does not use the file or check if it exists,
/// the file will only be used when the message is prepared
/// for sending.
pub fn set_file(&mut self, file: impl ToString, filemime: Option<&str>) {
self.param.set(Param::File, file);
if let Some(filemime) = filemime {
@@ -746,11 +803,13 @@ impl Message {
}
}
/// Sets the dimensions of associated image or video file.
pub fn set_dimension(&mut self, width: i32, height: i32) {
self.param.set_int(Param::Width, width);
self.param.set_int(Param::Height, height);
}
/// Sets the duration of associated audio or video file.
pub fn set_duration(&mut self, duration: i32) {
self.param.set_int(Param::Duration, duration);
}
@@ -760,6 +819,8 @@ impl Message {
self.param.set_int(Param::Reaction, 1);
}
/// Changes the message width, height or duration,
/// and stores it into the database.
pub async fn latefiling_mediasize(
&mut self,
context: &Context,
@@ -824,10 +885,12 @@ impl Message {
Ok(())
}
/// Returns quoted message text, if any.
pub fn quoted_text(&self) -> Option<String> {
self.param.get(Param::Quote).map(|s| s.to_string())
}
/// Returns quoted message, if any.
pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
return self.parent(context).await;
@@ -835,6 +898,10 @@ impl Message {
Ok(None)
}
/// Returns parent message according to the `In-Reply-To` header
/// if it exists in the database and is not trashed.
///
/// `References` header is not taken into account.
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
if let Some(in_reply_to) = &self.in_reply_to {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
@@ -855,6 +922,7 @@ impl Message {
self.param.set_int(Param::ForcePlaintext, 1);
}
/// Updates `param` column of the message in the database without changing other columns.
pub async fn update_param(&self, context: &Context) -> Result<()> {
context
.sql
@@ -894,6 +962,9 @@ impl Message {
}
}
/// State of the message.
/// For incoming messages, stores the information on whether the message was read or not.
/// For outgoing message, the message could be pending, already delivered or confirmed.
#[derive(
Debug,
Clone,
@@ -911,6 +982,7 @@ impl Message {
)]
#[repr(u32)]
pub enum MessageState {
/// Undefined message state.
Undefined = 0,
/// Incoming *fresh* message. Fresh messages are neither noticed
@@ -981,6 +1053,7 @@ impl std::fmt::Display for MessageState {
}
impl MessageState {
/// Returns true if the message can transition to `OutFailed` state from the current state.
pub fn can_fail(self) -> bool {
use MessageState::*;
matches!(
@@ -988,6 +1061,8 @@ impl MessageState {
OutPreparing | OutPending | OutDelivered | OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed.
)
}
/// Returns true for any outgoing message states.
pub fn is_outgoing(self) -> bool {
use MessageState::*;
matches!(
@@ -997,6 +1072,7 @@ impl MessageState {
}
}
/// Returns detailed message information in a multi-line text form.
pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
let msg = Message::load_from_db(context, msg_id).await?;
let rawtxt: Option<String> = context
@@ -1104,7 +1180,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
if let Some(path) = msg.get_file(context) {
let bytes = get_filebytes(context, &path).await?;
ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes);
ret += &format!("\nFile: {}, {} bytes\n", path.display(), bytes);
}
if msg.viewtype != Viewtype::Text {
@@ -1161,7 +1237,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
Ok(ret)
}
pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
pub(crate) fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
let extension: &str = &path.extension()?.to_str()?.to_lowercase();
let info = match extension {
// before using viewtype other than Viewtype::File,
@@ -1274,6 +1350,9 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
Ok(headers)
}
/// Deletes requested messages
/// by moving them to the trash chat
/// and scheduling for deletion on IMAP.
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
for msg_id in msg_ids.iter() {
let msg = Message::load_from_db(context, *msg_id).await?;
@@ -1321,6 +1400,7 @@ async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()>
Ok(())
}
/// Marks requested messages as seen.
pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
if msg_ids.is_empty() {
return Ok(());
@@ -1453,7 +1533,8 @@ pub(crate) async fn update_msg_state(
// Context functions to work with messages
pub async fn exists(context: &Context, msg_id: MsgId) -> Result<bool> {
/// Returns true if given message ID exists in the database and is not trashed.
pub(crate) async fn exists(context: &Context, msg_id: MsgId) -> Result<bool> {
if msg_id.is_special() {
return Ok(false);
}
@@ -1470,7 +1551,7 @@ pub async fn exists(context: &Context, msg_id: MsgId) -> Result<bool> {
}
}
pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str) {
pub(crate) async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str) {
if let Ok(mut msg) = Message::load_from_db(context, msg_id).await {
if msg.state.can_fail() {
msg.state = MessageState::OutFailed;
@@ -1716,6 +1797,20 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
}
}
/// Estimates the number of messages that will be deleted
/// by the options `delete_device_after` or `delete_server_after`.
/// This is typically used to show the estimated impact to the user
/// before actually enabling deletion of old messages.
///
/// If `from_server` is true,
/// estimate deletion count for server,
/// otherwise estimate deletion count for device.
///
/// Count messages older than the given number of `seconds`.
///
/// Returns the number of messages that are older than the given number of seconds.
/// This includes e-mails downloaded due to the `show_emails` option.
/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
pub async fn estimate_deletion_cnt(
context: &Context,
from_server: bool,
@@ -1804,6 +1899,7 @@ pub(crate) async fn rfc724_mid_exists(
)]
#[repr(u32)]
pub enum Viewtype {
/// Unknown message type.
Unknown = 0,
/// Text message.
@@ -1886,14 +1982,13 @@ impl Viewtype {
mod tests {
use num_traits::FromPrimitive;
use super::*;
use crate::chat::{marknoticed_chat, ChatItem};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::test_utils as test;
use crate::test_utils::{TestContext, TestContextManager};
use super::*;
#[test]
fn test_guess_msgtype_from_suffix() {
assert_eq!(

View File

@@ -76,6 +76,7 @@ pub struct MimeFactory<'a> {
/// and must be deleted if the message is actually queued for sending.
sync_ids_to_delete: Option<String>,
/// True if the avatar should be attached.
attach_selfavatar: bool,
}
@@ -689,7 +690,9 @@ impl<'a> MimeFactory<'a> {
.fold(message, |message, header| message.header(header));
// Add gossip headers in chats with multiple recipients
if peerstates.len() > 1 && self.should_do_gossip(context).await? {
if (peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?)
&& self.should_do_gossip(context).await?
{
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if peerstate.peek_key(min_verified).is_some() {
if let Some(header) = peerstate.render_gossip_header(min_verified) {
@@ -722,9 +725,11 @@ impl<'a> MimeFactory<'a> {
));
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "mimefactory: outgoing message mime:");
let raw_message = message.clone().build().as_string();
println!("{}", raw_message);
info!(
context,
"mimefactory: unencrypted message mime-body:\n{}",
message.clone().build().as_string(),
);
}
let encrypted = encrypt_helper
@@ -782,6 +787,14 @@ impl<'a> MimeFactory<'a> {
.into_iter()
.fold(outer_message, |message, header| message.header(header));
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"mimefactory: outgoing message mime-body:\n{}",
outer_message.clone().build().as_string(),
);
}
let MimeFactory {
last_added_location_id,
..
@@ -905,6 +918,17 @@ impl<'a> MimeFactory<'a> {
"Secure-Join".to_string(),
"vg-member-added".to_string(),
));
// FIXME: Old clients require Secure-Join-Fingerprint header. Remove this
// eventually.
let fingerprint = Peerstate::from_addr(context, email_to_add)
.await?
.context("No peerstate found in db")?
.public_key_fingerprint
.context("No public key fingerprint in db for the member to add")?;
headers.protected.push(Header::new(
"Secure-Join-Fingerprint".into(),
fingerprint.hex(),
));
}
}
SystemMessage::GroupNameChanged => {
@@ -1484,18 +1508,17 @@ fn maybe_encode_words(words: &str) -> String {
mod tests {
use mailparse::{addrparse_header, MailHeaderMap};
use super::*;
use crate::chat::ChatId;
use crate::chat::{
self, add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
ProtectionStatus,
};
use crate::chatlist::Chatlist;
use crate::contact::Origin;
use crate::contact::{ContactAddress, Origin};
use crate::mimeparser::MimeMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::{get_chat_msg, TestContext};
use super::*;
#[test]
fn test_render_email_address() {
let display_name = "ä space";
@@ -1817,11 +1840,15 @@ mod tests {
}
async fn first_subject_str(t: TestContext) -> String {
let contact_id =
Contact::add_or_lookup(&t, "Dave", "dave@example.com", Origin::ManuallyCreated)
.await
.unwrap()
.0;
let contact_id = Contact::add_or_lookup(
&t,
"Dave",
ContactAddress::new("dave@example.com").unwrap(),
Origin::ManuallyCreated,
)
.await
.unwrap()
.0;
let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap();

View File

@@ -57,6 +57,8 @@ pub struct MimeMessage {
/// Whether the From address was repeated in the signed part
/// (and we know that the signer intended to send from this address)
pub from_is_signed: bool,
/// The List-Post address is only set for mailing lists. Users can send
/// messages to this address to post them to the list.
pub list_post: Option<String>,
pub chat_disposition_notification_to: Option<SingleInfo>,
pub decryption_info: DecryptionInfo,
@@ -244,8 +246,11 @@ impl MimeMessage {
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "decrypted message mime-body:");
println!("{}", String::from_utf8_lossy(&mail_raw));
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}
(Ok(decrypted_mail), signatures, true)
}
@@ -718,6 +723,8 @@ impl MimeMessage {
!self.signatures.is_empty()
}
/// Returns whether the email contains a `chat-version` header.
/// This indicates that the email is a DC-email.
pub(crate) fn has_chat_version(&self) -> bool {
self.header.contains_key("chat-version")
}
@@ -1950,6 +1957,8 @@ where
mod tests {
#![allow(clippy::indexing_slicing)]
use mailparse::ParsedMail;
use super::*;
use crate::{
chatlist::Chatlist,
@@ -1959,7 +1968,6 @@ mod tests {
receive_imf::receive_imf,
test_utils::TestContext,
};
use mailparse::ParsedMail;
impl AvatarAction {
pub fn is_change(&self) -> bool {

View File

@@ -1,25 +1,180 @@
///! # Common network utilities.
use std::net::{IpAddr, SocketAddr};
use std::pin::Pin;
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context as _, Result};
use tokio::net::{TcpStream, ToSocketAddrs};
use anyhow::{Context as _, Error, Result};
use tokio::net::{lookup_host, TcpStream};
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::tools::time;
async fn connect_tcp_inner(addr: SocketAddr, timeout_val: Duration) -> Result<TcpStream> {
let tcp_stream = timeout(timeout_val, TcpStream::connect(addr))
.await
.context("connection timeout")?
.context("connection failure")?;
Ok(tcp_stream)
}
async fn lookup_host_with_timeout(
hostname: &str,
port: u16,
timeout_val: Duration,
) -> Result<Vec<SocketAddr>> {
let res = timeout(timeout_val, lookup_host((hostname, port)))
.await
.context("DNS lookup timeout")?
.context("DNS lookup failure")?;
Ok(res.collect())
}
/// Looks up hostname and port using DNS and updates the address resolution cache.
///
/// If `load_cache` is true, appends cached results not older than 30 days to the end.
async fn lookup_host_with_cache(
context: &Context,
hostname: &str,
port: u16,
timeout_val: Duration,
load_cache: bool,
) -> Result<Vec<SocketAddr>> {
let now = time();
let mut resolved_addrs = match lookup_host_with_timeout(hostname, port, timeout_val).await {
Ok(res) => res,
Err(err) => {
warn!(
context,
"DNS resolution for {}:{} failed: {:#}.", hostname, port, err
);
Vec::new()
}
};
for addr in resolved_addrs.iter() {
let ip_string = addr.ip().to_string();
if ip_string == hostname {
// IP address resolved into itself, not interesting to cache.
continue;
}
info!(context, "Resolved {}:{} into {}.", hostname, port, &addr);
// Update the cache.
context
.sql
.execute(
"INSERT INTO dns_cache
(hostname, address, timestamp)
VALUES (?, ?, ?)
ON CONFLICT (hostname, address)
DO UPDATE SET timestamp=excluded.timestamp",
paramsv![hostname, ip_string, now],
)
.await?;
}
if load_cache {
for cached_address in context
.sql
.query_map(
"SELECT address
FROM dns_cache
WHERE hostname = ?
AND ? < timestamp + 30 * 24 * 3600
ORDER BY timestamp DESC",
paramsv![hostname, now],
|row| {
let address: String = row.get(0)?;
Ok(address)
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?
{
match IpAddr::from_str(&cached_address) {
Ok(ip_addr) => {
let addr = SocketAddr::new(ip_addr, port);
if !resolved_addrs.contains(&addr) {
resolved_addrs.push(addr);
}
}
Err(err) => {
warn!(
context,
"Failed to parse cached address {:?}: {:#}.", cached_address, err
);
}
}
}
}
Ok(resolved_addrs)
}
/// Returns a TCP connection stream with read/write timeouts set
/// and Nagle's algorithm disabled with `TCP_NODELAY`.
///
/// `TCP_NODELAY` ensures writing to the stream always results in immediate sending of the packet
/// to the network, which is important to reduce the latency of interactive protocols such as IMAP.
///
/// If `load_cache` is true, may use cached DNS results.
/// Because the cache may be poisoned with incorrect results by networks hijacking DNS requests,
/// this option should only be used when connection is authenticated,
/// for example using TLS.
/// If TLS is not used or invalid TLS certificates are allowed,
/// this option should be disabled.
pub(crate) async fn connect_tcp(
addr: impl ToSocketAddrs,
context: &Context,
host: &str,
port: u16,
timeout_val: Duration,
load_cache: bool,
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
let tcp_stream = timeout(timeout_val, TcpStream::connect(addr))
.await
.context("connection timeout")?
.context("connection failure")?;
let mut tcp_stream = None;
let mut last_error = None;
for resolved_addr in
lookup_host_with_cache(context, host, port, timeout_val, load_cache).await?
{
match connect_tcp_inner(resolved_addr, timeout_val).await {
Ok(stream) => {
tcp_stream = Some(stream);
// Maximize priority of this cached entry.
context
.sql
.execute(
"UPDATE dns_cache
SET timestamp = ?
WHERE address = ?",
paramsv![time(), resolved_addr.ip().to_string()],
)
.await?;
break;
}
Err(err) => {
warn!(
context,
"Failed to connect to {}: {:#}.", resolved_addr, err
);
last_error = Some(err);
}
}
}
let tcp_stream = match tcp_stream {
Some(tcp_stream) => tcp_stream,
None => {
return Err(last_error.unwrap_or_else(|| Error::msg("no DNS resolution results")));
}
};
// Disable Nagle's algorithm.
tcp_stream.set_nodelay(true)?;

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