Compare commits

..

7 Commits

Author SHA1 Message Date
Floris Bruynooghe
b9fd5296bb Re-add info message using elapsed stopping time 2023-12-16 17:00:51 +01:00
Floris Bruynooghe
4aa248da74 Merge branch 'master' into flub/ongoing-guard 2023-12-16 16:29:26 +01:00
Floris Bruynooghe
b7edd4e57a Merge branch 'master' into flub/ongoing-guard 2023-04-16 17:11:05 +02:00
Floris Bruynooghe
61b00f9991 typo 2023-04-04 14:44:29 +02:00
Floris Bruynooghe
32629b9f9e Merge branch 'master' into flub/ongoing-guard 2023-04-04 13:03:29 +02:00
Floris Bruynooghe
201d05d4fa Remove Context::free_ongoing function
This is now handled better by the Drop from the OngoingGuard returned
by Context::alloc_ongoing.
2023-03-30 10:52:47 +02:00
Floris Bruynooghe
0c5d1832ae ref: make Context::alloc_ongoing return a guard
This guard can be awaited and will resolve when Context::stop_ongoing
is called, i.e. the ongoing process is cancelled.  The guard will also
free the ongoing process when dropped, making it RAII and easier to
not accidentally free the ongoing process.
2023-03-30 10:46:52 +02:00
41 changed files with 764 additions and 905 deletions

View File

@@ -14,7 +14,8 @@ on:
pull_request:
push:
branches:
- main
- master
- stable
env:
RUSTFLAGS: -Dwarnings
@@ -82,9 +83,9 @@ jobs:
- os: macos-latest
rust: 1.73.0
# Minimum Supported Rust Version = 1.70.0
# Minimum Supported Rust Version = 1.67.0
- os: ubuntu-latest
rust: 1.70.0
rust: 1.67.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
@@ -130,7 +131,7 @@ jobs:
name: Build deltachat-rpc-server
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
@@ -145,7 +146,7 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
path: target/debug/deltachat-rpc-server
retention-days: 1
python_lint:
@@ -166,8 +167,8 @@ jobs:
working-directory: deltachat-rpc-client
run: tox -e lint
cffi_python_tests:
name: CFFI Python tests
python_tests:
name: Python tests
needs: ["c_library", "python_lint"]
strategy:
fail-fast: false
@@ -217,8 +218,8 @@ jobs:
working-directory: python
run: tox -e mypy,doc,py
rpc_python_tests:
name: JSON-RPC Python tests
aysnc_python_tests:
name: Async Python tests
needs: ["python_lint", "rpc_server"]
strategy:
fail-fast: false
@@ -228,8 +229,6 @@ jobs:
python: 3.12
- os: macos-latest
python: 3.12
- os: windows-latest
python: 3.12
# PyPy tests
- os: ubuntu-latest
@@ -260,18 +259,11 @@ jobs:
path: target/debug
- name: Make deltachat-rpc-server executable
if: ${{ matrix.os != 'windows-latest' }}
run: chmod +x target/debug/deltachat-rpc-server
- name: Add deltachat-rpc-server to path
if: ${{ matrix.os != 'windows-latest' }}
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
- name: Add deltachat-rpc-server to path
if: ${{ matrix.os == 'windows-latest' }}
run: |
"${{ github.workspace }}/target/debug" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Run deltachat-rpc-client tests
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}

View File

@@ -2,9 +2,9 @@ name: JSON-RPC API Test
on:
push:
branches: [main]
branches: [master]
pull_request:
branches: [main]
branches: [master]
env:
CARGO_TERM_COLOR: always

View File

@@ -8,7 +8,7 @@ name: Generate & upload node.js documentation
on:
push:
branches:
- main
- master
jobs:
generate:

View File

@@ -13,7 +13,7 @@ on:
pull_request:
push:
branches:
- main
- master
jobs:
tests:

View File

@@ -3,7 +3,8 @@ name: Build & Deploy Documentation on rs.delta.chat
on:
push:
branches:
- main
- master
- docs-gh-action
jobs:
build:

View File

@@ -7,7 +7,8 @@ name: Build & Deploy Documentation on cffi.delta.chat
on:
push:
branches:
- main
- master
- docs-gh-action
jobs:
build:

View File

@@ -1,69 +1,5 @@
# Changelog
## [1.127.1] - 2023-10-27
### API-Changes
- jsonrpc: add `.is_protection_broken` to `FullChat` and `BasicChat`.
- jsonrpc: Add `id` to `ProviderInfo`.
## [1.127.0] - 2023-10-26
### API-Changes
- [**breaking**] `dc_accounts_new` API is changed. Unused `os_name` argument is removed and `writable` argument is added.
- jsonrpc: Add `resend_messages`.
- [**breaking**] Remove unused function `is_verified_ex()` ([#4551](https://github.com/deltachat/deltachat-core-rust/pull/4551))
- [**breaking**] Make `MsgId.delete_from_db()` private.
- [**breaking**] deltachat-jsonrpc: use `kind` as a tag for all union types
- json-rpc: Force stickers to be sent as stickers ([#4819](https://github.com/deltachat/deltachat-core-rust/pull/4819)).
- Add mailto parse api ([#4829](https://github.com/deltachat/deltachat-core-rust/pull/4829)).
- [**breaking**] Remove unused `DC_STR_PROTECTION_(EN)ABLED` strings
- [**breaking**] Remove unused `dc_set_chat_protection()`
- Hide `DcSecretKey` trait from the API.
- Verified 1:1 chats ([#4315](https://github.com/deltachat/deltachat-core-rust/pull/4315)). Disabled by default, enable with `verified_one_on_one_chats` config.
### CI
- Run Rust tests with `RUST_BACKTRACE` set.
- Replace `master` branch with `main`. Run CI only on `main` branch pushes.
- Test `deltachat-rpc-client` on Windows.
### Documentation
- Document how logs and error messages should be formatted in `CONTRIBUTING.md`.
- Clarify transitive behaviour of `dc_contact_is_verfified()`.
- Document `configured_addr`.
### Features / Changes
- Add lockfile to account manager ([#4314](https://github.com/deltachat/deltachat-core-rust/pull/4314)).
- Don't show a contact as verified if their key changed since the verification ([#4574](https://github.com/deltachat/deltachat-core-rust/pull/4574)).
- deltachat-rpc-server: Add `--openrpc` option to print OpenRPC specification for JSON-RPC API. This specification can be used to generate JSON-RPC API clients.
- Track whether contact is a bot or not ([#4821](https://github.com/deltachat/deltachat-core-rust/pull/4821)).
- Replace `Config::SendSyncMsgs` with `SyncMsgs` ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)).
### Fixes
- Don't create 1:1 chat as protected for contact who doesn't prefer to encrypt ([#4538](https://github.com/deltachat/deltachat-core-rust/pull/4538)).
- Allow to save a draft if the verification is broken ([#4542](https://github.com/deltachat/deltachat-core-rust/pull/4542)).
- Fix info-message orderings of verified 1:1 chats ([#4545](https://github.com/deltachat/deltachat-core-rust/pull/4545)).
- Fix example; this was changed some time ago, see https://docs.webxdc.org/spec.html#sendupdate
- `receive_imf`: Update peerstate from db after handling Securejoin handshake ([#4600](https://github.com/deltachat/deltachat-core-rust/pull/4600)).
- Sort old incoming messages below all outgoing ones ([#4621](https://github.com/deltachat/deltachat-core-rust/pull/4621)).
- Do not mark non-verified group chats as verified when using securejoin.
- `receive_imf`: Set protection only for Chattype::Single ([#4597](https://github.com/deltachat/deltachat-core-rust/pull/4597)).
- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
- Clear VerifiedOneOnOneChats config on backup ([#4615](https://github.com/deltachat/deltachat-core-rust/pull/4615)).
- Try removal of accounts multiple times with timeouts in case the database file is blocked (restore `try_many_times` workaround).
### Build system
- Remove examples/simple.rs.
- Increase MSRV to 1.70.0.
- Update dependencies.
- Switch to iroh 0.4.x fork with updated dependencies.
## [1.126.1] - 2023-10-24
### Fixes
@@ -3031,6 +2967,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0
[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0
[1.126.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.0...v1.126.1
[1.127.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.1...v1.127.0
[1.127.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.0...v1.127.1

980
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.127.1"
version = "1.126.1"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
rust-version = "1.67"
[profile.dev]
debug = 0
@@ -26,13 +26,17 @@ opt-level = "z"
codegen-units = 1
strip = true
[patch.crates-io]
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
quinn-proto = { git = "https://github.com/quinn-rs/quinn", branch="main" }
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
anyhow = "1"
async-channel = "2.0.0"
async-channel = "1.8.0"
async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
@@ -47,12 +51,12 @@ escaper = "0.1"
fast-socks5 = "0.8"
fd-lock = "3.0.11"
futures = "0.3"
futures-lite = "2.0.0"
futures-lite = "1.13.0"
hex = "0.4.0"
hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { git = "https://github.com/deltachat/iroh", branch = "0.4-update-quic", default-features = false }
iroh = { version = "0.4.1", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
@@ -65,10 +69,9 @@ once_cell = "1.18.0"
percent-encoding = "2.3"
parking_lot = "0.12"
pgp = { version = "0.10", default-features = false }
pin-project = "1"
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.31"
quick-xml = "0.30"
rand = "0.8"
regex = "1.9"
reqwest = { version = "0.11.20", features = ["json"] }
@@ -97,7 +100,7 @@ uuid = { version = "1", features = ["serde", "v4"] }
[dev-dependencies]
ansi_term = "0.12.0"
criterion = { version = "0.5.1", features = ["async_tokio"] }
futures-lite = "2.0.0"
futures-lite = "1.13"
log = "0.4"
pretty_env_logger = "0.5"
proptest = { version = "1", default-features = false, features = ["std"] }

View File

@@ -3,8 +3,8 @@
</p>
<p align="center">
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
<a href="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml">
<img alt="Rust CI" src="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml/badge.svg">
</a>
</p>

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.127.1"
version = "1.126.1"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -19,12 +19,12 @@ schemars = "0.8.13"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.8.0"
log = "0.4"
async-channel = { version = "2.0.0" }
async-channel = { version = "1.8.0" }
futures = { version = "0.3.28" }
serde_json = "1.0.105"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { version = "1.33.0" }
tokio = { version = "1.32.0" }
sanitize-filename = "0.5"
walkdir = "2.3.3"
base64 = "0.21"
@@ -34,7 +34,7 @@ axum = { version = "0.6.20", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.33.0", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.32.0", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -31,7 +31,6 @@ pub struct FullChat {
fresh_message_counter: usize,
// is_group - please check over chat.type in frontend instead
is_contact_request: bool,
is_protection_broken: bool,
is_device_chat: bool,
self_in_group: bool,
is_muted: bool,
@@ -101,7 +100,6 @@ impl FullChat {
color,
fresh_message_counter,
is_contact_request: chat.is_contact_request(),
is_protection_broken: chat.is_protection_broken(),
is_device_chat: chat.is_device_talk(),
self_in_group: contact_ids.contains(&ContactId::SELF),
is_muted: chat.is_muted(),
@@ -136,7 +134,6 @@ pub struct BasicChat {
is_self_talk: bool,
color: String,
is_contact_request: bool,
is_protection_broken: bool,
is_device_chat: bool,
is_muted: bool,
}
@@ -163,7 +160,6 @@ impl BasicChat {
is_self_talk: chat.is_self_talk(),
color,
is_contact_request: chat.is_contact_request(),
is_protection_broken: chat.is_protection_broken(),
is_device_chat: chat.is_device_talk(),
is_muted: chat.is_muted(),
})

View File

@@ -6,8 +6,6 @@ use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderInfo {
/// Unique ID, corresponding to provider database filename.
pub id: String,
pub before_login_hint: String,
pub overview_page: String,
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
@@ -16,7 +14,6 @@ pub struct ProviderInfo {
impl ProviderInfo {
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
provider.map(|p| ProviderInfo {
id: p.id.to_owned(),
before_login_hint: p.before_login_hint.to_owned(),
overview_page: p.overview_page.to_owned(),
status: p.status.to_u32().unwrap(),

View File

@@ -17,7 +17,7 @@ mod tests {
let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts);
let (sender, receiver) = unbounded::<String>();
let (sender, mut receiver) = unbounded::<String>();
let (client, mut rx) = RpcClient::new();
let session = RpcSession::new(client, api);
@@ -36,17 +36,17 @@ mod tests {
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await;
let result = receiver.recv().await?;
let result = receiver.next().await;
println!("{result:?}");
assert_eq!(result, response.to_owned());
assert_eq!(result, Some(response.to_owned()));
}
{
let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
session.handle_incoming(request).await;
let result = receiver.recv().await?;
let result = receiver.next().await;
println!("{result:?}");
assert_eq!(result, response.to_owned());
assert_eq!(result, Some(response.to_owned()));
}
Ok(())
@@ -59,7 +59,7 @@ mod tests {
let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts);
let (sender, receiver) = unbounded::<String>();
let (sender, mut receiver) = unbounded::<String>();
let (client, mut rx) = RpcClient::new();
let session = RpcSession::new(client, api);
@@ -78,15 +78,15 @@ mod tests {
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await;
let result = receiver.recv().await?;
assert_eq!(result, response.to_owned());
let result = receiver.next().await;
assert_eq!(result, Some(response.to_owned()));
}
{
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
session.handle_incoming(request).await;
let result = receiver.recv().await?;
assert_eq!(result, response.to_owned());
let result = receiver.next().await;
assert_eq!(result, Some(response.to_owned()));
}
Ok(())

View File

@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.127.1"
"version": "1.126.1"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.127.1"
version = "1.126.1"
license = "MPL-2.0"
edition = "2021"

View File

@@ -71,3 +71,6 @@ line-length = 120
[tool.isort]
profile = "black"
[tool.pytest.ini_options]
log_cli = true

View File

@@ -358,34 +358,3 @@ def test_openrpc_command_line() -> None:
openrpc = json.loads(out)
assert "openrpc" in openrpc
assert "methods" in openrpc
def test_provider_info(rpc) -> None:
account_id = rpc.add_account()
provider_info = rpc.get_provider_info(account_id, "example.org")
assert provider_info["id"] == "example.com"
provider_info = rpc.get_provider_info(account_id, "uep7oiw4ahtaizuloith.org")
assert provider_info is None
# Test MX record resolution.
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info["id"] == "gmail"
# Disable MX record resolution.
rpc.set_config(account_id, "socks5_enabled", "1")
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
def test_qr_setup_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
while True:
event = alice.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
return

View File

@@ -29,5 +29,3 @@ commands =
[pytest]
timeout = 60
log_cli = true
log_level = info

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.127.1"
version = "1.126.1"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,12 +15,11 @@ deltachat = { path = "..", default-features = false }
anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "2.0.0"
futures-lite = "1.13.0"
log = "0.4"
num_cpus = "1"
serde_json = "1.0.105"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.33.0", features = ["io-std"] }
tokio = { version = "1.32.0", features = ["io-std"] }
tokio-util = "0.7.9"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }

View File

@@ -20,18 +20,9 @@ use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use yerpc::{RpcClient, RpcSession};
fn main() {
// Build multithreaded runtime with at least two threads.
// This ensures that on systems with one CPU
// such as CI runners there are at least two threads
// and it is more difficult to deadlock.
let r = tokio::runtime::Builder::new_multi_thread()
.worker_threads(std::cmp::max(2, num_cpus::get()))
.enable_all()
.build()
.unwrap()
.block_on(main_impl());
#[tokio::main(flavor = "multi_thread")]
async fn main() {
let r = main_impl().await;
// From tokio documentation:
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang

View File

@@ -11,7 +11,6 @@ ignore = [
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "bitflags", version = "1.3.2" },
@@ -25,7 +24,7 @@ skip = [
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "event-listener", version = "2.5.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "hashbrown", version = "<0.14.0" },
{ name = "indexmap", version = "<2.0.0" },
@@ -37,7 +36,6 @@ skip = [
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "ring", version = "0.16.20" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
@@ -47,12 +45,15 @@ skip = [
{ name = "spki", version = "0.6.0" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "untrusted", version = "0.7.1" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.48" },
{ name = "windows_aarch64_msvc", version = "<0.48" },
{ name = "windows_i686_gnu", version = "<0.48" },
{ name = "windows_i686_msvc", version = "<0.48" },
{ name = "windows-sys", version = "<0.48" },
{ name = "windows-targets", version = "<0.48" },
{ name = "windows", version = "0.32.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows_x86_64_gnu", version = "<0.48" },
{ name = "windows_x86_64_msvc", version = "<0.48" },
]
@@ -86,4 +87,5 @@ license-files = [
github = [
"async-email",
"deltachat",
"quinn-rs",
]

View File

@@ -270,7 +270,7 @@ describe('Basic offline Tests', function () {
'quota_exceeding',
'scan_all_folders_debounce_secs',
'selfavatar',
'sync_msgs',
'send_sync_msgs',
'sentbox_watch',
'show_emails',
'socks5_enabled',

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.127.1"
"version": "1.126.1"
}

View File

@@ -1 +1 @@
2023-10-27
2023-10-24

View File

@@ -14,23 +14,16 @@ and an own build machine.
- `../.github/workflows` contains jobs run by GitHub Actions.
- `run-python-test.sh` runs CFFI Python tests.
- `run-rpc-test.sh` runs JSON-RPC Python tests.
- `make-python-testenv.sh` creates a local python test development environment with CFFI bindings.
Reusing the same environment is faster than running `run-python-test.sh` which always
recreates environment from scratch and runs additional lints.
- `make-rpc-testenv.sh` creates a local python development environment with JSON-RPC bindings,
i.e. `deltachat-rpc-client` and `deltachat-rpc-server`.
- `remote_tests_python.sh` rsyncs to a build machine and runs
`run-python-test.sh` remotely on the build machine.
- `remote_tests_rust.sh` rsyncs to the build machine and runs
`run-rust-test.sh` remotely on the build machine.
- `make-python-testenv.sh` creates local python test development environment.
Reusing the same environment is faster than running `run-python-test.sh` which always
recreates environment from scratch and runs additional lints.
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
- `run_all.sh` builds Python wheels

View File

@@ -3,14 +3,14 @@ resources:
type: git
icon: github
source:
branch: main
branch: master
uri: https://github.com/deltachat/deltachat-core-rust.git
- name: deltachat-core-rust-release
type: git
icon: github
source:
branch: main
branch: master
uri: https://github.com/deltachat/deltachat-core-rust.git
tag_filter: "v*"

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
tox -c deltachat-rpc-client -e py --devenv venv
venv/bin/pip install --upgrade pip
cargo install --path deltachat-rpc-server/ --root "$PWD/venv"

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cargo install --path deltachat-rpc-server/ --root "$PWD/venv"
PATH="$PWD/venv/bin:$PATH" tox -c deltachat-rpc-client

View File

@@ -1,7 +1,6 @@
//! # Account manager module.
use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
@@ -156,7 +155,7 @@ impl Accounts {
if let Some(cfg) = self.config.get_account(id) {
let account_path = self.dir.join(cfg.dir);
try_many_times(|| fs::remove_dir_all(&account_path))
fs::remove_dir_all(&account_path)
.await
.context("failed to remove account data")?;
}
@@ -192,10 +191,10 @@ impl Accounts {
fs::create_dir_all(self.dir.join(&account_config.dir))
.await
.context("failed to create dir")?;
try_many_times(|| fs::rename(&dbfile, &new_dbfile))
fs::rename(&dbfile, &new_dbfile)
.await
.context("failed to rename dbfile")?;
try_many_times(|| fs::rename(&blobdir, &new_blobdir))
fs::rename(&blobdir, &new_blobdir)
.await
.context("failed to rename blobdir")?;
if walfile.exists() {
@@ -220,7 +219,7 @@ impl Accounts {
}
Err(err) => {
let account_path = std::path::PathBuf::from(&account_config.dir);
try_many_times(|| fs::remove_dir_all(&account_path))
fs::remove_dir_all(&account_path)
.await
.context("failed to remove account data")?;
self.config.remove_account(account_config.id).await?;
@@ -561,37 +560,6 @@ impl Config {
}
}
/// Spend up to 1 minute trying to do the operation.
///
/// Even if Delta Chat itself does not hold the file lock,
/// there may be other processes such as antivirus,
/// or the filesystem may be network-mounted.
///
/// Without this workaround removing account may fail on Windows with an error
/// "The process cannot access the file because it is being used by another process. (os error 32)".
async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
where
F: Fn() -> Fut,
Fut: Future<Output = std::result::Result<(), T>>,
{
let mut counter = 0;
loop {
counter += 1;
if let Err(err) = f().await {
if counter > 60 {
return Err(err);
}
// Wait 1 second and try again.
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
} else {
break;
}
}
Ok(())
}
/// Configuration of a single account.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct AccountConfig {

View File

@@ -297,9 +297,10 @@ pub enum Config {
#[strum(props(default = "0"))]
DownloadLimit,
/// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set.
/// Send sync messages, requires `BccSelf` to be set as well.
/// In a future versions, this switch may be removed.
#[strum(props(default = "0"))]
SyncMsgs,
SendSyncMsgs,
/// Space-separated list of all the authserv-ids which we believe
/// may be the one of our email server.
@@ -500,7 +501,7 @@ impl Context {
| Config::Configured
| Config::Bot
| Config::NotifyAboutWrongPw
| Config::SyncMsgs
| Config::SendSyncMsgs
| Config::SignUnencrypted
| Config::DisableIdle => {
ensure!(

View File

@@ -73,18 +73,16 @@ impl Context {
self.sql.is_open().await,
"cannot configure, database not opened."
);
let cancel_channel = self.alloc_ongoing().await?;
let ongoing_guard = self.alloc_ongoing().await?;
let res = self
.inner_configure()
.race(cancel_channel.recv().map(|_| {
.race(ongoing_guard.map(|_| {
progress!(self, 0);
Ok(())
}))
.await;
self.free_ongoing().await;
if let Err(err) = res.as_ref() {
progress!(
self,

View File

@@ -2,16 +2,18 @@
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::future::Future;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::task::Poll;
use std::time::{Duration, Instant, SystemTime};
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
use tokio::sync::{oneshot, Mutex, Notify, RwLock};
use crate::chat::{get_chat_cnt, ChatId};
use crate::config::Config;
@@ -250,7 +252,7 @@ pub struct InnerContext {
#[derive(Debug)]
enum RunningState {
/// Ongoing process is allocated.
Running { cancel_sender: Sender<()> },
Running { cancel_sender: oneshot::Sender<()> },
/// Cancel signal has been sent, waiting for ongoing process to be freed.
ShallStop { request: Instant },
@@ -502,51 +504,66 @@ impl Context {
/// This is for modal operations during which no other user actions are allowed. Only
/// one such operation is allowed at any given time.
///
/// The return value is a cancel token, which will release the ongoing mutex when
/// dropped.
pub(crate) async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
/// The return value is a guard which does two things:
///
/// - It is a Future which will complete when the ongoing process is cancelled using
/// [`Context::stop_ongoing`] and must stop.
/// - It will free the ongoing process, aka release the mutex, when dropped.
pub(crate) async fn alloc_ongoing(&self) -> Result<OngoingGuard> {
let mut s = self.running_state.write().await;
ensure!(
matches!(*s, RunningState::Stopped),
"There is already another ongoing process running."
);
let (sender, receiver) = channel::bounded(1);
let (cancel_tx, cancel_rx) = oneshot::channel();
*s = RunningState::Running {
cancel_sender: sender,
cancel_sender: cancel_tx,
};
let (drop_tx, drop_rx) = oneshot::channel();
let context = self.clone();
Ok(receiver)
}
tokio::spawn(async move {
drop_rx.await.ok();
let mut s = context.running_state.write().await;
if let RunningState::ShallStop { request } = *s {
info!(context, "Ongoing stopped in {:?}", request.elapsed());
}
*s = RunningState::Stopped;
});
pub(crate) async fn free_ongoing(&self) {
let mut s = self.running_state.write().await;
if let RunningState::ShallStop { request } = *s {
info!(self, "Ongoing stopped in {:?}", request.elapsed());
}
*s = RunningState::Stopped;
Ok(OngoingGuard {
cancel_rx,
drop_tx: Some(drop_tx),
})
}
/// Signal an ongoing process to stop.
pub async fn stop_ongoing(&self) {
let mut s = self.running_state.write().await;
match &*s {
RunningState::Running { cancel_sender } => {
if let Err(err) = cancel_sender.send(()).await {
warn!(self, "could not cancel ongoing: {:#}", err);
}
info!(self, "Signaling the ongoing process to stop ASAP.",);
*s = RunningState::ShallStop {
request: Instant::now(),
};
}
// Take out the state so we can call the oneshot sender (which takes ownership).
let current_state = std::mem::replace(
&mut *s,
RunningState::ShallStop {
request: Instant::now(),
},
);
match current_state {
RunningState::Running { cancel_sender } => match cancel_sender.send(()) {
Ok(()) => info!(self, "Signaling the ongoing process to stop ASAP."),
Err(()) => warn!(self, "could not cancel ongoing"),
},
RunningState::ShallStop { .. } | RunningState::Stopped => {
// Put back the current state
*s = current_state;
info!(self, "No ongoing process to stop.",);
}
}
}
#[allow(unused)]
#[cfg(test)]
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
match &*self.running_state.read().await {
RunningState::Running { .. } => false,
@@ -584,7 +601,7 @@ impl Context {
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
let sync_msgs = self.get_config_int(Config::SyncMsgs).await?;
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
let disable_idle = self.get_config_bool(Config::DisableIdle).await?;
let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?;
@@ -697,7 +714,7 @@ impl Context {
self.get_config_int(Config::KeyGenType).await?.to_string(),
);
res.insert("bcc_self", bcc_self.to_string());
res.insert("sync_msgs", sync_msgs.to_string());
res.insert("send_sync_msgs", send_sync_msgs.to_string());
res.insert("disable_idle", disable_idle.to_string());
res.insert("private_key_count", prv_key_cnt.to_string());
res.insert("public_key_count", pub_key_cnt.to_string());
@@ -1034,6 +1051,54 @@ impl Context {
}
}
/// Guard received when calling [`Context::alloc_ongoing`].
///
/// While holding this guard the ongoing mutex is held, dropping this guard frees the
/// ongoing process.
///
/// The ongoing process can also be cancelled by unrelated code calling
/// [`Context::stop_ongoing`]. This guard implements [`Future`] and the future will
/// complete when the ongoing process is cancelled and must be aborted. Freeing the ongoing
/// process works as usual in this case: when this guard is dropped. So if you need to do
/// some more work before freeing make sure to keep ownership of the guard, e.g.:
///
/// ```no_compile
/// let mut guard = context.alloc_ongoing().await?;
/// tokio::select!{
/// biased;
/// _ = &mut guard => (), // guard is not moved, so we keep ownership.
/// _ = do_work() => (),
/// };
/// do_cleaup().await;
/// drop(guard);
/// ```
pub(crate) struct OngoingGuard {
/// Receives a message when the ongoing process should be cancelled.
cancel_rx: oneshot::Receiver<()>,
/// Used by `Drop` to send a message which will free the ongoing process.
drop_tx: Option<oneshot::Sender<()>>,
}
impl Future for OngoingGuard {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
match Pin::new(&mut self.cancel_rx).poll(cx) {
Poll::Ready(_) => Poll::Ready(()),
Poll::Pending => Poll::Pending,
}
}
}
impl Drop for OngoingGuard {
fn drop(&mut self) {
if let Some(sender) = self.drop_tx.take() {
// TODO: Maybe this should log? But we'd need to have a context.
sender.send(()).ok();
}
}
}
/// Returns core version as a string.
pub fn get_version_str() -> &'static str {
&DC_VERSION_STR
@@ -1524,38 +1589,52 @@ mod tests {
async fn test_ongoing() -> Result<()> {
let context = TestContext::new().await;
// No ongoing process allocated.
println!("No ongoing process allocated.");
assert!(context.shall_stop_ongoing().await);
let receiver = context.alloc_ongoing().await?;
let mut guard = context.alloc_ongoing().await?;
// Cannot allocate another ongoing process while the first one is running.
println!("Cannot allocate another ongoing process while the first one is running.");
assert!(context.alloc_ongoing().await.is_err());
// Stop signal is not sent yet.
assert!(receiver.try_recv().is_err());
println!("Stop signal is not sent yet.");
assert!(matches!(futures::poll!(&mut guard), Poll::Pending));
assert!(!context.shall_stop_ongoing().await);
// Send the stop signal.
println!("Send the stop signal.");
context.stop_ongoing().await;
// Receive stop signal.
receiver.recv().await?;
println!("Receive stop signal.");
(&mut guard).await;
assert!(context.shall_stop_ongoing().await);
// Ongoing process is still running even though stop signal was received,
// so another one cannot be allocated.
println!("Ongoing process still running even though stop signal was received");
assert!(context.alloc_ongoing().await.is_err());
context.free_ongoing().await;
println!("free the ongoing process");
// context.free_ongoing().await;
drop(guard);
// No ongoing process allocated, should have been stopped already.
assert!(context.shall_stop_ongoing().await);
// Another ongoing process can be allocated now.
let _receiver = context.alloc_ongoing().await?;
println!("re-acquire the ongoing process");
// Since the drop guard needs to send a message and the receiving task must run and
// acquire a lock this needs some time so won't succeed immediately.
#[allow(clippy::async_yields_async)]
let _guard = tokio::time::timeout(Duration::from_secs(10), async {
loop {
match context.alloc_ongoing().await {
Ok(guard) => break guard,
Err(_) => {
// tokio::task::yield_now() results in a lot hotter loop, it takes a
// lot of yields.
tokio::time::sleep(Duration::from_millis(1)).await;
}
}
}
})
.await
.expect("timeout");
Ok(())
}

View File

@@ -1,7 +1,6 @@
//! # Events specification.
use async_channel::{self as channel, Receiver, Sender, TrySendError};
use pin_project::pin_project;
mod payload;
@@ -65,8 +64,7 @@ impl Events {
/// [`Context::get_event_emitter`]: crate::context::Context::get_event_emitter
/// [`Stream`]: futures::stream::Stream
#[derive(Debug, Clone)]
#[pin_project]
pub struct EventEmitter(#[pin] Receiver<Event>);
pub struct EventEmitter(Receiver<Event>);
impl EventEmitter {
/// Async recv of an event. Return `None` if the `Sender` has been dropped.
@@ -79,10 +77,10 @@ impl futures::stream::Stream for EventEmitter {
type Item = Event;
fn poll_next(
self: std::pin::Pin<&mut Self>,
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.project().0.poll_next(cx)
std::pin::Pin::new(&mut self.0).poll_next(cx)
}
}

View File

@@ -89,18 +89,17 @@ pub async fn imex(
path: &Path,
passphrase: Option<String>,
) -> Result<()> {
let cancel = context.alloc_ongoing().await?;
let ongoing_guard = context.alloc_ongoing().await?;
let res = {
let _guard = context.scheduler.pause(context.clone()).await?;
imex_inner(context, what, path, passphrase)
.race(async {
cancel.recv().await.ok();
Err(format_err!("canceled"))
ongoing_guard.await;
Err(format_err!("cancelled"))
})
.await
};
context.free_ongoing().await;
if let Err(err) = res.as_ref() {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:

View File

@@ -30,7 +30,6 @@ use std::pin::Pin;
use std::task::Poll;
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
use async_channel::Receiver;
use futures_lite::StreamExt;
use iroh::blobs::Collection;
use iroh::get::DataStream;
@@ -48,7 +47,7 @@ use tokio_util::sync::CancellationToken;
use crate::blob::BlobDirContents;
use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs};
use crate::context::Context;
use crate::context::{Context, OngoingGuard};
use crate::message::{Message, Viewtype};
use crate::qr::{self, Qr};
use crate::stock_str::backup_transfer_msg_body;
@@ -98,8 +97,8 @@ impl BackupProvider {
.context("Private key not available, aborting backup export")?;
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
let paused_guard = context.scheduler.pause(context.clone()).await?;
let mut ongoing_guard = context.alloc_ongoing().await?;
let paused_guard = context.scheduler.pause(context.clone()).await;
let context_dir = context
.get_blobdir()
.parent()
@@ -110,7 +109,7 @@ impl BackupProvider {
warn!(context, "Previous database export deleted");
}
let dbfile = TempPathGuard::new(dbfile);
let res = tokio::select! {
let (provider, ticket) = tokio::select! {
biased;
res = Self::prepare_inner(context, &dbfile) => {
match res {
@@ -121,22 +120,14 @@ impl BackupProvider {
},
}
},
_ = cancel_token.recv() => Err(format_err!("cancelled")),
};
let (provider, ticket) = match res {
Ok((provider, ticket)) => (provider, ticket),
Err(err) => {
context.free_ongoing().await;
return Err(err);
}
};
_ = &mut ongoing_guard => Err(format_err!("cancelled")),
}?;
let drop_token = CancellationToken::new();
let handle = {
let context = context.clone();
let drop_token = drop_token.clone();
tokio::spawn(async move {
let res = Self::watch_provider(&context, provider, cancel_token, drop_token).await;
context.free_ongoing().await;
let res = Self::watch_provider(&context, provider, ongoing_guard, drop_token).await;
// Explicit drop to move the guards into this future
drop(paused_guard);
@@ -201,7 +192,7 @@ impl BackupProvider {
async fn watch_provider(
context: &Context,
mut provider: Provider,
cancel_token: Receiver<()>,
mut cancel_token: OngoingGuard,
drop_token: CancellationToken,
) -> Result<()> {
let mut events = provider.subscribe();
@@ -261,7 +252,7 @@ impl BackupProvider {
}
}
},
_ = cancel_token.recv() => {
_ = &mut cancel_token => {
provider.shutdown();
break Err(anyhow!("BackupProvider cancelled"));
},
@@ -394,20 +385,18 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
"Cannot import backups to accounts in use."
);
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
let mut cancel_token = context.alloc_ongoing().await?;
let _guard = context.scheduler.pause(context.clone()).await;
info!(
context,
"Running get_backup for {}",
qr::format_backup(&qr)?
);
let res = tokio::select! {
tokio::select! {
biased;
res = get_backup_inner(context, qr) => res,
_ = cancel_token.recv() => Err(format_err!("cancelled")),
};
context.free_ongoing().await;
res
_ = &mut cancel_token => Err(format_err!("cancelled")),
}
}
async fn get_backup_inner(context: &Context, qr: Qr) -> Result<()> {

View File

@@ -1208,9 +1208,6 @@ impl MimeMessage {
}
msg_type
} else if filename == "multi-device-sync.json" {
if !context.get_config_bool(Config::SyncMsgs).await? {
return Ok(());
}
let serialized = String::from_utf8_lossy(decoded_data)
.parse()
.unwrap_or_default();

View File

@@ -43,6 +43,13 @@ pub(crate) struct SyncItems {
}
impl Context {
/// Checks if sync messages shall be sent.
/// Receiving sync messages is currently always enabled;
/// the messages are force-encrypted anyway.
async fn is_sync_sending_enabled(&self) -> Result<bool> {
self.get_config_bool(Config::SendSyncMsgs).await
}
/// Adds an item to the list of items that should be synchronized to other devices.
pub(crate) async fn add_sync_item(&self, data: SyncData) -> Result<()> {
self.add_sync_item_with_timestamp(data, time()).await
@@ -51,7 +58,7 @@ impl Context {
/// Adds item and timestamp to the list of items that should be synchronized to other devices.
/// If device synchronization is disabled, the function does nothing.
async fn add_sync_item_with_timestamp(&self, data: SyncData, timestamp: i64) -> Result<()> {
if !self.get_config_bool(Config::SyncMsgs).await? {
if !self.is_sync_sending_enabled().await? {
return Ok(());
}
@@ -68,7 +75,7 @@ impl Context {
/// If device synchronization is disabled,
/// no tokens exist or the chat is unpromoted, the function does nothing.
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
if !self.get_config_bool(Config::SyncMsgs).await? {
if !self.is_sync_sending_enabled().await? {
return Ok(());
}
@@ -260,20 +267,20 @@ mod tests {
use crate::token::Namespace;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_config_sync_msgs() -> Result<()> {
async fn test_is_sync_sending_enabled() -> Result<()> {
let t = TestContext::new_alice().await;
assert!(!t.get_config_bool(Config::SyncMsgs).await?);
t.set_config_bool(Config::SyncMsgs, true).await?;
assert!(t.get_config_bool(Config::SyncMsgs).await?);
t.set_config_bool(Config::SyncMsgs, false).await?;
assert!(!t.get_config_bool(Config::SyncMsgs).await?);
assert!(!t.is_sync_sending_enabled().await?);
t.set_config_bool(Config::SendSyncMsgs, true).await?;
assert!(t.is_sync_sending_enabled().await?);
t.set_config_bool(Config::SendSyncMsgs, false).await?;
assert!(!t.is_sync_sending_enabled().await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_build_sync_json() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::SyncMsgs, true).await?;
t.set_config_bool(Config::SendSyncMsgs, true).await?;
assert!(t.build_sync_json().await?.is_none());
@@ -318,7 +325,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_build_sync_json_sync_msgs_off() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::SyncMsgs, false).await?;
t.set_config_bool(Config::SendSyncMsgs, false).await?;
t.add_sync_item(SyncData::AddQrToken(QrTokenData {
invitenumber: "testinvite".to_string(),
auth: "testauth".to_string(),
@@ -446,7 +453,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_sync_msg() -> Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config_bool(Config::SyncMsgs, true).await?;
alice.set_config_bool(Config::SendSyncMsgs, true).await?;
alice
.add_sync_item(SyncData::AddQrToken(QrTokenData {
invitenumber: "in".to_string(),
@@ -473,7 +480,6 @@ mod tests {
// also here, self-talk should stay hidden
let sent_msg = alice.pop_sent_msg().await;
let alice2 = TestContext::new_alice().await;
alice2.set_config_bool(Config::SyncMsgs, true).await?;
alice2.recv_msg(&sent_msg).await;
assert!(token::exists(&alice2, token::Namespace::Auth, "testtoken").await);
assert_eq!(Chatlist::try_load(&alice2, 0, None, None).await?.len(), 0);

View File

@@ -20,7 +20,6 @@ use mailparse::headers::Headers;
use mailparse::MailHeaderMap;
use rand::{thread_rng, Rng};
use tokio::{fs, io};
use url::Url;
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
@@ -482,43 +481,7 @@ pub(crate) fn time() -> i64 {
.as_secs() as i64
}
/// Struct containing all mailto information
#[derive(Debug, Default, Eq, PartialEq)]
pub struct MailTo {
pub to: Vec<EmailAddress>,
pub subject: Option<String>,
pub body: Option<String>,
}
/// Parse mailto urls
pub fn parse_mailto(mailto_url: &str) -> Option<MailTo> {
if let Ok(url) = Url::parse(mailto_url) {
if url.scheme() == "mailto" {
let mut mailto: MailTo = Default::default();
// Extract the email address
url.path().split(',').for_each(|email| {
if let Ok(email) = EmailAddress::new(email) {
mailto.to.push(email);
}
});
// Extract query parameters
for (key, value) in url.query_pairs() {
if key == "subject" {
mailto.subject = Some(value.to_string());
} else if key == "body" {
mailto.body = Some(value.to_string());
}
}
Some(mailto)
} else {
None
}
} else {
None
}
}
/// Very simple email address wrapper.
///
/// Represents an email address, right now just the `name@domain` portion.
///
@@ -761,18 +724,18 @@ mod tests {
let raw = include_bytes!("../test-data/message/wrong-html.eml");
let expected =
"Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 6 Aug 2020 16:40:31 +0000\n\
Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 6 Aug 2020 16:40:32 +0000";
"Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 06 Aug 2020 16:40:31 +0000\n\
Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 06 Aug 2020 16:40:32 +0000";
check_parse_receive_headers(raw, expected);
let raw = include_bytes!("../test-data/message/posteo_ndn.eml");
let expected =
"Hop: By: mout01.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\
Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 9 Jun 2020 18:44:22 +0000\n\
Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\
Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\
Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 9 Jun 2020 18:44:23 +0000\n\
Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 9 Jun 2020 18:44:24 +0000";
"Hop: By: mout01.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\
Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\
Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 09 Jun 2020 18:44:24 +0000";
check_parse_receive_headers(raw, expected);
}
@@ -1320,55 +1283,4 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
assert_eq!(remove_subject_prefix("Fwd: Subject"), "Subject");
assert_eq!(remove_subject_prefix("Fw: Subject"), "Subject");
}
#[test]
fn test_parse_mailto() {
let mailto_url = "mailto:someone@example.com";
let reps = parse_mailto(mailto_url);
assert_eq!(
Some(MailTo {
to: vec![EmailAddress {
local: "someone".to_string(),
domain: "example.com".to_string()
}],
subject: None,
body: None
}),
reps
);
let mailto_url = "mailto:someone@example.com?subject=Hello%20World";
let reps = parse_mailto(mailto_url);
assert_eq!(
Some(MailTo {
to: vec![EmailAddress {
local: "someone".to_string(),
domain: "example.com".to_string()
}],
subject: Some("Hello World".to_string()),
body: None
}),
reps
);
let mailto_url = "mailto:someone@example.com,someoneelse@example.com?subject=Hello%20World&body=This%20is%20a%20test";
let reps = parse_mailto(mailto_url);
assert_eq!(
Some(MailTo {
to: vec![
EmailAddress {
local: "someone".to_string(),
domain: "example.com".to_string()
},
EmailAddress {
local: "someoneelse".to_string(),
domain: "example.com".to_string()
}
],
subject: Some("Hello World".to_string()),
body: Some("This is a test".to_string())
}),
reps
);
}
}