) -> Response {
- let (client, out_receiver) = RpcClient::new();
- let session = RpcSession::new(client.clone(), api.clone());
- handle_ws_rpc(ws, out_receiver, session).await
-}
diff --git a/deltachat-jsonrpc/typescript/example.html b/deltachat-jsonrpc/typescript/example.html
deleted file mode 100644
index 1f5ca16717..0000000000
--- a/deltachat-jsonrpc/typescript/example.html
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
- DeltaChat JSON-RPC example
-
-
-
-
- DeltaChat JSON-RPC example
-
-
- Tip: open the dev console and use the client with
- window.client
-
-
-
diff --git a/deltachat-jsonrpc/typescript/example/example.ts b/deltachat-jsonrpc/typescript/example/example.ts
deleted file mode 100644
index e45bc18ccd..0000000000
--- a/deltachat-jsonrpc/typescript/example/example.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { DcEvent, DeltaChat } from "../deltachat.js";
-
-var SELECTED_ACCOUNT = 0;
-
-window.addEventListener("DOMContentLoaded", (_event) => {
- (window as any).selectDeltaAccount = (id: string) => {
- SELECTED_ACCOUNT = Number(id);
- window.dispatchEvent(new Event("account-changed"));
- };
- console.log("launch run script...");
- run().catch((err) => console.error("run failed", err));
-});
-
-async function run() {
- const $main = document.getElementById("main")!;
- const $side = document.getElementById("side")!;
- const $head = document.getElementById("header")!;
-
- const client = new DeltaChat("ws://localhost:20808/ws");
-
- (window as any).client = client.rpc;
-
- client.on("ALL", (accountId, event) => {
- onIncomingEvent(accountId, event);
- });
-
- window.addEventListener("account-changed", async (_event: Event) => {
- listChatsForSelectedAccount();
- });
-
- await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
-
- async function loadAccountsInHeader() {
- console.log("load accounts");
- const accounts = await client.rpc.getAllAccounts();
- console.log("accounts loaded", accounts);
- for (const account of accounts) {
- if (account.kind === "Configured") {
- write(
- $head,
- `
- ${account.id}: ${account.addr!}
- `
- );
- } else {
- write(
- $head,
- `
- ${account.id}: (unconfigured)
- `
- );
- }
- }
- }
-
- async function listChatsForSelectedAccount() {
- clear($main);
- const selectedAccount = SELECTED_ACCOUNT;
- const info = await client.rpc.getAccountInfo(selectedAccount);
- if (info.kind !== "Configured") {
- return write($main, "Account is not configured");
- }
- write($main, `${info.addr!} `);
- const chats = await client.rpc.getChatlistEntries(
- selectedAccount,
- 0,
- null,
- null
- );
- for (const chatId of chats) {
- const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
- write($main, `${chat.name} `);
- const messageIds = await client.rpc.getMessageIds(
- selectedAccount,
- chatId,
- false,
- false
- );
- const messages = await client.rpc.getMessages(
- selectedAccount,
- messageIds
- );
- for (const [_messageId, message] of Object.entries(messages)) {
- if (message.kind === "message") write($main, `${message.text}
`);
- else write($main, `loading error: ${message.error}
`);
- }
- }
- }
-
- function onIncomingEvent(accountId: number, event: DcEvent) {
- write(
- $side,
- `
-
- [${event.kind} on account ${accountId}]
- f1: ${JSON.stringify(
- Object.assign({}, event, { kind: undefined })
- )}
-
`
- );
- }
-}
-
-function write(el: HTMLElement, html: string) {
- el.innerHTML += html;
-}
-function clear(el: HTMLElement) {
- el.innerHTML = "";
-}
diff --git a/deltachat-jsonrpc/typescript/example/node-add-account.js b/deltachat-jsonrpc/typescript/example/node-add-account.js
deleted file mode 100644
index df47c39c92..0000000000
--- a/deltachat-jsonrpc/typescript/example/node-add-account.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { DeltaChat } from "../dist/deltachat.js";
-
-run().catch(console.error);
-
-async function run() {
- const delta = new DeltaChat("ws://localhost:20808/ws");
- delta.on("event", (event) => {
- console.log("event", event.data);
- });
-
- const email = process.argv[2];
- const password = process.argv[3];
- if (!email || !password)
- throw new Error(
- "USAGE: node node-add-account.js "
- );
- console.log(`creating account for ${email}`);
- const id = await delta.rpc.addAccount();
- console.log(`created account id ${id}`);
- await delta.rpc.setConfig(id, "addr", email);
- await delta.rpc.setConfig(id, "mail_pw", password);
- console.log("configuration updated");
- await delta.rpc.configure(id);
- console.log("account configured!");
-
- const accounts = await delta.rpc.getAllAccounts();
- console.log("accounts", accounts);
- console.log("waiting for events...");
-}
diff --git a/deltachat-jsonrpc/typescript/example/node-demo.js b/deltachat-jsonrpc/typescript/example/node-demo.js
deleted file mode 100644
index 83ea89b3fd..0000000000
--- a/deltachat-jsonrpc/typescript/example/node-demo.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { DeltaChat } from "../dist/deltachat.js";
-
-run().catch(console.error);
-
-async function run() {
- const delta = new DeltaChat();
- delta.on("event", (event) => {
- console.log("event", event.data);
- });
-
- const accounts = await delta.rpc.getAllAccounts();
- console.log("accounts", accounts);
- console.log("waiting for events...");
-}
diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json
index b7a7d34475..85a2866760 100644
--- a/deltachat-jsonrpc/typescript/package.json
+++ b/deltachat-jsonrpc/typescript/package.json
@@ -34,7 +34,7 @@
"name": "@deltachat/jsonrpc-client",
"repository": {
"type": "git",
- "url": "https://github.com/deltachat/deltachat-core-rust.git"
+ "url": "https://github.com/chatmail/core.git"
},
"scripts": {
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
@@ -42,10 +42,6 @@
"build:cjs": "esbuild --format=cjs --bundle --packages=external dist/deltachat.js --outfile=dist/deltachat.cjs",
"build:tsc": "tsc",
"docs": "typedoc --out docs deltachat.ts",
- "example": "run-s build example:build example:start",
- "example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
- "example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
- "example:start": "http-server .",
"extract-constants": "node ./scripts/generate-constants.js",
"generate-bindings": "cargo test",
"prettier:check": "prettier --check .",
@@ -58,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
- "version": "1.155.2"
+ "version": "1.159.5"
}
diff --git a/deltachat-jsonrpc/typescript/src/client.ts b/deltachat-jsonrpc/typescript/src/client.ts
index 83cc2f7e71..32b86e7386 100644
--- a/deltachat-jsonrpc/typescript/src/client.ts
+++ b/deltachat-jsonrpc/typescript/src/client.ts
@@ -2,7 +2,7 @@ import * as T from "../generated/types.js";
import { EventType } from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
-import { WebsocketTransport, BaseTransport, Request } from "yerpc";
+import { BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
type Events = { ALL: (accountId: number, event: EventType) => void } & {
@@ -74,34 +74,6 @@ export class BaseDeltaChat<
}
}
-export type Opts = {
- url: string;
- startEventLoop: boolean;
-};
-
-export const DEFAULT_OPTS: Opts = {
- url: "ws://localhost:20808/ws",
- startEventLoop: true,
-};
-export class DeltaChat extends BaseDeltaChat {
- opts: Opts;
- close() {
- this.transport.close();
- }
- constructor(opts?: Opts | string) {
- if (typeof opts === "string") {
- opts = { ...DEFAULT_OPTS, url: opts };
- } else if (opts) {
- opts = { ...DEFAULT_OPTS, ...opts };
- } else {
- opts = { ...DEFAULT_OPTS };
- }
- const transport = new WebsocketTransport(opts.url);
- super(transport, opts.startEventLoop);
- this.opts = opts;
- }
-}
-
export class StdioDeltaChat extends BaseDeltaChat {
close() {}
constructor(input: any, output: any, startEventLoop: boolean) {
diff --git a/deltachat-jsonrpc/typescript/test/basic.ts b/deltachat-jsonrpc/typescript/test/basic.ts
index d5ce57aecb..b3ba3b6fa0 100644
--- a/deltachat-jsonrpc/typescript/test/basic.ts
+++ b/deltachat-jsonrpc/typescript/test/basic.ts
@@ -1,4 +1,3 @@
-import { strictEqual } from "assert";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
diff --git a/deltachat-jsonrpc/typescript/test/online.ts b/deltachat-jsonrpc/typescript/test/online.ts
index 2d90e80606..e92071426b 100644
--- a/deltachat-jsonrpc/typescript/test/online.ts
+++ b/deltachat-jsonrpc/typescript/test/online.ts
@@ -1,5 +1,5 @@
import { assert, expect } from "chai";
-import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js";
+import { StdioDeltaChat as DeltaChat, DcEvent, C } from "../deltachat.js";
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
const EVENT_TIMEOUT = 20000;
@@ -80,11 +80,8 @@ describe("online tests", function () {
}
this.timeout(15000);
- const contactId = await dc.rpc.createContact(
- accountId1,
- account2.email,
- null
- );
+ const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]);
+ const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0];
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
@@ -101,20 +98,18 @@ describe("online tests", function () {
expect(messageList).have.length(1);
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
expect(message.text).equal("Hello");
+ expect(message.showPadlock).equal(true);
});
- it("send and receive text message roundtrip, encrypted on answer onwards", async function () {
+ it("send and receive text message roundtrip", async function () {
if (!accountsConfigured) {
this.skip();
}
this.timeout(10000);
// send message from A to B
- const contactId = await dc.rpc.createContact(
- accountId1,
- account2.email,
- null
- );
+ const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]);
+ const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0];
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
diff --git a/deltachat-jsonrpc/typescript/tsconfig.json b/deltachat-jsonrpc/typescript/tsconfig.json
index bbb699cf45..70cf3ca0e0 100644
--- a/deltachat-jsonrpc/typescript/tsconfig.json
+++ b/deltachat-jsonrpc/typescript/tsconfig.json
@@ -15,6 +15,6 @@
"noImplicitAny": true,
"isolatedModules": true
},
- "include": ["*.ts", "example/*.ts", "test/*.ts"],
+ "include": ["*.ts", "test/*.ts"],
"compileOnSave": false
}
diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml
index e4b3f1ef2a..f89ad6e1e5 100644
--- a/deltachat-repl/Cargo.toml
+++ b/deltachat-repl/Cargo.toml
@@ -1,14 +1,14 @@
[package]
name = "deltachat-repl"
-version = "1.155.2"
+version = "1.159.5"
license = "MPL-2.0"
edition = "2021"
-repository = "https://github.com/deltachat/deltachat-core-rust"
+repository = "https://github.com/chatmail/core"
[dependencies]
anyhow = { workspace = true }
deltachat = { workspace = true, features = ["internals"]}
-dirs = "5"
+dirs = "6"
log = { workspace = true }
nu-ansi-term = { workspace = true }
qr2term = "0.3.3"
diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs
index ae676143de..0aceb14b8d 100644
--- a/deltachat-repl/src/cmdline.rs
+++ b/deltachat-repl/src/cmdline.rs
@@ -92,7 +92,7 @@ async fn reset_tables(context: &Context, bits: i32) {
context.emit_msgs_changed_without_ids();
}
-async fn poke_eml_file(context: &Context, filename: impl AsRef) -> Result<()> {
+async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
let data = read_file(context, filename).await?;
if let Err(err) = receive_imf(context, &data, false).await {
@@ -126,7 +126,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
real_spec = rs.unwrap();
}
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
- if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
+ if suffix == "eml" && poke_eml_file(context, Path::new(&real_spec)).await.is_ok() {
read_cnt += 1
}
} else {
@@ -140,7 +140,10 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", &real_spec, name);
println!("Import: {path_plus_name}");
- if poke_eml_file(context, path_plus_name).await.is_ok() {
+ if poke_eml_file(context, Path::new(&path_plus_name))
+ .await
+ .is_ok()
+ {
read_cnt += 1
}
}
@@ -1159,17 +1162,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let reaction = arg2;
send_reaction(&context, msg_id, reaction).await?;
}
- "listcontacts" | "contacts" | "listverified" => {
- let contacts = Contact::get_all(
- &context,
- if arg0 == "listverified" {
- DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
- } else {
- DC_GCL_ADD_SELF
- },
- Some(arg1),
- )
- .await?;
+ "listcontacts" | "contacts" => {
+ let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
log_contactlist(&context, &contacts).await?;
println!("{} contacts.", contacts.len());
}
@@ -1278,7 +1272,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument missing.");
- if let Ok(buf) = read_file(&context, &arg1).await {
+ if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
let (width, height) = get_filemeta(&buf)?;
println!("width={width}, height={height}");
} else {
diff --git a/deltachat-repl/src/main.rs b/deltachat-repl/src/main.rs
index 52a7a87549..8d3dfc3eb1 100644
--- a/deltachat-repl/src/main.rs
+++ b/deltachat-repl/src/main.rs
@@ -323,7 +323,7 @@ async fn start(args: Vec) -> Result<(), Error> {
}
});
- println!("Delta Chat Core is awaiting your commands.");
+ println!("Chatmail is awaiting your commands.");
let config = Config::builder()
.history_ignore_space(true)
diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml
index acb7a5430a..4fd75e90d3 100644
--- a/deltachat-rpc-client/pyproject.toml
+++ b/deltachat-rpc-client/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
-version = "1.155.2"
+version = "1.159.5"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -70,3 +70,11 @@ line-length = 120
[tool.isort]
profile = "black"
+
+[dependency-groups]
+dev = [
+ "imap-tools",
+ "pytest",
+ "pytest-timeout",
+ "pytest-xdist",
+]
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py
index 2fef315ef2..1a532c7011 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py
@@ -1,4 +1,4 @@
-"""Delta Chat JSON-RPC high-level API"""
+"""Delta Chat JSON-RPC high-level API."""
from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py
index 8849e72ea0..47d9b878cd 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py
@@ -1,4 +1,5 @@
import argparse
+import os
import re
import sys
from threading import Thread
@@ -89,8 +90,8 @@ def _run_cli(
help="accounts folder (default: current working directory)",
nargs="?",
)
- parser.add_argument("--email", action="store", help="email address")
- parser.add_argument("--password", action="store", help="password")
+ parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
+ parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
args = parser.parse_args(argv[1:])
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
@@ -114,7 +115,7 @@ def _run_cli(
def extract_addr(text: str) -> str:
- """extract email address from the given text."""
+ """Extract email address from the given text."""
match = re.match(r".*\((.+@.+)\)", text)
if match:
text = match.group(1)
@@ -123,7 +124,7 @@ def extract_addr(text: str) -> str:
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
- """return image changed/deleted info from parsing the given system message text."""
+ """Return image changed/deleted info from parsing the given system message text."""
text = text.lower()
match = re.match(r"group image (changed|deleted) by (.+).", text)
if match:
@@ -142,7 +143,7 @@ def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]:
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
- """return add/remove info from parsing the given system message text.
+ """Return add/remove info from parsing the given system message text.
returns a (action, affected, actor) tuple.
"""
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py
index 1648ebad6b..11d76ff049 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py
@@ -1,3 +1,5 @@
+"""Account module."""
+
from __future__ import annotations
from dataclasses import dataclass
@@ -26,18 +28,36 @@ class Account:
def _rpc(self) -> "Rpc":
return self.manager.rpc
- def wait_for_event(self) -> AttrDict:
+ def wait_for_event(self, event_type=None) -> AttrDict:
"""Wait until the next event and return it."""
- return AttrDict(self._rpc.wait_for_event(self.id))
+ while True:
+ next_event = AttrDict(self._rpc.wait_for_event(self.id))
+ if event_type is None or next_event.kind == event_type:
+ return next_event
def clear_all_events(self):
- """Removes all queued-up events for a given account. Useful for tests."""
+ """Remove all queued-up events for a given account.
+
+ Useful for tests.
+ """
self._rpc.clear_all_events(self.id)
def remove(self) -> None:
"""Remove the account."""
self._rpc.remove_account(self.id)
+ def clone(self) -> "Account":
+ """Clone given account.
+
+ This uses backup-transfer via iroh, i.e. the 'Add second device' feature.
+ """
+ future = self._rpc.provide_backup.future(self.id)
+ qr = self._rpc.get_backup_qr(self.id)
+ new_account = self.manager.add_account()
+ new_account._rpc.get_backup(new_account.id, qr)
+ future()
+ return new_account
+
def start_io(self) -> None:
"""Start the account I/O."""
self._rpc.start_io(self.id)
@@ -67,7 +87,7 @@ def get_config(self, key: str) -> Optional[str]:
return self._rpc.get_config(self.id, key)
def update_config(self, **kwargs) -> None:
- """update config values."""
+ """Update config values."""
for key, value in kwargs.items():
self.set_config(key, value)
@@ -83,9 +103,15 @@ def get_avatar(self) -> Optional[str]:
return self.get_config("selfavatar")
def check_qr(self, qr):
+ """Parse QR code contents.
+
+ This function takes the raw text scanned
+ and checks what can be done with it.
+ """
return self._rpc.check_qr(self.id, qr)
def set_config_from_qr(self, qr: str):
+ """Set configuration values from a QR code."""
self._rpc.set_config_from_qr(self.id, qr)
@futuremethod
@@ -93,15 +119,23 @@ def configure(self):
"""Configure an account."""
yield self._rpc.configure.future(self.id)
+ @futuremethod
+ def add_or_update_transport(self, params):
+ """Add a new transport."""
+ yield self._rpc.add_or_update_transport.future(self.id, params)
+
+ @futuremethod
+ def list_transports(self):
+ """Return the list of all email accounts that are used as a transport in the current profile."""
+ transports = yield self._rpc.list_transports.future(self.id)
+ return transports
+
def bring_online(self):
"""Start I/O and wait until IMAP becomes IDLE."""
self.start_io()
- while True:
- event = self.wait_for_event()
- if event.kind == EventType.IMAP_INBOX_IDLE:
- break
+ self.wait_for_event(EventType.IMAP_INBOX_IDLE)
- def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
+ def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
Calling this method will always result in the same
@@ -109,19 +143,42 @@ def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = No
with that e-mail address, it is unblocked and its display
name is updated if specified.
- :param obj: email-address or contact id.
+ :param obj: email-address, contact id or account.
:param name: (optional) display name for this contact.
"""
+ if isinstance(obj, Account):
+ vcard = obj.self_contact.make_vcard()
+ [contact] = self.import_vcard(vcard)
+ if name:
+ contact.set_name(name)
+ return contact
if isinstance(obj, int):
obj = Contact(self, obj)
if isinstance(obj, Contact):
obj = obj.get_snapshot().address
return Contact(self, self._rpc.create_contact(self.id, obj, name))
+ def make_vcard(self, contacts: list[Contact]) -> str:
+ """Create vCard with the given contacts."""
+ assert all(contact.account == self for contact in contacts)
+ contact_ids = [contact.id for contact in contacts]
+ return self._rpc.make_vcard(self.id, contact_ids)
+
+ def import_vcard(self, vcard: str) -> list[Contact]:
+ """Import vCard.
+
+ Return created or modified contacts in the order they appear in vCard.
+ """
+ contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
+ return [Contact(self, contact_id) for contact_id in contact_ids]
+
def create_chat(self, account: "Account") -> Chat:
- addr = account.get_config("addr")
- contact = self.create_contact(addr)
- return contact.create_chat()
+ """Create a 1:1 chat with another account."""
+ return self.create_contact(account).create_chat()
+
+ def get_device_chat(self) -> Chat:
+ """Return device chat."""
+ return self.device_contact.create_chat()
def get_contact_by_id(self, contact_id: int) -> Contact:
"""Return Contact instance for the given contact ID."""
@@ -154,8 +211,8 @@ def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
def get_contacts(
self,
query: Optional[str] = None,
+ *,
with_self: bool = False,
- verified_only: bool = False,
snapshot: bool = False,
) -> Union[list[Contact], list[AttrDict]]:
"""Get a filtered list of contacts.
@@ -163,12 +220,9 @@ def get_contacts(
:param query: if a string is specified, only return contacts
whose name or e-mail matches query.
:param with_self: if True the self-contact is also included if it matches the query.
- :param only_verified: if True only return verified contacts.
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
"""
flags = 0
- if verified_only:
- flags |= ContactFlag.VERIFIED_ONLY
if with_self:
flags |= ContactFlag.ADD_SELF
@@ -180,9 +234,14 @@ def get_contacts(
@property
def self_contact(self) -> Contact:
- """This account's identity as a Contact."""
+ """Account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
+ @property
+ def device_contact(self) -> Chat:
+ """Account's device contact."""
+ return Contact(self, SpecialContactId.DEVICE)
+
def get_chatlist(
self,
query: Optional[str] = None,
@@ -238,8 +297,7 @@ def get_chat_by_id(self, chat_id: int) -> Chat:
return Chat(self, chat_id)
def secure_join(self, qrdata: str) -> Chat:
- """Continue a Setup-Contact or Verified-Group-Invite protocol started on
- another device.
+ """Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
The function returns immediately and the handshake runs in background, sending
and receiving several messages.
@@ -296,34 +354,40 @@ def wait_next_messages(self) -> list[Message]:
def wait_for_incoming_msg_event(self):
"""Wait for incoming message event and return it."""
- while True:
- event = self.wait_for_event()
- if event.kind == EventType.INCOMING_MSG:
- return event
+ return self.wait_for_event(EventType.INCOMING_MSG)
+
+ def wait_for_msgs_changed_event(self):
+ """Wait for messages changed event and return it."""
+ return self.wait_for_event(EventType.MSGS_CHANGED)
+
+ def wait_for_msgs_noticed_event(self):
+ """Wait for messages noticed event and return it."""
+ return self.wait_for_event(EventType.MSGS_NOTICED)
def wait_for_incoming_msg(self):
"""Wait for incoming message and return it.
- Consumes all events before the next incoming message event."""
+ Consumes all events before the next incoming message event.
+ """
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
def wait_for_securejoin_inviter_success(self):
+ """Wait until SecureJoin process finishes successfully on the inviter side."""
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
def wait_for_securejoin_joiner_success(self):
+ """Wait until SecureJoin process finishes successfully on the joiner side."""
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
def wait_for_reactions_changed(self):
- while True:
- event = self.wait_for_event()
- if event.kind == EventType.REACTIONS_CHANGED:
- return event
+ """Wait for reaction change event."""
+ return self.wait_for_event(EventType.REACTIONS_CHANGED)
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
@@ -352,3 +416,7 @@ def import_self_keys(self, path) -> None:
"""Import keys."""
passphrase = "" # Importing passphrase-protected keys is currently not supported.
self._rpc.import_self_keys(self.id, str(path), passphrase)
+
+ def initiate_autocrypt_key_transfer(self) -> None:
+ """Send Autocrypt Setup Message."""
+ return self._rpc.initiate_autocrypt_key_transfer(self.id)
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py
index 9d33a5a0f4..fa4006e0fd 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py
@@ -1,3 +1,5 @@
+"""Chat module."""
+
from __future__ import annotations
import calendar
@@ -89,7 +91,8 @@ def set_name(self, name: str) -> None:
def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat in seconds.
- 0 means the timer is disabled, use 1 for immediate deletion."""
+ 0 means the timer is disabled, use 1 for immediate deletion.
+ """
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
def get_encryption_info(self) -> str:
@@ -124,6 +127,7 @@ def send_message(
html: Optional[str] = None,
viewtype: Optional[ViewType] = None,
file: Optional[str] = None,
+ filename: Optional[str] = None,
location: Optional[tuple[float, float]] = None,
override_sender_name: Optional[str] = None,
quoted_msg: Optional[Union[int, Message]] = None,
@@ -137,6 +141,7 @@ def send_message(
"html": html,
"viewtype": viewtype,
"file": file,
+ "filename": filename,
"location": location,
"overrideSenderName": override_sender_name,
"quotedMessageId": quoted_msg,
@@ -172,13 +177,14 @@ def set_draft(
self,
text: Optional[str] = None,
file: Optional[str] = None,
+ filename: Optional[str] = None,
quoted_msg: Optional[int] = None,
viewtype: Optional[str] = None,
) -> None:
"""Set draft message."""
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
- self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg, viewtype)
+ self._rpc.misc_set_draft(self.account.id, self.id, text, file, filename, quoted_msg, viewtype)
def remove_draft(self) -> None:
"""Remove draft message."""
@@ -196,12 +202,12 @@ def get_draft(self) -> Optional[AttrDict]:
return snapshot
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
- """get the list of messages in this chat."""
+ """Get the list of messages in this chat."""
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs]
def get_fresh_message_count(self) -> int:
- """Get number of fresh messages in this chat"""
+ """Get number of fresh messages in this chat."""
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
def mark_noticed(self) -> None:
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py
index cceda316eb..a62c22deb3 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py
@@ -48,6 +48,7 @@ def __init__(
self.add_hooks(hooks or [])
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
+ """Register multiple hooks."""
for hook, event in hooks:
self.add_hook(hook, event)
@@ -77,9 +78,11 @@ def remove_hook(self, hook: Callable, event: Union[type, EventFilter]) -> None:
self._hooks.get(type(event), set()).remove((hook, event))
def is_configured(self) -> bool:
+ """Return True if the client is configured."""
return self.account.is_configured()
def configure(self, email: str, password: str, **kwargs) -> None:
+ """Configure the client."""
self.account.set_config("addr", email)
self.account.set_config("mail_pw", password)
for key, value in kwargs.items():
@@ -198,5 +201,6 @@ class Bot(Client):
"""Simple bot implementation that listens to events of a single account."""
def configure(self, email: str, password: str, **kwargs) -> None:
+ """Configure the bot."""
kwargs.setdefault("bot", "1")
super().configure(email, password, **kwargs)
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/const.py b/deltachat-rpc-client/src/deltachat_rpc_client/const.py
index 5268a281c6..6ba25bdeb7 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/const.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/const.py
@@ -1,14 +1,19 @@
+"""Constants module."""
+
from enum import Enum, IntEnum
COMMAND_PREFIX = "/"
class ContactFlag(IntEnum):
- VERIFIED_ONLY = 0x01
+ """Bit flags for get_contacts() method."""
+
ADD_SELF = 0x02
class ChatlistFlag(IntEnum):
+ """Bit flags for get_chatlist() method."""
+
ARCHIVED_ONLY = 0x01
NO_SPECIALS = 0x02
ADD_ALLDONE_HINT = 0x04
@@ -16,6 +21,8 @@ class ChatlistFlag(IntEnum):
class SpecialContactId(IntEnum):
+ """Special contact IDs."""
+
SELF = 1
INFO = 2 # centered messages as "member added", used in all chats
DEVICE = 5 # messages "update info" in the device-chat
@@ -23,7 +30,7 @@ class SpecialContactId(IntEnum):
class EventType(str, Enum):
- """Core event types"""
+ """Core event types."""
INFO = "Info"
SMTP_CONNECTED = "SmtpConnected"
@@ -48,6 +55,7 @@ class EventType(str, Enum):
MSG_READ = "MsgRead"
MSG_DELETED = "MsgDeleted"
CHAT_MODIFIED = "ChatModified"
+ CHAT_DELETED = "ChatDeleted"
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
CONTACTS_CHANGED = "ContactsChanged"
LOCATION_CHANGED = "LocationChanged"
@@ -70,7 +78,7 @@ class EventType(str, Enum):
class ChatId(IntEnum):
- """Special chat ids"""
+ """Special chat IDs."""
TRASH = 3
ARCHIVED_LINK = 6
@@ -79,7 +87,7 @@ class ChatId(IntEnum):
class ChatType(IntEnum):
- """Chat types"""
+ """Chat type."""
UNDEFINED = 0
SINGLE = 100
@@ -89,7 +97,7 @@ class ChatType(IntEnum):
class ChatVisibility(str, Enum):
- """Chat visibility types"""
+ """Chat visibility types."""
NORMAL = "Normal"
ARCHIVED = "Archived"
@@ -97,7 +105,7 @@ class ChatVisibility(str, Enum):
class DownloadState(str, Enum):
- """Message download state"""
+ """Message download state."""
DONE = "Done"
AVAILABLE = "Available"
@@ -158,14 +166,14 @@ class MessageState(IntEnum):
class MessageId(IntEnum):
- """Special message ids"""
+ """Special message IDs."""
DAYMARKER = 9
LAST_SPECIAL = 9
class CertificateChecks(IntEnum):
- """Certificate checks mode"""
+ """Certificate checks mode."""
AUTOMATIC = 0
STRICT = 1
@@ -173,7 +181,7 @@ class CertificateChecks(IntEnum):
class Connectivity(IntEnum):
- """Connectivity states"""
+ """Connectivity states."""
NOT_CONNECTED = 1000
CONNECTING = 2000
@@ -182,7 +190,7 @@ class Connectivity(IntEnum):
class KeyGenType(IntEnum):
- """Type of the key to generate"""
+ """Type of the key to generate."""
DEFAULT = 0
RSA2048 = 1
@@ -192,21 +200,21 @@ class KeyGenType(IntEnum):
# "Lp" means "login parameters"
class LpAuthFlag(IntEnum):
- """Authorization flags"""
+ """Authorization flags."""
OAUTH2 = 0x2
NORMAL = 0x4
class MediaQuality(IntEnum):
- """Media quality setting"""
+ """Media quality setting."""
BALANCED = 0
WORSE = 1
class ProviderStatus(IntEnum):
- """Provider status according to manual testing"""
+ """Provider status according to manual testing."""
OK = 1
PREPARATION = 2
@@ -214,7 +222,7 @@ class ProviderStatus(IntEnum):
class PushNotifyState(IntEnum):
- """Push notifications state"""
+ """Push notifications state."""
NOT_CONNECTED = 0
HEARTBEAT = 1
@@ -222,7 +230,7 @@ class PushNotifyState(IntEnum):
class ShowEmails(IntEnum):
- """Show emails mode"""
+ """Show emails mode."""
OFF = 0
ACCEPTED_CONTACTS = 1
@@ -230,7 +238,7 @@ class ShowEmails(IntEnum):
class SocketSecurity(IntEnum):
- """Socket security"""
+ """Socket security."""
AUTOMATIC = 0
SSL = 1
@@ -239,7 +247,7 @@ class SocketSecurity(IntEnum):
class VideochatType(IntEnum):
- """Video chat URL type"""
+ """Video chat URL type."""
UNKNOWN = 0
BASICWEBRTC = 1
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py
index 81c4bba59d..c2444afbd2 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py
@@ -1,3 +1,5 @@
+"""Contact module."""
+
from dataclasses import dataclass
from typing import TYPE_CHECKING
@@ -11,8 +13,7 @@
@dataclass
class Contact:
- """
- Contact API.
+ """Contact API.
Essentially a wrapper for RPC, account ID and a contact ID.
"""
@@ -45,8 +46,9 @@ def set_name(self, name: str) -> None:
self._rpc.change_contact_name(self.account.id, self.id, name)
def get_encryption_info(self) -> str:
- """Get a multi-line encryption info, containing your fingerprint and
- the fingerprint of the contact.
+ """Get a multi-line encryption info.
+
+ Encryption info contains your fingerprint and the fingerprint of the contact.
"""
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
@@ -66,4 +68,5 @@ def create_chat(self) -> "Chat":
)
def make_vcard(self) -> str:
- return self._rpc.make_vcard(self.account.id, [self.id])
+ """Make a vCard for the contact."""
+ return self.account.make_vcard([self])
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py
index c972a865e2..58ee30b322 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py
@@ -1,3 +1,5 @@
+"""Account manager module."""
+
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -10,12 +12,13 @@
class DeltaChat:
- """
- Delta Chat accounts manager.
+ """Delta Chat accounts manager.
+
This is the root of the object oriented API.
"""
def __init__(self, rpc: "Rpc") -> None:
+ """Initialize account manager."""
self.rpc = rpc
def add_account(self) -> Account:
@@ -37,9 +40,7 @@ def stop_io(self) -> None:
self.rpc.stop_io_for_all_accounts()
def maybe_network(self) -> None:
- """Indicate that the network likely has come back or just that the network
- conditions might have changed.
- """
+ """Indicate that the network conditions might have changed."""
self.rpc.maybe_network()
def get_system_info(self) -> AttrDict:
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/events.py b/deltachat-rpc-client/src/deltachat_rpc_client/events.py
index ca46f5b77d..98dfbc6801 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/events.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/events.py
@@ -36,7 +36,7 @@ def __init__(self, func: Optional[Callable] = None):
@abstractmethod
def __hash__(self) -> int:
- """Object's unique hash"""
+ """Object's unique hash."""
@abstractmethod
def __eq__(self, other) -> bool:
@@ -52,9 +52,7 @@ def _call_func(self, event) -> bool:
@abstractmethod
def filter(self, event):
- """Return True-like value if the event passed the filter and should be
- used, or False-like value otherwise.
- """
+ """Return True-like value if the event passed the filter."""
class RawEvent(EventFilter):
@@ -82,31 +80,17 @@ def __eq__(self, other) -> bool:
return False
def filter(self, event: "AttrDict") -> bool:
+ """Filter an event.
+
+ Return true if the event should be processed.
+ """
if self.types and event.kind not in self.types:
return False
return self._call_func(event)
class NewMessage(EventFilter):
- """Matches whenever a new message arrives.
-
- Warning: registering a handler for this event will cause the messages
- to be marked as read. Its usage is mainly intended for bots.
-
- :param pattern: if set, this Pattern will be used to filter the message by its text
- content.
- :param command: If set, only match messages with the given command (ex. /help).
- Setting this property implies `is_info==False`.
- :param is_bot: If set to True only match messages sent by bots, if set to None
- match messages from bots and users. If omitted or set to False
- only messages from users will be matched.
- :param is_info: If set to True only match info/system messages, if set to False
- only match messages that are not info/system messages. If omitted
- info/system messages as well as normal messages will be matched.
- :param func: A Callable function that should accept the event as input
- parameter, and return a bool value indicating whether the event
- should be dispatched or not.
- """
+ """Matches whenever a new message arrives."""
def __init__(
self,
@@ -121,6 +105,25 @@ def __init__(
is_info: Optional[bool] = None,
func: Optional[Callable[["AttrDict"], bool]] = None,
) -> None:
+ """Initialize a new message filter.
+
+ Warning: registering a handler for this event will cause the messages
+ to be marked as read. Its usage is mainly intended for bots.
+
+ :param pattern: if set, this Pattern will be used to filter the message by its text
+ content.
+ :param command: If set, only match messages with the given command (ex. /help).
+ Setting this property implies `is_info==False`.
+ :param is_bot: If set to True only match messages sent by bots, if set to None
+ match messages from bots and users. If omitted or set to False
+ only messages from users will be matched.
+ :param is_info: If set to True only match info/system messages, if set to False
+ only match messages that are not info/system messages. If omitted
+ info/system messages as well as normal messages will be matched.
+ :param func: A Callable function that should accept the event as input
+ parameter, and return a bool value indicating whether the event
+ should be dispatched or not.
+ """
super().__init__(func=func)
self.is_bot = is_bot
self.is_info = is_info
@@ -159,6 +162,7 @@ def __eq__(self, other) -> bool:
return False
def filter(self, event: "AttrDict") -> bool:
+ """Return true if if the event is a new message event."""
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
return False
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
@@ -199,6 +203,7 @@ def __eq__(self, other) -> bool:
return False
def filter(self, event: "AttrDict") -> bool:
+ """Return true if if the event is a member addition event."""
if self.added is not None and self.added != event.member_added:
return False
return self._call_func(event)
@@ -231,6 +236,7 @@ def __eq__(self, other) -> bool:
return False
def filter(self, event: "AttrDict") -> bool:
+ """Return True if event is matched."""
if self.deleted is not None and self.deleted != event.image_deleted:
return False
return self._call_func(event)
@@ -256,13 +262,12 @@ def __eq__(self, other) -> bool:
return False
def filter(self, event: "AttrDict") -> bool:
+ """Return True if event is matched."""
return self._call_func(event)
class HookCollection:
- """
- Helper class to collect event hooks that can later be added to a Delta Chat client.
- """
+ """Helper class to collect event hooks that can later be added to a Delta Chat client."""
def __init__(self) -> None:
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py
index 6d1d68ac4f..bc35ffc62e 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py
@@ -1,3 +1,5 @@
+"""Message module."""
+
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
@@ -45,6 +47,7 @@ def get_reactions(self) -> Optional[AttrDict]:
return None
def get_sender_contact(self) -> Contact:
+ """Return sender contact."""
from_id = self.get_snapshot().from_id
return self.account.get_contact_by_id(from_id)
@@ -52,6 +55,14 @@ def mark_seen(self) -> None:
"""Mark the message as seen."""
self._rpc.markseen_msgs(self.account.id, [self.id])
+ def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
+ """Continue the Autocrypt Setup Message key transfer.
+
+ This function can be called on received Autocrypt Setup Message
+ to import the key encrypted with the provided setup code.
+ """
+ self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
+
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):
@@ -59,9 +70,15 @@ def send_webxdc_status_update(self, update: Union[dict, str], description: str)
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
+ """Return a list of Webxdc status updates for Webxdc instance message."""
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
+ def get_info(self) -> str:
+ """Return message info."""
+ return self._rpc.get_message_info(self.account.id, self.id)
+
def get_webxdc_info(self) -> dict:
+ """Get info from a Webxdc message in JSON format."""
return self._rpc.get_webxdc_info(self.account.id, self.id)
def wait_until_delivered(self) -> None:
@@ -73,8 +90,10 @@ def wait_until_delivered(self) -> None:
@futuremethod
def send_webxdc_realtime_advertisement(self):
+ """Send an advertisement to join the realtime channel."""
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
@futuremethod
def send_webxdc_realtime_data(self, data) -> None:
+ """Send data to the realtime channel."""
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py
index 63f42b1b3d..31577dbe9c 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py
@@ -1,9 +1,12 @@
+"""Pytest plugin module."""
+
from __future__ import annotations
import os
import random
from typing import AsyncGenerator, Optional
+import py
import pytest
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
@@ -11,55 +14,55 @@
from .rpc import Rpc
-def get_temp_credentials() -> dict:
- domain = os.getenv("CHATMAIL_DOMAIN")
- username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
- password = f"{username}${username}"
- addr = f"{username}@{domain}"
- return {"email": addr, "password": password}
-
-
class ACFactory:
+ """Test account factory."""
+
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
def get_unconfigured_account(self) -> Account:
+ """Create a new unconfigured account."""
account = self.deltachat.add_account()
account.set_config("verified_one_on_one_chats", "1")
return account
def get_unconfigured_bot(self) -> Bot:
+ """Create a new unconfigured bot."""
return Bot(self.get_unconfigured_account())
- def new_preconfigured_account(self) -> Account:
- """Make a new account with configuration options set, but configuration not started."""
- credentials = get_temp_credentials()
- account = self.get_unconfigured_account()
- account.set_config("addr", credentials["email"])
- account.set_config("mail_pw", credentials["password"])
- assert not account.is_configured()
- return account
+ def get_credentials(self) -> (str, str):
+ """Generate new credentials for chatmail account."""
+ domain = os.getenv("CHATMAIL_DOMAIN")
+ username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
+ return f"{username}@{domain}", f"{username}${username}"
@futuremethod
def new_configured_account(self):
- account = self.new_preconfigured_account()
- yield account.configure.future()
+ """Create a new configured account."""
+ addr, password = self.get_credentials()
+ account = self.get_unconfigured_account()
+ params = {"addr": addr, "password": password}
+ yield account.add_or_update_transport.future(params)
+
assert account.is_configured()
return account
def new_configured_bot(self) -> Bot:
- credentials = get_temp_credentials()
+ """Create a new configured bot."""
+ addr, password = self.get_credentials()
bot = self.get_unconfigured_bot()
- bot.configure(credentials["email"], credentials["password"])
+ bot.configure(addr, password)
return bot
@futuremethod
def get_online_account(self):
+ """Create a new account and start I/O."""
account = yield self.new_configured_account.future()
account.bring_online()
return account
def get_online_accounts(self, num: int) -> list[Account]:
+ """Create multiple online accounts."""
futures = [self.get_online_account.future() for _ in range(num)]
return [f() for f in futures]
@@ -74,6 +77,10 @@ def resetup_account(self, ac: Account) -> Account:
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
+ """Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
+
+ Returned chat is a chat with ac2 from ac1 point of view.
+ """
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
@@ -85,9 +92,10 @@ def send_message(
file: Optional[str] = None,
group: Optional[str] = None,
) -> Message:
+ """Send a message."""
if not from_account:
from_account = (self.get_online_accounts(1))[0]
- to_contact = from_account.create_contact(to_account.get_config("addr"))
+ to_contact = from_account.create_contact(to_account)
if group:
to_chat = from_account.create_group(group)
to_chat.add_contact(to_contact)
@@ -103,6 +111,7 @@ def process_message(
file: Optional[str] = None,
group: Optional[str] = None,
) -> AttrDict:
+ """Send a message and wait until recipient processes it."""
self.send_message(
to_account=to_client.account,
from_account=from_account,
@@ -116,6 +125,7 @@ def process_message(
@pytest.fixture
def rpc(tmp_path) -> AsyncGenerator:
+ """RPC client fixture."""
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
yield rpc_server
@@ -123,4 +133,52 @@ def rpc(tmp_path) -> AsyncGenerator:
@pytest.fixture
def acfactory(rpc) -> AsyncGenerator:
+ """Return account factory fixture."""
return ACFactory(DeltaChat(rpc))
+
+
+@pytest.fixture
+def data():
+ """Test data."""
+
+ class Data:
+ def __init__(self) -> None:
+ for path in reversed(py.path.local(__file__).parts()):
+ datadir = path.join("test-data")
+ if datadir.isdir():
+ self.path = datadir
+ return
+ raise Exception("Data path cannot be found")
+
+ def get_path(self, bn):
+ """Return path of file or None if it doesn't exist."""
+ fn = os.path.join(self.path, *bn.split("/"))
+ assert os.path.exists(fn)
+ return fn
+
+ def read_path(self, bn, mode="r"):
+ fn = self.get_path(bn)
+ if fn is not None:
+ with open(fn, mode) as f:
+ return f.read()
+ return None
+
+ return Data()
+
+
+@pytest.fixture
+def log():
+ """Log printer fixture."""
+
+ class Printer:
+ def section(self, msg: str) -> None:
+ print()
+ print("=" * 10, msg, "=" * 10)
+
+ def step(self, msg: str) -> None:
+ print("-" * 5, "step " + msg, "-" * 5)
+
+ def indent(self, msg: str) -> None:
+ print(" " + msg)
+
+ return Printer()
diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py
index dcf955ac8e..9ed192925a 100644
--- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py
+++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py
@@ -1,3 +1,5 @@
+"""JSON-RPC client module."""
+
from __future__ import annotations
import itertools
@@ -12,16 +14,19 @@
class JsonRpcError(Exception):
- pass
+ """JSON-RPC error."""
class RpcFuture:
+ """RPC future waiting for RPC call result."""
+
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
self.rpc = rpc
self.request_id = request_id
self.event = event
def __call__(self):
+ """Wait for the future to return the result."""
self.event.wait()
response = self.rpc.request_results.pop(self.request_id)
if "error" in response:
@@ -32,17 +37,19 @@ def __call__(self):
class RpcMethod:
+ """RPC method."""
+
def __init__(self, rpc: "Rpc", name: str):
self.rpc = rpc
self.name = name
def __call__(self, *args) -> Any:
- """Synchronously calls JSON-RPC method."""
+ """Call JSON-RPC method synchronously."""
future = self.future(*args)
return future()
def future(self, *args) -> Any:
- """Asynchronously calls JSON-RPC method."""
+ """Call JSON-RPC method asynchronously."""
request_id = next(self.rpc.id_iterator)
request = {
"jsonrpc": "2.0",
@@ -58,8 +65,13 @@ def future(self, *args) -> Any:
class Rpc:
+ """RPC client."""
+
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
- """The given arguments will be passed to subprocess.Popen()"""
+ """Initialize RPC client.
+
+ The given arguments will be passed to subprocess.Popen().
+ """
if accounts_dir:
kwargs["env"] = {
**kwargs.get("env", os.environ),
@@ -81,6 +93,7 @@ def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
self.events_thread: Thread
def start(self) -> None:
+ """Start RPC server subprocess."""
if sys.version_info >= (3, 11):
self.process = subprocess.Popen(
"deltachat-rpc-server",
@@ -130,6 +143,7 @@ def __exit__(self, _exc_type, _exc, _tb):
self.close()
def reader_loop(self) -> None:
+ """Process JSON-RPC responses from the RPC server process output."""
try:
while line := self.process.stdout.readline():
response = json.loads(line)
@@ -157,12 +171,13 @@ def writer_loop(self) -> None:
logging.exception("Exception in the writer loop")
def get_queue(self, account_id: int) -> Queue:
+ """Get event queue corresponding to the given account ID."""
if account_id not in self.event_queues:
self.event_queues[account_id] = Queue()
return self.event_queues[account_id]
def events_loop(self) -> None:
- """Requests new events and distributes them between queues."""
+ """Request new events and distributes them between queues."""
try:
while True:
if self.closing:
@@ -178,12 +193,12 @@ def events_loop(self) -> None:
logging.exception("Exception in the event loop")
def wait_for_event(self, account_id: int) -> Optional[dict]:
- """Waits for the next event from the given account and returns it."""
+ """Wait for the next event from the given account and returns it."""
queue = self.get_queue(account_id)
return queue.get()
def clear_all_events(self, account_id: int):
- """Removes all queued-up events for a given account. Useful for tests."""
+ """Remove all queued-up events for a given account. Useful for tests."""
queue = self.get_queue(account_id)
try:
while True:
diff --git a/deltachat-rpc-client/tests/test_account_events.py b/deltachat-rpc-client/tests/test_account_events.py
index 4c4d5c4473..c462f25f43 100644
--- a/deltachat-rpc-client/tests/test_account_events.py
+++ b/deltachat-rpc-client/tests/test_account_events.py
@@ -13,10 +13,11 @@ def test_event_on_configuration(acfactory: ACFactory) -> None:
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
"""
- account = acfactory.new_preconfigured_account()
+ addr, password = acfactory.get_credentials()
+ account = acfactory.get_unconfigured_account()
account.clear_all_events()
assert not account.is_configured()
- future = account.configure.future()
+ future = account.add_or_update_transport.future({"addr": addr, "password": password})
while True:
event = account.wait_for_event()
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
diff --git a/deltachat-rpc-client/tests/test_chatlist_events.py b/deltachat-rpc-client/tests/test_chatlist_events.py
index 981d4c8d7b..7c9b63466a 100644
--- a/deltachat-rpc-client/tests/test_chatlist_events.py
+++ b/deltachat-rpc-client/tests/test_chatlist_events.py
@@ -48,8 +48,7 @@ def test_delivery_status(acfactory: ACFactory) -> None:
"""
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice.clear_all_events()
@@ -119,8 +118,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
"""
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")
@@ -150,18 +148,13 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")
bob.wait_for_incoming_msg_event()
- alice_second_device: Account = acfactory.get_unconfigured_account()
-
- alice._rpc.provide_backup.future(alice.id)
- backup_code = alice._rpc.get_backup_qr(alice.id)
- alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
+ alice_second_device = alice.clone()
alice_second_device.start_io()
alice.clear_all_events()
alice_second_device.clear_all_events()
diff --git a/deltachat-rpc-client/tests/test_iroh_webxdc.py b/deltachat-rpc-client/tests/test_iroh_webxdc.py
index 30c24a72a3..c2433d2f76 100644
--- a/deltachat-rpc-client/tests/test_iroh_webxdc.py
+++ b/deltachat-rpc-client/tests/test_iroh_webxdc.py
@@ -175,17 +175,11 @@ def thread_run():
threading.Thread(target=thread_run, daemon=True).start()
- while 1:
- event = ac2.wait_for_event()
- if event.kind == EventType.WEBXDC_REALTIME_DATA:
- n = int(bytes(event.data).decode())
- break
+ event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
+ n = int(bytes(event.data).decode())
- while 1:
- event = ac2.wait_for_event()
- if event.kind == EventType.WEBXDC_REALTIME_DATA:
- assert int(bytes(event.data).decode()) > n
- break
+ event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
+ assert int(bytes(event.data).decode()) > n
def test_no_reordering(acfactory, path_to_webxdc):
@@ -229,8 +223,5 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
ac2_hello_msg_snapshot.chat.accept()
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
- while 1:
- event = ac1.wait_for_event()
- if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
- assert event.msg_id == ac1_webxdc_msg.id
- break
+ event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
+ assert event.msg_id == ac1_webxdc_msg.id
diff --git a/deltachat-rpc-client/tests/test_key_transfer.py b/deltachat-rpc-client/tests/test_key_transfer.py
new file mode 100644
index 0000000000..2fc2143fa4
--- /dev/null
+++ b/deltachat-rpc-client/tests/test_key_transfer.py
@@ -0,0 +1,53 @@
+import pytest
+
+from deltachat_rpc_client import EventType
+from deltachat_rpc_client.rpc import JsonRpcError
+
+
+def wait_for_autocrypt_setup_message(account):
+ while True:
+ event = account.wait_for_event()
+ if event.kind == EventType.MSGS_CHANGED and event.msg_id != 0:
+ msg_id = event.msg_id
+ msg = account.get_message_by_id(msg_id)
+ if msg.get_snapshot().is_setupmessage:
+ return msg
+
+
+def test_autocrypt_setup_message_key_transfer(acfactory):
+ alice1 = acfactory.get_online_account()
+
+ alice2 = acfactory.get_unconfigured_account()
+ alice2.set_config("addr", alice1.get_config("addr"))
+ alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
+ alice2.configure()
+ alice2.bring_online()
+
+ setup_code = alice1.initiate_autocrypt_key_transfer()
+ msg = wait_for_autocrypt_setup_message(alice2)
+
+ # Test that entering wrong code returns an error.
+ with pytest.raises(JsonRpcError):
+ msg.continue_autocrypt_key_transfer("7037-0673-6287-3013-4095-7956-5617-6806-6756")
+
+ msg.continue_autocrypt_key_transfer(setup_code)
+
+
+def test_ac_setup_message_twice(acfactory):
+ alice1 = acfactory.get_online_account()
+
+ alice2 = acfactory.get_unconfigured_account()
+ alice2.set_config("addr", alice1.get_config("addr"))
+ alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
+ alice2.configure()
+ alice2.bring_online()
+
+ # Send the first Autocrypt Setup Message and ignore it.
+ _setup_code = alice1.initiate_autocrypt_key_transfer()
+ wait_for_autocrypt_setup_message(alice2)
+
+ # Send the second Autocrypt Setup Message and import it.
+ setup_code = alice1.initiate_autocrypt_key_transfer()
+ msg = wait_for_autocrypt_setup_message(alice2)
+
+ msg.continue_autocrypt_key_transfer(setup_code)
diff --git a/deltachat-rpc-client/tests/test_multidevice.py b/deltachat-rpc-client/tests/test_multidevice.py
new file mode 100644
index 0000000000..663542aa08
--- /dev/null
+++ b/deltachat-rpc-client/tests/test_multidevice.py
@@ -0,0 +1,110 @@
+from imap_tools import AND
+
+from deltachat_rpc_client import EventType
+from deltachat_rpc_client.const import MessageState
+
+
+def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
+ ac1, ac2 = acfactory.get_online_accounts(2)
+ ac1_clone = ac1.clone()
+ ac1_clone.bring_online()
+
+ log.section("send out message without bcc to ourselves")
+ ac1.set_config("bcc_self", "0")
+ chat = ac1.create_chat(ac2)
+ self_addr = ac1.get_config("addr")
+ other_addr = ac2.get_config("addr")
+
+ msg_out = chat.send_text("message1")
+ assert not msg_out.get_snapshot().is_forwarded
+
+ # wait for send out (no BCC)
+ ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
+ assert ac1.get_config("bcc_self") == "0"
+
+ assert self_addr not in ev.msg
+ assert other_addr in ev.msg
+
+ log.section("ac1: setting bcc_self=1")
+ ac1.set_config("bcc_self", "1")
+
+ log.section("send out message with bcc to ourselves")
+ msg_out = chat.send_text("message2")
+
+ # wait for send out (BCC)
+ ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
+ assert ac1.get_config("bcc_self") == "1"
+
+ # Second client receives only second message, but not the first.
+ ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
+ assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
+
+ # now make sure we are sending message to ourselves too
+ assert self_addr in ev.msg
+ assert self_addr in ev.msg
+
+ # BCC-self messages are marked as seen by the sender device.
+ while True:
+ event = ac1.wait_for_event()
+ if event.kind == EventType.INFO and event.msg.endswith("Marked messages 1 in folder INBOX as seen."):
+ break
+
+ # Check that the message is marked as seen on IMAP.
+ ac1_direct_imap = direct_imap(ac1)
+ ac1_direct_imap.connect()
+ ac1_direct_imap.select_folder("Inbox")
+ assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1
+
+
+def test_multidevice_sync_seen(acfactory, log):
+ """Test that message marked as seen on one device is marked as seen on another."""
+ ac1, ac2 = acfactory.get_online_accounts(2)
+ ac1_clone = ac1.clone()
+ ac1_clone.bring_online()
+
+ ac1.set_config("bcc_self", "1")
+ ac1_clone.set_config("bcc_self", "1")
+
+ ac1_chat = ac1.create_chat(ac2)
+ ac1_clone_chat = ac1_clone.create_chat(ac2)
+ ac2_chat = ac2.create_chat(ac1)
+
+ log.section("Send a message from ac2 to ac1 and check that it's 'fresh'")
+ ac2_chat.send_text("Hi")
+ ac1_message = ac1.wait_for_incoming_msg()
+ ac1_clone_message = ac1_clone.wait_for_incoming_msg()
+ assert ac1_chat.get_fresh_message_count() == 1
+ assert ac1_clone_chat.get_fresh_message_count() == 1
+ assert ac1_message.get_snapshot().state == MessageState.IN_FRESH
+ assert ac1_clone_message.get_snapshot().state == MessageState.IN_FRESH
+
+ log.section("ac1 marks message as seen on the first device")
+ ac1.mark_seen_messages([ac1_message])
+ assert ac1_message.get_snapshot().state == MessageState.IN_SEEN
+
+ log.section("ac1 clone detects that message is marked as seen")
+ ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
+ assert ev.chat_id == ac1_clone_chat.id
+
+ log.section("Send an ephemeral message from ac2 to ac1")
+ ac2_chat.set_ephemeral_timer(60)
+ ac1.wait_for_event(EventType.CHAT_EPHEMERAL_TIMER_MODIFIED)
+ ac1.wait_for_incoming_msg()
+ ac1_clone.wait_for_event(EventType.CHAT_EPHEMERAL_TIMER_MODIFIED)
+ ac1_clone.wait_for_incoming_msg()
+
+ ac2_chat.send_text("Foobar")
+ ac1_message = ac1.wait_for_incoming_msg()
+ ac1_clone_message = ac1_clone.wait_for_incoming_msg()
+ assert "Ephemeral timer: 60\n" in ac1_message.get_info()
+ assert "Expires: " not in ac1_clone_message.get_info()
+ assert "Ephemeral timer: 60\n" in ac1_message.get_info()
+ assert "Expires: " not in ac1_clone_message.get_info()
+
+ ac1_message.mark_seen()
+ assert "Expires: " in ac1_message.get_info()
+ ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
+ assert ev.chat_id == ac1_clone_chat.id
+ assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
+ # Test that the timer is started on the second device after synchronizing the seen status.
+ assert "Expires: " in ac1_clone_message.get_info()
diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py
index 17b17ec1e4..03ade4d04e 100644
--- a/deltachat-rpc-client/tests/test_securejoin.py
+++ b/deltachat-rpc-client/tests/test_securejoin.py
@@ -4,6 +4,7 @@
import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
+from deltachat_rpc_client.rpc import JsonRpcError
def test_qr_setup_contact(acfactory, tmp_path) -> None:
@@ -26,17 +27,21 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
- # Test that if Bob changes the key, backwards verification is lost.
+ # Test that if Bob imports a key,
+ # backwards verification is not lost
+ # because default key is not changed.
logging.info("Bob 2 is created")
bob2 = acfactory.new_configured_account()
bob2.export_self_keys(tmp_path)
- logging.info("Bob imports a key")
- bob.import_self_keys(tmp_path)
+ logging.info("Bob tries to import a key")
+ # Importing a second key is not allowed.
+ with pytest.raises(JsonRpcError):
+ bob.import_self_keys(tmp_path)
- assert bob.get_config("key_id") == "2"
+ assert bob.get_config("key_id") == "1"
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
- assert not bob_contact_alice_snapshot.is_verified
+ assert bob_contact_alice_snapshot.is_verified
def test_qr_setup_contact_svg(acfactory) -> None:
@@ -55,15 +60,12 @@ def test_qr_setup_contact_svg(acfactory) -> None:
@pytest.mark.parametrize("protect", [True, False])
-def test_qr_securejoin(acfactory, protect, tmp_path):
+def test_qr_securejoin(acfactory, protect):
alice, bob, fiona = acfactory.get_online_accounts(3)
# Setup second device for Alice
# to test observing securejoin protocol.
- alice.export_backup(tmp_path)
- files = list(tmp_path.glob("*.tar"))
- alice2 = acfactory.get_unconfigured_account()
- alice2.import_backup(files[0])
+ alice2 = alice.clone()
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group", protect=protect)
@@ -74,17 +76,11 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
bob.secure_join(qr_code)
# Alice deletes "vg-request".
- while True:
- event = alice.wait_for_event()
- if event["kind"] == "ImapMessageDeleted":
- break
+ alice.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
alice.wait_for_securejoin_inviter_success()
# Bob deletes "vg-auth-required", Alice deletes "vg-request-with-auth".
for ac in [alice, bob]:
- while True:
- event = ac.wait_for_event()
- if event["kind"] == "ImapMessageDeleted":
- break
+ ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
bob.wait_for_securejoin_joiner_success()
# Test that Alice verified Bob's profile.
@@ -93,7 +89,7 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
assert alice_contact_bob_snapshot.is_verified
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
- assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
+ assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected == protect
# Test that Bob verified Alice's profile.
@@ -121,8 +117,7 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -159,11 +154,8 @@ def test_qr_readreceipt(acfactory) -> None:
logging.info("Alice creates a verified group")
group = alice.create_group("Group", protect=True)
- bob_addr = bob.get_config("addr")
- charlie_addr = charlie.get_config("addr")
-
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
- alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
+ alice_contact_charlie = alice.create_contact(charlie, "Charlie")
group.add_contact(alice_contact_bob)
group.add_contact(alice_contact_charlie)
@@ -190,7 +182,7 @@ def test_qr_readreceipt(acfactory) -> None:
charlie_snapshot = charlie_message.get_snapshot()
assert charlie_snapshot.text == "Hi from Bob!"
- bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
+ bob_contact_charlie = bob.create_contact(charlie, "Charlie")
assert not bob.get_chat_by_contact(bob_contact_charlie)
logging.info("Charlie reads Bob's message")
@@ -461,12 +453,12 @@ def test_qr_new_group_unblocked(acfactory):
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
+@pytest.mark.skip(reason="AEAP is disabled for now")
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2 = acfactory.get_online_accounts(2)
- # ac1new is only used to get a new address.
- ac1new = acfactory.new_preconfigured_account()
+ addr, password = acfactory.get_credentials()
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
@@ -486,8 +478,8 @@ def test_aeap_flow_verified(acfactory):
assert msg_in_1.text == msg_out.text
logging.info("changing email account")
- ac1.set_config("addr", ac1new.get_config("addr"))
- ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
+ ac1.set_config("addr", addr)
+ ac1.set_config("mail_pw", password)
ac1.stop_io()
ac1.configure()
ac1.start_io()
@@ -500,11 +492,9 @@ def test_aeap_flow_verified(acfactory):
msg_in_2_snapshot = msg_in_2.get_snapshot()
assert msg_in_2_snapshot.text == msg_out.text
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
- assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
+ assert msg_in_2.get_sender_contact().get_snapshot().address == addr
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
- assert ac1new.get_config("addr") in [
- contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
- ]
+ assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
def test_gossip_verification(acfactory) -> None:
@@ -520,9 +510,9 @@ def test_gossip_verification(acfactory) -> None:
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
- bob_contact_alice = bob.create_contact(alice.get_config("addr"), "Alice")
- bob_contact_carol = bob.create_contact(carol.get_config("addr"), "Carol")
- carol_contact_alice = carol.create_contact(alice.get_config("addr"), "Alice")
+ bob_contact_alice = bob.create_contact(alice, "Alice")
+ bob_contact_carol = bob.create_contact(carol, "Carol")
+ carol_contact_alice = carol.create_contact(alice, "Alice")
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
@@ -573,7 +563,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 waits for member added message and creates a QR code.
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
- assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
+ assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
ac1_qr_code = snapshot.chat.get_qr_code()
# ac2 verifies ac1
@@ -582,7 +572,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
ac2.wait_for_securejoin_joiner_success()
# ac1 is verified for ac2.
- ac2_contact_ac1 = ac2.create_contact(ac1.get_config("addr"), "")
+ ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert ac2_contact_ac1.get_snapshot().is_verified
# ac1 resetups the account.
@@ -597,7 +587,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# header sent by old ac1.
while True:
# ac1 sends a message to ac2.
- ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
+ ac1_contact_ac2 = ac1.create_contact(ac2, "")
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
ac1_chat_ac2.send_text("Hello!")
@@ -653,12 +643,14 @@ def test_withdraw_securejoin_qr(acfactory):
bob_chat = bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
+ alice.clear_all_events()
+
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
- assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
+ assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
bob_chat.leave()
- snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
+ snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
logging.info("Alice withdraws QR code.")
diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py
index f91ce90761..d859140f71 100644
--- a/deltachat-rpc-client/tests/test_something.py
+++ b/deltachat-rpc-client/tests/test_something.py
@@ -61,61 +61,102 @@ def test_acfactory(acfactory) -> None:
def test_configure_starttls(acfactory) -> None:
- account = acfactory.new_preconfigured_account()
+ addr, password = acfactory.get_credentials()
+ account = acfactory.get_unconfigured_account()
+ account.add_or_update_transport(
+ {
+ "addr": addr,
+ "password": password,
+ "imapSecurity": "starttls",
+ "smtpSecurity": "starttls",
+ },
+ )
+ assert account.is_configured()
+
- # Use STARTTLS
- account.set_config("mail_security", "2")
- account.set_config("send_security", "2")
- account.configure()
+def test_lowercase_address(acfactory) -> None:
+ addr, password = acfactory.get_credentials()
+ addr_upper = addr.upper()
+ account = acfactory.get_unconfigured_account()
+ account.add_or_update_transport(
+ {
+ "addr": addr_upper,
+ "password": password,
+ },
+ )
assert account.is_configured()
+ assert addr_upper != addr
+ assert account.get_config("configured_addr") == addr
+ assert account.list_transports()[0]["addr"] == addr
+ for param in [
+ account.get_info()["used_account_settings"],
+ account.get_info()["entered_account_settings"],
+ ]:
+ assert addr in param
+ assert addr_upper not in param
-def test_configure_ip(acfactory) -> None:
- account = acfactory.new_preconfigured_account()
- domain = account.get_config("addr").rsplit("@")[-1]
- ip_address = socket.gethostbyname(domain)
+def test_configure_ip(acfactory) -> None:
+ addr, password = acfactory.get_credentials()
+ account = acfactory.get_unconfigured_account()
+ ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
- # This should fail TLS check.
- account.set_config("mail_server", ip_address)
with pytest.raises(JsonRpcError):
- account.configure()
+ account.add_or_update_transport(
+ {
+ "addr": addr,
+ "password": password,
+ # This should fail TLS check.
+ "imapServer": ip_address,
+ },
+ )
def test_configure_alternative_port(acfactory) -> None:
"""Test that configuration with alternative port 443 works."""
- account = acfactory.new_preconfigured_account()
-
- account.set_config("mail_port", "443")
- account.set_config("send_port", "443")
-
- account.configure()
-
-
-def test_configure_username(acfactory) -> None:
- account = acfactory.new_preconfigured_account()
+ addr, password = acfactory.get_credentials()
+ account = acfactory.get_unconfigured_account()
+ account.add_or_update_transport(
+ {
+ "addr": addr,
+ "password": password,
+ "imapPort": 443,
+ "smtpPort": 443,
+ },
+ )
+ assert account.is_configured()
- addr = account.get_config("addr")
- account.set_config("mail_user", addr)
- account.configure()
- assert account.get_config("configured_mail_user") == addr
+def test_list_transports(acfactory) -> None:
+ addr, password = acfactory.get_credentials()
+ account = acfactory.get_unconfigured_account()
+ account.add_or_update_transport(
+ {
+ "addr": addr,
+ "password": password,
+ "imapUser": addr,
+ },
+ )
+ transports = account.list_transports()
+ assert len(transports) == 1
+ params = transports[0]
+ assert params["addr"] == addr
+ assert params["password"] == password
+ assert params["imapUser"] == addr
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
- while True:
- event = bob.wait_for_event()
- if event.kind == EventType.INCOMING_MSG:
- chat_id = event.chat_id
- msg_id = event.msg_id
- break
+ event = bob.wait_for_incoming_msg_event()
+ chat_id = event.chat_id
+ msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
@@ -174,8 +215,7 @@ def test_account(acfactory) -> None:
def test_chat(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -241,7 +281,7 @@ def test_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
assert repr(alice_contact_bob)
@@ -258,8 +298,7 @@ def test_contact(acfactory) -> None:
def test_message(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -287,28 +326,74 @@ def test_message(acfactory) -> None:
assert reactions == snapshot.reactions
+def test_selfavatar_sync(acfactory, data, log) -> None:
+ alice = acfactory.get_online_account()
+
+ log.section("Alice adds a second device")
+ alice2 = alice.clone()
+
+ log.section("Second device goes online")
+ alice2.start_io()
+
+ log.section("First device changes avatar")
+ image = data.get_path("image/avatar1000x1000.jpg")
+ alice.set_config("selfavatar", image)
+ avatar_config = alice.get_config("selfavatar")
+ avatar_hash = os.path.basename(avatar_config)
+ print("Info: avatar hash is ", avatar_hash)
+
+ log.section("First device receives avatar change")
+ alice2.wait_for_event(EventType.SELFAVATAR_CHANGED)
+ avatar_config2 = alice2.get_config("selfavatar")
+ avatar_hash2 = os.path.basename(avatar_config2)
+ print("Info: avatar hash on second device is ", avatar_hash2)
+ assert avatar_hash == avatar_hash2
+ assert avatar_config != avatar_config2
+
+
+def test_reaction_seen_on_another_dev(acfactory) -> None:
+ alice, bob = acfactory.get_online_accounts(2)
+ alice2 = alice.clone()
+ alice2.start_io()
+
+ alice_contact_bob = alice.create_contact(bob, "Bob")
+ alice_chat_bob = alice_contact_bob.create_chat()
+ alice_chat_bob.send_text("Hello!")
+
+ event = bob.wait_for_incoming_msg_event()
+ msg_id = event.msg_id
+
+ message = bob.get_message_by_id(msg_id)
+ snapshot = message.get_snapshot()
+ snapshot.chat.accept()
+ message.send_reaction("😎")
+ for a in [alice, alice2]:
+ a.wait_for_event(EventType.INCOMING_REACTION)
+
+ alice2.clear_all_events()
+ alice_chat_bob.mark_noticed()
+ chat_id = alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id
+ alice2_chat_bob = alice2.create_chat(bob)
+ assert chat_id == alice2_chat_bob.id
+
+
def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
# Alice becomes a bot.
alice.set_config("bot", "1")
alice_chat_bob.send_text("Hello!")
- while True:
- event = bob.wait_for_event()
- if event.kind == EventType.INCOMING_MSG:
- msg_id = event.msg_id
- message = bob.get_message_by_id(msg_id)
- snapshot = message.get_snapshot()
- assert snapshot.chat_id == event.chat_id
- assert snapshot.text == "Hello!"
- assert snapshot.is_bot
- break
+ event = bob.wait_for_incoming_msg_event()
+ message = bob.get_message_by_id(event.msg_id)
+ snapshot = message.get_snapshot()
+ assert snapshot.chat_id == event.chat_id
+ assert snapshot.text == "Hello!"
+ assert snapshot.is_bot
def test_bot(acfactory) -> None:
@@ -355,9 +440,11 @@ def test_wait_next_messages(acfactory) -> None:
alice = acfactory.new_configured_account()
# Create a bot account so it does not receive device messages in the beginning.
- bot = acfactory.new_preconfigured_account()
+ addr, password = acfactory.get_credentials()
+ bot = acfactory.get_unconfigured_account()
bot.set_config("bot", "1")
- bot.configure()
+ bot.add_or_update_transport({"addr": addr, "password": password})
+ assert bot.is_configured()
# There are no old messages and the call returns immediately.
assert not bot.wait_next_messages()
@@ -366,8 +453,7 @@ def test_wait_next_messages(acfactory) -> None:
# Bot starts waiting for messages.
next_messages_task = executor.submit(bot.wait_next_messages)
- bot_addr = bot.get_config("addr")
- alice_contact_bot = alice.create_contact(bot_addr, "Bot")
+ alice_contact_bot = alice.create_contact(bot, "Bot")
alice_chat_bot = alice_contact_bot.create_chat()
alice_chat_bot.send_text("Hello!")
@@ -391,9 +477,7 @@ def test_import_export_backup(acfactory, tmp_path) -> None:
def test_import_export_keys(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
- alice_chat_bob = alice_contact_bob.create_chat()
+ alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello Bob!")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
@@ -443,9 +527,7 @@ def test_provider_info(rpc) -> None:
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
-
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
# Bob creates chat manually so chat with Alice is accepted.
alice_chat_bob = alice_contact_bob.create_chat()
@@ -469,10 +551,7 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
# Alice reads Bob's message.
message.mark_seen()
- while True:
- event = bob.wait_for_event()
- if event.kind == EventType.MSG_READ:
- break
+ bob.wait_for_event(EventType.MSG_READ)
# Bob sends a message to Alice, it should also be encrypted.
bob_chat_alice.send_text("Hi Alice!")
@@ -544,9 +623,13 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
- ac2 = acfactory.new_preconfigured_account()
- ac2.configure()
+
+ addr, password = acfactory.get_credentials()
+ ac2 = acfactory.get_unconfigured_account()
+ ac2.add_or_update_transport({"addr": addr, "password": password})
ac2.set_config("mvbox_move", "1")
+ assert ac2.is_configured()
+
ac2.bring_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
@@ -590,9 +673,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
chat.send_text("Hello Alice!")
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
- contact_addr = account.get_config("addr")
- contact = alice.create_contact(contact_addr, "")
-
+ contact = alice.create_contact(account)
alice_group.add_contact(contact)
if n_accounts == 2:
@@ -623,7 +704,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
assert snapshot.chat == bob_chat_alice
-def test_markseen_contact_request(acfactory, tmp_path):
+def test_markseen_contact_request(acfactory):
"""
Test that seen status is synchronized for contact request messages
even though read receipt is not sent.
@@ -631,10 +712,7 @@ def test_markseen_contact_request(acfactory, tmp_path):
alice, bob = acfactory.get_online_accounts(2)
# Bob sets up a second device.
- bob.export_backup(tmp_path)
- files = list(tmp_path.glob("*.tar"))
- bob2 = acfactory.get_unconfigured_account()
- bob2.import_backup(files[0])
+ bob2 = bob.clone()
bob2.start_io()
alice_chat_bob = alice.create_chat(bob)
@@ -645,10 +723,7 @@ def test_markseen_contact_request(acfactory, tmp_path):
assert message2.get_snapshot().state == MessageState.IN_FRESH
message.mark_seen()
- while True:
- event = bob2.wait_for_event()
- if event.kind == EventType.MSGS_NOTICED:
- break
+ bob2.wait_for_event(EventType.MSGS_NOTICED)
assert message2.get_snapshot().state == MessageState.IN_SEEN
@@ -661,12 +736,11 @@ def test_get_http_response(acfactory):
def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
- configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
# Certificate checks should be configured (not None)
- assert configured_certificate_checks
+ assert "cert_automatic" in alice.get_info().used_account_settings
- # 0 is the value old Delta Chat core versions used
+ # "cert_old_automatic" is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic)
# and configuration failed to use strict TLS checks
# so it switched strict TLS checks off.
@@ -677,4 +751,99 @@ def test_configured_imap_certificate_checks(acfactory):
#
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
# This test is a regression test to prevent this happening again.
- assert configured_certificate_checks != "0"
+ assert "cert_old_automatic" not in alice.get_info().used_account_settings
+
+
+def test_no_old_msg_is_fresh(acfactory):
+ ac1, ac2 = acfactory.get_online_accounts(2)
+ ac1_clone = ac1.clone()
+ ac1_clone.start_io()
+
+ ac1.create_chat(ac2)
+ ac1_clone_chat = ac1_clone.create_chat(ac2)
+
+ ac1.get_device_chat().mark_noticed()
+
+ logging.info("Send a first message from ac2 to ac1 and check that it's 'fresh'")
+ first_msg = ac2.create_chat(ac1).send_text("Hi")
+ ac1.wait_for_incoming_msg_event()
+ assert ac1.create_chat(ac2).get_fresh_message_count() == 1
+ assert len(list(ac1.get_fresh_messages())) == 1
+
+ ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
+
+ logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
+ ac1_clone_chat.send_text("Hi back")
+ ev = ac1.wait_for_msgs_noticed_event()
+
+ assert ev.chat_id == first_msg.get_snapshot().chat_id
+ assert ac1.create_chat(ac2).get_fresh_message_count() == 0
+ assert len(list(ac1.get_fresh_messages())) == 0
+
+
+def test_rename_synchronization(acfactory):
+ """Test synchronization of contact renaming."""
+ alice, bob = acfactory.get_online_accounts(2)
+ alice2 = alice.clone()
+ alice2.bring_online()
+
+ bob.set_config("displayname", "Bob")
+ bob.create_chat(alice).send_text("Hello!")
+ alice_msg = alice.wait_for_incoming_msg().get_snapshot()
+ alice2_msg = alice2.wait_for_incoming_msg().get_snapshot()
+
+ assert alice2_msg.sender.get_snapshot().display_name == "Bob"
+ alice_msg.sender.set_name("Bobby")
+ alice2.wait_for_event(EventType.CONTACTS_CHANGED)
+ assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
+
+
+def test_rename_group(acfactory):
+ """Test renaming the group."""
+ alice, bob = acfactory.get_online_accounts(2)
+
+ alice_group = alice.create_group("Test group")
+ alice_contact_bob = alice.create_contact(bob)
+ alice_group.add_contact(alice_contact_bob)
+ alice_group.send_text("Hello!")
+
+ bob_msg = bob.wait_for_incoming_msg()
+ bob_chat = bob_msg.get_snapshot().chat
+ assert bob_chat.get_basic_snapshot().name == "Test group"
+
+ for name in ["Baz", "Foo bar", "Xyzzy"]:
+ alice_group.set_name(name)
+ bob.wait_for_incoming_msg_event()
+ assert bob_chat.get_basic_snapshot().name == name
+
+
+def test_get_all_accounts_deadlock(rpc):
+ """Regression test for get_all_accounts deadlock."""
+ for _ in range(100):
+ all_accounts = rpc.get_all_accounts.future()
+ rpc.add_account()
+ all_accounts()
+
+
+def test_delete_deltachat_folder(acfactory, direct_imap):
+ """Test that DeltaChat folder is recreated if user deletes it manually."""
+ ac1 = acfactory.new_configured_account()
+ ac1.set_config("mvbox_move", "1")
+ ac1.bring_online()
+
+ ac1_direct_imap = direct_imap(ac1)
+ ac1_direct_imap.conn.folder.delete("DeltaChat")
+ assert "DeltaChat" not in ac1_direct_imap.list_folders()
+
+ # Wait until new folder is created and UIDVALIDITY is updated.
+ while True:
+ event = ac1.wait_for_event()
+ if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
+ break
+
+ ac2 = acfactory.get_online_account()
+ ac2.create_chat(ac1).send_text("hello")
+ msg = ac1.wait_for_incoming_msg().get_snapshot()
+ assert msg.text == "hello"
+
+ assert "DeltaChat" in ac1_direct_imap.list_folders()
diff --git a/deltachat-rpc-client/tests/test_vcard.py b/deltachat-rpc-client/tests/test_vcard.py
index cb4d954021..53e8348b33 100644
--- a/deltachat-rpc-client/tests/test_vcard.py
+++ b/deltachat-rpc-client/tests/test_vcard.py
@@ -1,8 +1,7 @@
def test_vcard(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
alice_chat_bob = alice_contact_bob.create_chat()
diff --git a/deltachat-rpc-client/tests/test_webxdc.py b/deltachat-rpc-client/tests/test_webxdc.py
index e40d169716..314e8f6747 100644
--- a/deltachat-rpc-client/tests/test_webxdc.py
+++ b/deltachat-rpc-client/tests/test_webxdc.py
@@ -1,20 +1,13 @@
-from deltachat_rpc_client import EventType
-
-
def test_webxdc(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
- while True:
- event = bob.wait_for_event()
- if event.kind == EventType.INCOMING_MSG:
- bob_chat_alice = bob.get_chat_by_id(event.chat_id)
- message = bob.get_message_by_id(event.msg_id)
- break
+ event = bob.wait_for_incoming_msg_event()
+ bob_chat_alice = bob.get_chat_by_id(event.chat_id)
+ message = bob.get_message_by_id(event.msg_id)
webxdc_info = message.get_webxdc_info()
assert webxdc_info == {
@@ -51,8 +44,7 @@ def test_webxdc(acfactory) -> None:
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
- bob_addr = bob.get_config("addr")
- alice_contact_bob = alice.create_contact(bob_addr, "Bob")
+ alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
diff --git a/deltachat-rpc-client/tox.ini b/deltachat-rpc-client/tox.ini
index 2c3b0cb4ef..2ad52b8f52 100644
--- a/deltachat-rpc-client/tox.ini
+++ b/deltachat-rpc-client/tox.ini
@@ -12,11 +12,8 @@ setenv =
RUST_MIN_STACK=8388608
passenv =
CHATMAIL_DOMAIN
-deps =
- pytest
- pytest-timeout
- pytest-xdist
- imap-tools
+dependency_groups =
+ dev
[testenv:lint]
skipsdist = True
@@ -24,7 +21,7 @@ skip_install = True
deps =
ruff
commands =
- ruff format --quiet --diff src/ examples/ tests/
+ ruff format --diff src/ examples/ tests/
ruff check src/ examples/ tests/
[pytest]
diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml
index abae5611e7..f5678301cd 100644
--- a/deltachat-rpc-server/Cargo.toml
+++ b/deltachat-rpc-server/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
-version = "1.155.2"
+version = "1.159.5"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
diff --git a/deltachat-rpc-server/README.md b/deltachat-rpc-server/README.md
index 32ea83ac7b..a5108c4c8d 100644
--- a/deltachat-rpc-server/README.md
+++ b/deltachat-rpc-server/README.md
@@ -5,13 +5,13 @@ over standard I/O.
## Install
-To download binary pre-builds check the [releases page](https://github.com/deltachat/deltachat-core-rust/releases).
+To download binary pre-builds check the [releases page](https://github.com/chatmail/core/releases).
Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`.
To install from source run:
```sh
-cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server
+cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server
```
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available
diff --git a/deltachat-rpc-server/npm-package/package.json b/deltachat-rpc-server/npm-package/package.json
index 0b664671f1..0ea58135d6 100644
--- a/deltachat-rpc-server/npm-package/package.json
+++ b/deltachat-rpc-server/npm-package/package.json
@@ -8,12 +8,12 @@
},
"repository": {
"type": "git",
- "url": "https://github.com/deltachat/deltachat-core-rust.git"
+ "url": "https://github.com/chatmail/core.git"
},
"scripts": {
"prepack": "node scripts/update_optional_dependencies_and_version.js"
},
"type": "module",
"types": "index.d.ts",
- "version": "1.155.2"
+ "version": "1.159.5"
}
diff --git a/deltachat-rpc-server/npm-package/scripts/src/make_package.py b/deltachat-rpc-server/npm-package/scripts/src/make_package.py
index 0e9cc528a1..6400d04673 100644
--- a/deltachat-rpc-server/npm-package/scripts/src/make_package.py
+++ b/deltachat-rpc-server/npm-package/scripts/src/make_package.py
@@ -25,7 +25,7 @@ def write_package_json(platform_path, rust_target, my_binary_name):
"license": "MPL-2.0",
"repository": {
"type": "git",
- "url": "https://github.com/deltachat/deltachat-core-rust.git",
+ "url": "https://github.com/chatmail/core.git",
},
}
diff --git a/deltachat-rpc-server/npm-package/src/errors.js b/deltachat-rpc-server/npm-package/src/errors.js
index 52d8f115aa..92f23c1946 100644
--- a/deltachat-rpc-server/npm-package/src/errors.js
+++ b/deltachat-rpc-server/npm-package/src/errors.js
@@ -2,7 +2,7 @@
import { ENV_VAR_NAME } from "./const.js";
const cargoInstallCommand =
- "cargo install --git https://github.com/deltachat/deltachat-core-rust deltachat-rpc-server";
+ "cargo install --git https://github.com/chatmail/core deltachat-rpc-server";
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
return `deltachat-rpc-server not found:
diff --git a/deltachat-rpc-server/src/main.rs b/deltachat-rpc-server/src/main.rs
index 171edd3c11..66f9da93a4 100644
--- a/deltachat-rpc-server/src/main.rs
+++ b/deltachat-rpc-server/src/main.rs
@@ -30,7 +30,7 @@ async fn main() {
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
// until the user presses enter."
if let Err(error) = &r {
- log::error!("Fatal error: {error:#}.")
+ log::error!("Error: {error:#}.")
}
std::process::exit(if r.is_ok() { 0 } else { 1 });
}
diff --git a/deny.toml b/deny.toml
index 7f5e765503..5910ebc5f1 100644
--- a/deny.toml
+++ b/deny.toml
@@ -1,7 +1,5 @@
[advisories]
ignore = [
- "RUSTSEC-2020-0071",
-
# Timing attack on RSA.
# Delta Chat does not use RSA for new keys
# and this requires precise measurement of the decryption time by the attacker.
@@ -9,11 +7,11 @@ ignore = [
#
"RUSTSEC-2023-0071",
- # Unmaintained encoding
- "RUSTSEC-2021-0153",
-
# Unmaintained instant
"RUSTSEC-2024-0384",
+
+ # Unmaintained paste
+ "RUSTSEC-2024-0436",
]
[bans]
@@ -23,39 +21,31 @@ ignore = [
# Please keep this list alphabetically sorted.
skip = [
{ name = "async-channel", version = "1.9.0" },
- { name = "base64", version = "<0.21" },
- { name = "base64", version = "0.21.7" },
{ name = "bitflags", version = "1.3.2" },
{ name = "event-listener", version = "2.5.3" },
- { name = "fastrand", version = "1.9.0" },
- { name = "fiat-crypto", version = "0.1.20" },
- { name = "futures-lite", version = "1.13.0" },
{ name = "generator", version = "0.7.5" },
- { name = "getrandom", version = "<0.2" },
- { name = "hostname", version = "0.3.1" },
+ { name = "getrandom", version = "0.2.12" },
+ { name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
- { name = "iroh-metrics", version = "0.30.0" },
+ { name = "linux-raw-sys", version = "0.4.14" },
{ name = "loom", version = "0.5.6" },
{ name = "netlink-packet-route", version = "0.17.1" },
- { name = "netlink-packet-route", version = "0.21.0" },
- { name = "netwatch" },
- { name = "nix", version = "0.26.4" },
- { name = "nix", version = "0.27.1" },
- { name = "quick-error", version = "<2.0" },
- { name = "rand_chacha", version = "<0.3" },
- { name = "rand_core", version = "<0.6" },
- { name = "rand", version = "<0.8" },
+ { name = "nom", version = "7.1.3" },
+ { name = "rand_chacha", version = "0.3.1" },
+ { name = "rand_core", version = "0.6.4" },
+ { name = "rand", version = "0.8.5" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
- { name = "rtnetlink", version = "0.13.1" },
- { name = "sync_wrapper", version = "0.1.2" },
+ { name = "rustix", version = "0.38.44" },
+ { name = "spin", version = "0.9.8" },
+ { name = "strum_macros", version = "0.26.2" },
+ { name = "strum", version = "0.26.2" },
{ name = "syn", version = "1.0.109" },
+ { name = "lru", version = "0.12.3" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
- { name = "time", version = "<0.3" },
- { name = "unicode-width", version = "0.1.11" },
- { name = "wasi", version = "<0.11" },
+ { name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },
{ name = "windows_aarch64_msvc" },
@@ -72,6 +62,7 @@ skip = [
{ name = "windows_x86_64_gnu" },
{ name = "windows_x86_64_gnullvm" },
{ name = "windows_x86_64_msvc" },
+ { name = "zerocopy", version = "0.7.32" },
]
@@ -86,9 +77,9 @@ allow = [
"ISC",
"MIT",
"MPL-2.0",
- "OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
+ "Unlicense",
"Zlib",
]
@@ -98,9 +89,3 @@ expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
-
-[sources.allow-org]
-# Organisations which we allow git sources from.
-github = [
- "deltachat",
-]
diff --git a/flake.lock b/flake.lock
index feb520342a..e78a0e19b2 100644
--- a/flake.lock
+++ b/flake.lock
@@ -47,11 +47,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
- "lastModified": 1737527504,
- "narHash": "sha256-Z8S5gLPdIYeKwBXDaSxlJ72ZmiilYhu3418h3RSQZA0=",
+ "lastModified": 1747291057,
+ "narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
"owner": "nix-community",
"repo": "fenix",
- "rev": "aa13f23e3e91b95377a693ac655bbc6545ebec0d",
+ "rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
"type": "github"
},
"original": {
@@ -147,11 +147,11 @@
},
"nixpkgs_2": {
"locked": {
- "lastModified": 1737469691,
- "narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
+ "lastModified": 1747179050,
+ "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
+ "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"type": "github"
},
"original": {
@@ -175,11 +175,11 @@
},
"nixpkgs_4": {
"locked": {
- "lastModified": 1731139594,
- "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
+ "lastModified": 1747179050,
+ "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
+ "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"type": "github"
},
"original": {
@@ -202,11 +202,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
- "lastModified": 1737453499,
- "narHash": "sha256-fa5AJI9mjFU2oVXqdCq2oA2pripAXbHzkUkewJRQpxA=",
+ "lastModified": 1746889290,
+ "narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
"owner": "rust-lang",
"repo": "rust-analyzer",
- "rev": "0b68402d781955d526b80e5d479e9e47addb4075",
+ "rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index e5d232f303..527c463a65 100644
--- a/flake.nix
+++ b/flake.nix
@@ -18,9 +18,9 @@
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
- inherit (sdkPkgs) ndk-27-0-11902837 cmdline-tools-latest;
+ inherit (sdkPkgs) ndk-27-2-12479018 cmdline-tools-latest;
});
- androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.0.11902837";
+ androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.2.12479018";
rustSrc = nix-filter.lib {
root = ./.;
@@ -30,6 +30,7 @@
include = [
./benches
./assets
+ ./fuzz
./Cargo.lock
./Cargo.toml
./CMakeLists.txt
@@ -87,10 +88,6 @@
};
cargoLock = {
lockFile = ./Cargo.lock;
- outputHashes = {
- "email-0.0.20" = "sha256-cfR3D5jFQpw32bGsgapK2Uwuxmht+rRK/n1ZUmCb2WA=";
- "lettre-0.9.2" = "sha256-+hU1cFacyyeC9UGVBpS14BWlJjHy90i/3ynMkKAzclk=";
- };
};
mkRustPackage = packageName:
naersk'.buildPackage {
@@ -312,10 +309,41 @@
LD = "${targetCc}";
};
- mkAndroidPackages = arch: {
- "deltachat-rpc-server-${arch}-android" = mkAndroidRustPackage arch "deltachat-rpc-server";
- "deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
- };
+ mkAndroidPackages = arch:
+ let
+ rpc-server = mkAndroidRustPackage arch "deltachat-rpc-server";
+ in
+ {
+ "deltachat-rpc-server-${arch}-android" = rpc-server;
+ "deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
+ "deltachat-rpc-server-${arch}-android-wheel" =
+ pkgs.stdenv.mkDerivation {
+ pname = "deltachat-rpc-server-${arch}-android-wheel";
+ version = manifest.version;
+ src = nix-filter.lib {
+ root = ./.;
+ include = [
+ "scripts/wheel-rpc-server.py"
+ "deltachat-rpc-server/README.md"
+ "LICENSE"
+ "Cargo.toml"
+ ];
+ };
+ nativeBuildInputs = [
+ pkgs.python3
+ pkgs.python3Packages.wheel
+ ];
+ buildInputs = [
+ rpc-server
+ ];
+ buildPhase = ''
+ mkdir tmp
+ cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
+ python3 scripts/wheel-rpc-server.py ${arch}-android tmp/deltachat-rpc-server
+ '';
+ installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
+ };
+ };
mkRustPackages = arch:
let
@@ -556,6 +584,9 @@
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
+ (python3.withPackages (pypkgs: with pypkgs; [
+ tox
+ ]))
];
};
}
diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock
deleted file mode 100644
index 13489ec300..0000000000
--- a/fuzz/Cargo.lock
+++ /dev/null
@@ -1,6836 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "addr2line"
-version = "0.19.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
-dependencies = [
- "gimli",
-]
-
-[[package]]
-name = "adler"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
-
-[[package]]
-name = "aead"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
-dependencies = [
- "bytes",
- "crypto-common",
- "generic-array",
-]
-
-[[package]]
-name = "aes"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241"
-dependencies = [
- "cfg-if",
- "cipher",
- "cpufeatures",
-]
-
-[[package]]
-name = "aes-gcm"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
-dependencies = [
- "aead",
- "aes",
- "cipher",
- "ctr",
- "ghash",
- "subtle",
-]
-
-[[package]]
-name = "aes-kw"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c"
-dependencies = [
- "aes",
-]
-
-[[package]]
-name = "ahash"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
-dependencies = [
- "getrandom 0.2.11",
- "once_cell",
- "version_check",
-]
-
-[[package]]
-name = "ahash"
-version = "0.8.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
-dependencies = [
- "cfg-if",
- "once_cell",
- "version_check",
- "zerocopy",
-]
-
-[[package]]
-name = "aho-corasick"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "alloc-no-stdlib"
-version = "2.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
-
-[[package]]
-name = "alloc-stdlib"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
-dependencies = [
- "alloc-no-stdlib",
-]
-
-[[package]]
-name = "allocator-api2"
-version = "0.2.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
-
-[[package]]
-name = "android-tzdata"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
-
-[[package]]
-name = "android_system_properties"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "anyhow"
-version = "1.0.93"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
-
-[[package]]
-name = "argon2"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
-dependencies = [
- "base64ct",
- "blake2",
- "cpufeatures",
- "password-hash",
- "zeroize",
-]
-
-[[package]]
-name = "arrayref"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
-
-[[package]]
-name = "arrayvec"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
-
-[[package]]
-name = "ascii_utils"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
-
-[[package]]
-name = "asn1-rs"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22ad1373757efa0f70ec53939aabc7152e1591cb485208052993070ac8d2429d"
-dependencies = [
- "asn1-rs-derive",
- "asn1-rs-impl",
- "displaydoc",
- "nom",
- "num-traits",
- "rusticata-macros",
- "thiserror 1.0.58",
- "time 0.3.36",
-]
-
-[[package]]
-name = "asn1-rs-derive"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
- "synstructure 0.13.1",
-]
-
-[[package]]
-name = "asn1-rs-impl"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "async-broadcast"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
-dependencies = [
- "event-listener 5.3.1",
- "event-listener-strategy",
- "futures-core",
- "pin-project-lite",
-]
-
-[[package]]
-name = "async-channel"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
-dependencies = [
- "concurrent-queue",
- "event-listener 2.5.3",
- "futures-core",
-]
-
-[[package]]
-name = "async-channel"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
-dependencies = [
- "concurrent-queue",
- "event-listener-strategy",
- "futures-core",
- "pin-project-lite",
-]
-
-[[package]]
-name = "async-compression"
-version = "0.4.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857"
-dependencies = [
- "flate2",
- "futures-core",
- "futures-io",
- "memchr",
- "pin-project-lite",
- "tokio",
-]
-
-[[package]]
-name = "async-imap"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5488cd022c3c7bc41a9b34a540d9ac0d9c5cd42fdb106a67616521b7592d5b4e"
-dependencies = [
- "async-channel 2.3.1",
- "async-compression",
- "base64 0.21.7",
- "bytes",
- "chrono",
- "futures",
- "imap-proto",
- "log",
- "nom",
- "once_cell",
- "pin-project",
- "pin-utils",
- "self_cell",
- "stop-token",
- "thiserror 1.0.58",
- "tokio",
-]
-
-[[package]]
-name = "async-native-tls"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec"
-dependencies = [
- "native-tls",
- "thiserror 1.0.58",
- "tokio",
- "url",
-]
-
-[[package]]
-name = "async-recursion"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "async-smtp"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ee04bcf0a7ebf5594f9aff84935dc8cb0490b65055913a7a4c4d08f81e181d6"
-dependencies = [
- "anyhow",
- "base64 0.13.1",
- "futures",
- "log",
- "nom",
- "pin-project",
- "thiserror 1.0.58",
- "tokio",
-]
-
-[[package]]
-name = "async-trait"
-version = "0.1.81"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "async_zip"
-version = "0.0.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
-dependencies = [
- "async-compression",
- "crc32fast",
- "futures-lite 2.5.0",
- "pin-project",
- "thiserror 1.0.58",
- "tokio",
- "tokio-util",
-]
-
-[[package]]
-name = "attohttpc"
-version = "0.24.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2"
-dependencies = [
- "http 0.2.12",
- "log",
- "url",
-]
-
-[[package]]
-name = "autocfg"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
-
-[[package]]
-name = "backoff"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
-dependencies = [
- "getrandom 0.2.11",
- "instant",
- "rand 0.8.5",
-]
-
-[[package]]
-name = "backtrace"
-version = "0.3.67"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
-dependencies = [
- "addr2line",
- "cc",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
-]
-
-[[package]]
-name = "base16ct"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
-
-[[package]]
-name = "base64"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
-
-[[package]]
-name = "base64"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
-
-[[package]]
-name = "base64"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
-
-[[package]]
-name = "base64"
-version = "0.21.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
-
-[[package]]
-name = "base64"
-version = "0.22.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
-
-[[package]]
-name = "base64ct"
-version = "1.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf"
-
-[[package]]
-name = "bitfield"
-version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac"
-
-[[package]]
-name = "bitflags"
-version = "1.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
-
-[[package]]
-name = "bitflags"
-version = "2.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
-
-[[package]]
-name = "bitvec"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
-dependencies = [
- "funty",
- "radium",
- "tap",
- "wyz",
-]
-
-[[package]]
-name = "blake2"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
-dependencies = [
- "digest",
-]
-
-[[package]]
-name = "blake3"
-version = "1.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7"
-dependencies = [
- "arrayref",
- "arrayvec",
- "cc",
- "cfg-if",
- "constant_time_eq",
-]
-
-[[package]]
-name = "block-buffer"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "block-padding"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "blowfish"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
-dependencies = [
- "byteorder",
- "cipher",
-]
-
-[[package]]
-name = "bolero"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3387d308f66ed222bdbb19c6ba06b1517168c4e45dc64051c5f1b4845db2901c"
-dependencies = [
- "bolero-afl",
- "bolero-engine",
- "bolero-generator",
- "bolero-honggfuzz",
- "bolero-kani",
- "bolero-libfuzzer",
- "cfg-if",
- "rand 0.8.5",
-]
-
-[[package]]
-name = "bolero-afl"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "973bc6341b6a865dee93f17b78de4a100551014a527798ff1d7265d3bc0f7d89"
-dependencies = [
- "bolero-engine",
- "cc",
-]
-
-[[package]]
-name = "bolero-engine"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c506a476cea9e95f58c264b343ee279c353d93ceaebe98cbfb16e74bfaee2e2"
-dependencies = [
- "anyhow",
- "backtrace",
- "bolero-generator",
- "lazy_static",
- "pretty-hex",
- "rand 0.8.5",
-]
-
-[[package]]
-name = "bolero-generator"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48d52eca8714d110e581cf17eeacf0d1a0d409d38a9e9ce07efeda6125f7febb"
-dependencies = [
- "bolero-generator-derive",
- "either",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "bolero-generator-derive"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b3c57c2a0967ad1a09ba4c2bf8f1c6b6db2f71e8c0db4fa280c65a0f6c249c3"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.107",
-]
-
-[[package]]
-name = "bolero-honggfuzz"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7996a3fa8d93652358b9b3b805233807168f49740a8bf91a531cd61e4da65355"
-dependencies = [
- "bolero-engine",
-]
-
-[[package]]
-name = "bolero-kani"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "206879993fffa1cf2c703b1ef93b0febfa76bae85a0a5d4ae0ee6d99a2e3b74e"
-dependencies = [
- "bolero-engine",
-]
-
-[[package]]
-name = "bolero-libfuzzer"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdc5547411b84703d9020914f15a7d709cfb738c72b5e0f5a499fe56b8465c98"
-dependencies = [
- "bolero-engine",
- "cc",
-]
-
-[[package]]
-name = "bounded-integer"
-version = "0.5.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78a6932c88f1d2c29533a3b8a5f5a2f84cc19c3339b431677c3160c5c2e6ca85"
-
-[[package]]
-name = "brotli"
-version = "7.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
-dependencies = [
- "alloc-no-stdlib",
- "alloc-stdlib",
- "brotli-decompressor",
-]
-
-[[package]]
-name = "brotli-decompressor"
-version = "4.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76"
-dependencies = [
- "alloc-no-stdlib",
- "alloc-stdlib",
-]
-
-[[package]]
-name = "bstr"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
-dependencies = [
- "memchr",
- "serde",
-]
-
-[[package]]
-name = "buffer-redux"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2886ea01509598caac116942abd33ab5a88fa32acdf7e4abfa0fc489ca520c9"
-dependencies = [
- "memchr",
- "safemem",
-]
-
-[[package]]
-name = "bumpalo"
-version = "3.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
-
-[[package]]
-name = "byte_string"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11aade7a05aa8c3a351cedc44c3fc45806430543382fcc4743a9b757a2a0b4ed"
-
-[[package]]
-name = "bytemuck"
-version = "1.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f"
-
-[[package]]
-name = "byteorder"
-version = "1.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
-
-[[package]]
-name = "byteorder-lite"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
-
-[[package]]
-name = "bytes"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "camellia"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30"
-dependencies = [
- "byteorder",
- "cipher",
-]
-
-[[package]]
-name = "cast5"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911"
-dependencies = [
- "cipher",
-]
-
-[[package]]
-name = "cc"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
-dependencies = [
- "shlex",
-]
-
-[[package]]
-name = "cesu8"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
-
-[[package]]
-name = "cfb-mode"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330"
-dependencies = [
- "cipher",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
-
-[[package]]
-name = "chacha20"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
-dependencies = [
- "cfg-if",
- "cipher",
- "cpufeatures",
-]
-
-[[package]]
-name = "chacha20poly1305"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
-dependencies = [
- "aead",
- "chacha20",
- "cipher",
- "poly1305",
- "zeroize",
-]
-
-[[package]]
-name = "charset"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46"
-dependencies = [
- "base64 0.13.1",
- "encoding_rs",
-]
-
-[[package]]
-name = "chrono"
-version = "0.4.38"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
-dependencies = [
- "android-tzdata",
- "iana-time-zone",
- "js-sys",
- "num-traits",
- "serde",
- "wasm-bindgen",
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "cipher"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
-dependencies = [
- "crypto-common",
- "inout",
- "zeroize",
-]
-
-[[package]]
-name = "cmac"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa"
-dependencies = [
- "cipher",
- "dbl",
- "digest",
-]
-
-[[package]]
-name = "cobs"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
-
-[[package]]
-name = "codespan-reporting"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
-dependencies = [
- "termcolor",
- "unicode-width",
-]
-
-[[package]]
-name = "color_quant"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
-
-[[package]]
-name = "combine"
-version = "4.6.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
-dependencies = [
- "bytes",
- "memchr",
-]
-
-[[package]]
-name = "concurrent-queue"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
-name = "const-oid"
-version = "0.9.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913"
-
-[[package]]
-name = "const_format"
-version = "0.2.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7309d9b4d3d2c0641e018d449232f2e28f1b22933c137f157d3dbc14228b8c0e"
-dependencies = [
- "const_format_proc_macros",
-]
-
-[[package]]
-name = "const_format_proc_macros"
-version = "0.2.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d897f47bf7270cf70d370f8f98c1abb6d2d4cf60a6845d30e05bfb90c6568650"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-xid",
-]
-
-[[package]]
-name = "constant_time_eq"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
-
-[[package]]
-name = "cordyceps"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec10f0a762d93c4498d2e97a333805cb6250d60bead623f71d8034f9a4152ba3"
-dependencies = [
- "loom",
- "tracing",
-]
-
-[[package]]
-name = "core-foundation"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
-
-[[package]]
-name = "cpufeatures"
-version = "0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crc"
-version = "3.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
-dependencies = [
- "crc-catalog",
-]
-
-[[package]]
-name = "crc-catalog"
-version = "2.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
-
-[[package]]
-name = "crc24"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0"
-
-[[package]]
-name = "crc32fast"
-version = "1.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.8.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "crypto-bigint"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c2538c4e68e52548bacb3e83ac549f903d44f011ac9d5abb5e132e67d0808f7"
-dependencies = [
- "generic-array",
- "rand_core 0.6.4",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "crypto-common"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
-dependencies = [
- "generic-array",
- "rand_core 0.6.4",
- "typenum",
-]
-
-[[package]]
-name = "crypto_box"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009"
-dependencies = [
- "aead",
- "chacha20",
- "crypto_secretbox",
- "curve25519-dalek",
- "salsa20",
- "serdect",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "crypto_secretbox"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1"
-dependencies = [
- "aead",
- "chacha20",
- "cipher",
- "generic-array",
- "poly1305",
- "salsa20",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "ctr"
-version = "0.9.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
-dependencies = [
- "cipher",
-]
-
-[[package]]
-name = "curve25519-dalek"
-version = "4.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "curve25519-dalek-derive",
- "digest",
- "fiat-crypto 0.2.6",
- "rustc_version",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "curve25519-dalek-derive"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "cxx"
-version = "1.0.85"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd"
-dependencies = [
- "cc",
- "cxxbridge-flags",
- "cxxbridge-macro",
- "link-cplusplus",
-]
-
-[[package]]
-name = "cxx-build"
-version = "1.0.85"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0"
-dependencies = [
- "cc",
- "codespan-reporting",
- "once_cell",
- "proc-macro2",
- "quote",
- "scratch",
- "syn 1.0.107",
-]
-
-[[package]]
-name = "cxxbridge-flags"
-version = "1.0.85"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59"
-
-[[package]]
-name = "cxxbridge-macro"
-version = "1.0.85"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.107",
-]
-
-[[package]]
-name = "darling"
-version = "0.20.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
-dependencies = [
- "darling_core",
- "darling_macro",
-]
-
-[[package]]
-name = "darling_core"
-version = "0.20.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
-dependencies = [
- "fnv",
- "ident_case",
- "proc-macro2",
- "quote",
- "strsim",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "darling_macro"
-version = "0.20.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
-dependencies = [
- "darling_core",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "dashmap"
-version = "5.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
-dependencies = [
- "cfg-if",
- "hashbrown 0.12.3",
- "lock_api",
- "once_cell",
- "parking_lot_core",
-]
-
-[[package]]
-name = "data-encoding"
-version = "2.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
-
-[[package]]
-name = "dbl"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "deltachat"
-version = "1.151.5"
-dependencies = [
- "anyhow",
- "async-broadcast",
- "async-channel 2.3.1",
- "async-imap",
- "async-native-tls",
- "async-smtp",
- "async_zip",
- "base64 0.22.1",
- "brotli",
- "bytes",
- "chrono",
- "deltachat-contact-tools",
- "deltachat-time",
- "deltachat_derive",
- "email",
- "encoded-words",
- "escaper",
- "fast-socks5",
- "fd-lock",
- "format-flowed",
- "futures",
- "futures-lite 2.5.0",
- "hex",
- "hickory-resolver",
- "http-body-util",
- "humansize",
- "hyper",
- "hyper-util",
- "image",
- "iroh-gossip",
- "iroh-net",
- "kamadak-exif",
- "lettre_email",
- "libc",
- "mailparse 0.15.0",
- "mime",
- "num-derive",
- "num-traits",
- "num_cpus",
- "once_cell",
- "parking_lot",
- "percent-encoding",
- "pgp",
- "pin-project",
- "qrcodegen",
- "quick-xml",
- "quoted_printable 0.5.0",
- "rand 0.8.5",
- "ratelimit",
- "regex",
- "rusqlite",
- "rust-hsluv",
- "rustls",
- "rustls-pki-types",
- "sanitize-filename",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "sha-1",
- "sha2",
- "shadowsocks",
- "smallvec",
- "strum",
- "strum_macros",
- "tagger",
- "textwrap",
- "thiserror 1.0.58",
- "tokio",
- "tokio-io-timeout",
- "tokio-rustls",
- "tokio-stream",
- "tokio-tar",
- "tokio-util",
- "toml",
- "url",
- "uuid 1.2.2",
- "webpki-roots",
-]
-
-[[package]]
-name = "deltachat-contact-tools"
-version = "0.0.0"
-dependencies = [
- "anyhow",
- "chrono",
- "once_cell",
- "regex",
- "rusqlite",
-]
-
-[[package]]
-name = "deltachat-fuzz"
-version = "0.0.0"
-dependencies = [
- "bolero",
- "deltachat",
- "format-flowed",
- "mailparse 0.13.8",
-]
-
-[[package]]
-name = "deltachat-time"
-version = "1.0.0"
-
-[[package]]
-name = "deltachat_derive"
-version = "2.0.0"
-dependencies = [
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "der"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b10af9f9f9f2134a42d3f8aa74658660f2e0234b0eb81bd171df8aa32779ed"
-dependencies = [
- "const-oid",
- "der_derive",
- "pem-rfc7468",
- "zeroize",
-]
-
-[[package]]
-name = "der-parser"
-version = "9.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
-dependencies = [
- "asn1-rs",
- "displaydoc",
- "nom",
- "num-bigint",
- "num-traits",
- "rusticata-macros",
-]
-
-[[package]]
-name = "der_derive"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "deranged"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
-dependencies = [
- "powerfmt",
- "serde",
-]
-
-[[package]]
-name = "derive_builder"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
-dependencies = [
- "derive_builder_macro",
-]
-
-[[package]]
-name = "derive_builder_core"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
-dependencies = [
- "darling",
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "derive_builder_macro"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
-dependencies = [
- "derive_builder_core",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "derive_more"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
-dependencies = [
- "derive_more-impl",
-]
-
-[[package]]
-name = "derive_more-impl"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
- "unicode-xid",
-]
-
-[[package]]
-name = "des"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e"
-dependencies = [
- "cipher",
-]
-
-[[package]]
-name = "diatomic-waker"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c"
-
-[[package]]
-name = "digest"
-version = "0.10.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
-dependencies = [
- "block-buffer",
- "const-oid",
- "crypto-common",
- "subtle",
-]
-
-[[package]]
-name = "displaydoc"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.107",
-]
-
-[[package]]
-name = "dlopen2"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa"
-dependencies = [
- "libc",
- "once_cell",
- "winapi",
-]
-
-[[package]]
-name = "document-features"
-version = "0.2.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0"
-dependencies = [
- "litrs",
-]
-
-[[package]]
-name = "dsa"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689"
-dependencies = [
- "digest",
- "num-bigint-dig",
- "num-traits",
- "pkcs8",
- "rfc6979",
- "sha2",
- "signature",
- "zeroize",
-]
-
-[[package]]
-name = "dtoa"
-version = "1.0.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
-
-[[package]]
-name = "duct"
-version = "0.13.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c"
-dependencies = [
- "libc",
- "once_cell",
- "os_pipe",
- "shared_child",
-]
-
-[[package]]
-name = "dyn-clone"
-version = "1.0.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
-
-[[package]]
-name = "eax"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28"
-dependencies = [
- "aead",
- "cipher",
- "cmac",
- "ctr",
- "subtle",
-]
-
-[[package]]
-name = "ecdsa"
-version = "0.16.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a48e5d537b8a30c0b023116d981b16334be1485af7ca68db3a2b7024cbc957fd"
-dependencies = [
- "der",
- "digest",
- "elliptic-curve",
- "rfc6979",
- "signature",
-]
-
-[[package]]
-name = "ed25519"
-version = "2.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963"
-dependencies = [
- "pkcs8",
- "serde",
- "signature",
-]
-
-[[package]]
-name = "ed25519-dalek"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
-dependencies = [
- "curve25519-dalek",
- "ed25519",
- "rand_core 0.6.4",
- "serde",
- "sha2",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "ed448-goldilocks"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79"
-dependencies = [
- "fiat-crypto 0.1.20",
- "hex",
- "subtle",
-]
-
-[[package]]
-name = "either"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
-
-[[package]]
-name = "elliptic-curve"
-version = "0.13.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
-dependencies = [
- "base16ct",
- "crypto-bigint",
- "digest",
- "ff",
- "generic-array",
- "group",
- "hkdf",
- "pem-rfc7468",
- "pkcs8",
- "rand_core 0.6.4",
- "sec1",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "email"
-version = "0.0.21"
-source = "git+https://github.com/deltachat/rust-email?branch=master#25702df99254d059483b41417cd80696a258df8e"
-dependencies = [
- "base64 0.11.0",
- "chrono",
- "encoded-words",
- "encoding",
- "lazy_static",
- "rand 0.7.3",
- "time 0.1.45",
- "version_check",
-]
-
-[[package]]
-name = "encoded-words"
-version = "0.2.0"
-source = "git+https://github.com/async-email/encoded-words?branch=master#d55366b36f96e383f39c432aedce42ee8b43f796"
-dependencies = [
- "base64 0.12.3",
- "charset",
- "encoding_rs",
- "hex",
- "lazy_static",
- "regex",
- "thiserror 1.0.58",
-]
-
-[[package]]
-name = "encoding"
-version = "0.2.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
-dependencies = [
- "encoding-index-japanese",
- "encoding-index-korean",
- "encoding-index-simpchinese",
- "encoding-index-singlebyte",
- "encoding-index-tradchinese",
-]
-
-[[package]]
-name = "encoding-index-japanese"
-version = "1.20141219.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
-dependencies = [
- "encoding_index_tests",
-]
-
-[[package]]
-name = "encoding-index-korean"
-version = "1.20141219.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
-dependencies = [
- "encoding_index_tests",
-]
-
-[[package]]
-name = "encoding-index-simpchinese"
-version = "1.20141219.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
-dependencies = [
- "encoding_index_tests",
-]
-
-[[package]]
-name = "encoding-index-singlebyte"
-version = "1.20141219.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
-dependencies = [
- "encoding_index_tests",
-]
-
-[[package]]
-name = "encoding-index-tradchinese"
-version = "1.20141219.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
-dependencies = [
- "encoding_index_tests",
-]
-
-[[package]]
-name = "encoding_index_tests"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
-
-[[package]]
-name = "encoding_rs"
-version = "0.8.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "entities"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
-
-[[package]]
-name = "enum-as-inner"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
-dependencies = [
- "heck 0.4.1",
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "enumflags2"
-version = "0.7.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d"
-dependencies = [
- "enumflags2_derive",
-]
-
-[[package]]
-name = "enumflags2_derive"
-version = "0.7.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "equivalent"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
-
-[[package]]
-name = "erased-serde"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "erased_set"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76a5aa24577083f8190ad401e376b55887c7cd9083ae95d83ceec5d28ea78125"
-
-[[package]]
-name = "errno"
-version = "0.2.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
-dependencies = [
- "errno-dragonfly",
- "libc",
- "winapi",
-]
-
-[[package]]
-name = "errno"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
-dependencies = [
- "errno-dragonfly",
- "libc",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "errno-dragonfly"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
-dependencies = [
- "cc",
- "libc",
-]
-
-[[package]]
-name = "escaper"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a53eb97b7349ba1bdb31839eceafe9aaae8f1d8d944dc589b67fb0b26e1c1666"
-dependencies = [
- "entities",
-]
-
-[[package]]
-name = "event-listener"
-version = "2.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
-
-[[package]]
-name = "event-listener"
-version = "4.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e"
-dependencies = [
- "concurrent-queue",
- "parking",
- "pin-project-lite",
-]
-
-[[package]]
-name = "event-listener"
-version = "5.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
-dependencies = [
- "concurrent-queue",
- "parking",
- "pin-project-lite",
-]
-
-[[package]]
-name = "event-listener-strategy"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
-dependencies = [
- "event-listener 5.3.1",
- "pin-project-lite",
-]
-
-[[package]]
-name = "fallible-iterator"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
-
-[[package]]
-name = "fallible-streaming-iterator"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
-
-[[package]]
-name = "fast-socks5"
-version = "0.9.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d449e348301d5fb9b0e5781510d8235ffe3bbac3286bd305462736a9e7043039"
-dependencies = [
- "anyhow",
- "async-trait",
- "log",
- "thiserror 1.0.58",
- "tokio",
- "tokio-stream",
-]
-
-[[package]]
-name = "fast_chemail"
-version = "0.9.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4"
-dependencies = [
- "ascii_utils",
-]
-
-[[package]]
-name = "fastrand"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
-dependencies = [
- "instant",
-]
-
-[[package]]
-name = "fastrand"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
-
-[[package]]
-name = "fd-lock"
-version = "4.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b93f7a0db71c99f68398f80653ed05afb0b00e062e1a20c7ff849c4edfabbbcc"
-dependencies = [
- "cfg-if",
- "rustix 0.38.14",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "ff"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
-dependencies = [
- "rand_core 0.6.4",
- "subtle",
-]
-
-[[package]]
-name = "fiat-crypto"
-version = "0.1.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77"
-
-[[package]]
-name = "fiat-crypto"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382"
-
-[[package]]
-name = "filetime"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "windows-sys 0.42.0",
-]
-
-[[package]]
-name = "flate2"
-version = "1.0.25"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
-dependencies = [
- "crc32fast",
- "miniz_oxide",
-]
-
-[[package]]
-name = "flume"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
-dependencies = [
- "futures-core",
- "futures-sink",
- "nanorand",
- "spin 0.9.8",
-]
-
-[[package]]
-name = "fnv"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
-
-[[package]]
-name = "foreign-types"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
-dependencies = [
- "foreign-types-shared",
-]
-
-[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
-
-[[package]]
-name = "form_urlencoded"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
-dependencies = [
- "percent-encoding",
-]
-
-[[package]]
-name = "format-flowed"
-version = "1.0.0"
-
-[[package]]
-name = "funty"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
-
-[[package]]
-name = "futures"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-buffered"
-version = "0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34acda8ae8b63fbe0b2195c998b180cff89a8212fb2622a78b572a9f1c6f7684"
-dependencies = [
- "cordyceps",
- "diatomic-waker",
- "futures-core",
- "pin-project-lite",
-]
-
-[[package]]
-name = "futures-channel"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
-dependencies = [
- "futures-core",
- "futures-sink",
-]
-
-[[package]]
-name = "futures-concurrency"
-version = "7.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b14ac911e85d57c5ea6eef76d7b4d4a3177ecd15f4bea2e61927e9e3823e19f"
-dependencies = [
- "bitvec",
- "futures-buffered",
- "futures-core",
- "futures-lite 1.13.0",
- "pin-project",
- "slab",
- "smallvec",
-]
-
-[[package]]
-name = "futures-core"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
-
-[[package]]
-name = "futures-executor"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-io"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
-
-[[package]]
-name = "futures-lite"
-version = "1.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
-dependencies = [
- "fastrand 1.8.0",
- "futures-core",
- "futures-io",
- "memchr",
- "parking",
- "pin-project-lite",
- "waker-fn",
-]
-
-[[package]]
-name = "futures-lite"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
-dependencies = [
- "fastrand 2.0.1",
- "futures-core",
- "futures-io",
- "parking",
- "pin-project-lite",
-]
-
-[[package]]
-name = "futures-macro"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "futures-sink"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
-
-[[package]]
-name = "futures-task"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
-
-[[package]]
-name = "futures-timer"
-version = "3.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
-
-[[package]]
-name = "futures-util"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-macro",
- "futures-sink",
- "futures-task",
- "memchr",
- "pin-project-lite",
- "pin-utils",
- "slab",
-]
-
-[[package]]
-name = "genawaiter"
-version = "0.99.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0"
-dependencies = [
- "futures-core",
- "genawaiter-macro",
- "genawaiter-proc-macro",
- "proc-macro-hack",
-]
-
-[[package]]
-name = "genawaiter-macro"
-version = "0.99.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc"
-
-[[package]]
-name = "genawaiter-proc-macro"
-version = "0.99.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738"
-dependencies = [
- "proc-macro-error",
- "proc-macro-hack",
- "proc-macro2",
- "quote",
- "syn 1.0.107",
-]
-
-[[package]]
-name = "generator"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
-dependencies = [
- "cc",
- "libc",
- "log",
- "rustversion",
- "windows 0.48.0",
-]
-
-[[package]]
-name = "generic-array"
-version = "0.14.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
-dependencies = [
- "typenum",
- "version_check",
- "zeroize",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi 0.9.0+wasi-snapshot-preview1",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
-dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "ghash"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
-dependencies = [
- "opaque-debug",
- "polyval",
-]
-
-[[package]]
-name = "gif"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
-dependencies = [
- "color_quant",
- "weezl",
-]
-
-[[package]]
-name = "gimli"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793"
-
-[[package]]
-name = "glob"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
-
-[[package]]
-name = "governor"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b"
-dependencies = [
- "cfg-if",
- "dashmap",
- "futures",
- "futures-timer",
- "no-std-compat",
- "nonzero_ext",
- "parking_lot",
- "portable-atomic",
- "quanta",
- "rand 0.8.5",
- "smallvec",
- "spinning_top",
-]
-
-[[package]]
-name = "group"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
-dependencies = [
- "ff",
- "rand_core 0.6.4",
- "subtle",
-]
-
-[[package]]
-name = "h2"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
-dependencies = [
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "futures-util",
- "http 1.1.0",
- "indexmap",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-dependencies = [
- "ahash 0.7.6",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.14.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
-dependencies = [
- "ahash 0.8.11",
- "allocator-api2",
-]
-
-[[package]]
-name = "hashlink"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee"
-dependencies = [
- "hashbrown 0.14.3",
-]
-
-[[package]]
-name = "heck"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
-
-[[package]]
-name = "heck"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-
-[[package]]
-name = "hermit-abi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
-
-[[package]]
-name = "hex"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
-
-[[package]]
-name = "hickory-proto"
-version = "0.25.0-alpha.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8270a1857fb962b9914aafd46a89a187a4e63d0eb4190c327e7c7b8256a2d055"
-dependencies = [
- "async-recursion",
- "async-trait",
- "cfg-if",
- "data-encoding",
- "enum-as-inner",
- "futures-channel",
- "futures-io",
- "futures-util",
- "idna",
- "ipnet",
- "once_cell",
- "rand 0.8.5",
- "thiserror 1.0.58",
- "time 0.3.36",
- "tinyvec",
- "tokio",
- "tracing",
- "url",
-]
-
-[[package]]
-name = "hickory-resolver"
-version = "0.25.0-alpha.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46c110355b5703070d9e29c344d79818a7cde3de9c27fc35750defea6074b0ad"
-dependencies = [
- "cfg-if",
- "futures-util",
- "hickory-proto",
- "ipconfig",
- "lru-cache",
- "once_cell",
- "parking_lot",
- "rand 0.8.5",
- "resolv-conf",
- "smallvec",
- "thiserror 1.0.58",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "hkdf"
-version = "0.12.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
-dependencies = [
- "hmac",
-]
-
-[[package]]
-name = "hmac"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
-dependencies = [
- "digest",
-]
-
-[[package]]
-name = "hmac-sha1"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b05da5b9e5d4720bfb691eebb2b9d42da3570745da71eac8a1f5bb7e59aab88"
-dependencies = [
- "hmac",
- "sha1",
-]
-
-[[package]]
-name = "hmac-sha256"
-version = "1.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735"
-
-[[package]]
-name = "hostname"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
-dependencies = [
- "libc",
- "match_cfg",
- "winapi",
-]
-
-[[package]]
-name = "hostname-validator"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2"
-
-[[package]]
-name = "http"
-version = "0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
-dependencies = [
- "bytes",
- "fnv",
- "itoa",
-]
-
-[[package]]
-name = "http"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
-dependencies = [
- "bytes",
- "fnv",
- "itoa",
-]
-
-[[package]]
-name = "http-body"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
-dependencies = [
- "bytes",
- "http 1.1.0",
-]
-
-[[package]]
-name = "http-body-util"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
-dependencies = [
- "bytes",
- "futures-util",
- "http 1.1.0",
- "http-body",
- "pin-project-lite",
-]
-
-[[package]]
-name = "httparse"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
-
-[[package]]
-name = "httpdate"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
-
-[[package]]
-name = "humansize"
-version = "2.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756"
-dependencies = [
- "libm",
-]
-
-[[package]]
-name = "hyper"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-util",
- "h2",
- "http 1.1.0",
- "http-body",
- "httparse",
- "httpdate",
- "itoa",
- "pin-project-lite",
- "smallvec",
- "tokio",
- "want",
-]
-
-[[package]]
-name = "hyper-rustls"
-version = "0.27.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
-dependencies = [
- "futures-util",
- "http 1.1.0",
- "hyper",
- "hyper-util",
- "rustls",
- "rustls-pki-types",
- "tokio",
- "tokio-rustls",
- "tower-service",
- "webpki-roots",
-]
-
-[[package]]
-name = "hyper-util"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-util",
- "http 1.1.0",
- "http-body",
- "hyper",
- "pin-project-lite",
- "socket2 0.5.6",
- "tokio",
- "tower-service",
- "tracing",
-]
-
-[[package]]
-name = "iana-time-zone"
-version = "0.1.53"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
-dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "wasm-bindgen",
- "winapi",
-]
-
-[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
-dependencies = [
- "cxx",
- "cxx-build",
-]
-
-[[package]]
-name = "idea"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477"
-dependencies = [
- "cipher",
-]
-
-[[package]]
-name = "ident_case"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
-
-[[package]]
-name = "idna"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
-dependencies = [
- "unicode-bidi",
- "unicode-normalization",
-]
-
-[[package]]
-name = "igd-next"
-version = "0.15.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76b0d7d4541def58a37bf8efc559683f21edce7c82f0d866c93ac21f7e098f93"
-dependencies = [
- "async-trait",
- "attohttpc",
- "bytes",
- "futures",
- "http 1.1.0",
- "http-body-util",
- "hyper",
- "hyper-util",
- "log",
- "rand 0.8.5",
- "tokio",
- "url",
- "xmltree",
-]
-
-[[package]]
-name = "image"
-version = "0.25.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
-dependencies = [
- "bytemuck",
- "byteorder-lite",
- "color_quant",
- "gif",
- "image-webp",
- "num-traits",
- "png",
- "zune-core",
- "zune-jpeg",
-]
-
-[[package]]
-name = "image-webp"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
-dependencies = [
- "byteorder-lite",
- "quick-error 2.0.1",
-]
-
-[[package]]
-name = "imap-proto"
-version = "0.16.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22e70cd66882c8cb1c9802096ba75212822153c51478dc61621e1a22f6c92361"
-dependencies = [
- "nom",
-]
-
-[[package]]
-name = "indexmap"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
-dependencies = [
- "equivalent",
- "hashbrown 0.14.3",
-]
-
-[[package]]
-name = "inout"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "instant"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "io-lifetimes"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
-dependencies = [
- "libc",
- "windows-sys 0.42.0",
-]
-
-[[package]]
-name = "ipconfig"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be"
-dependencies = [
- "socket2 0.4.7",
- "widestring",
- "winapi",
- "winreg 0.10.1",
-]
-
-[[package]]
-name = "ipnet"
-version = "2.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e"
-
-[[package]]
-name = "iroh-base"
-version = "0.28.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c21fd8eb71f166a172a9779c2244db992218e9a9bd929b9df6fc355d2b630c9"
-dependencies = [
- "aead",
- "anyhow",
- "crypto_box",
- "data-encoding",
- "derive_more",
- "ed25519-dalek",
- "getrandom 0.2.11",
- "hex",
- "iroh-blake3",
- "once_cell",
- "postcard",
- "rand 0.8.5",
- "rand_core 0.6.4",
- "serde",
- "ssh-key",
- "thiserror 1.0.58",
- "ttl_cache",
- "url",
- "zeroize",
-]
-
-[[package]]
-name = "iroh-blake3"
-version = "1.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efbba31f40a650f58fa28dd585a8ca76d8ae3ba63aacab4c8269004a0c803930"
-dependencies = [
- "arrayref",
- "arrayvec",
- "cc",
- "cfg-if",
- "constant_time_eq",
-]
-
-[[package]]
-name = "iroh-gossip"
-version = "0.28.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c078057037f0e741c5ef285c67fd9cfdb928163dd046fb547089898bdb02990e"
-dependencies = [
- "anyhow",
- "async-channel 2.3.1",
- "bytes",
- "derive_more",
- "ed25519-dalek",
- "futures-concurrency",
- "futures-lite 2.5.0",
- "futures-util",
- "indexmap",
- "iroh-base",
- "iroh-blake3",
- "iroh-metrics",
- "iroh-net",
- "iroh-router",
- "postcard",
- "rand 0.8.5",
- "rand_core 0.6.4",
- "serde",
- "serde-error",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
-name = "iroh-metrics"
-version = "0.28.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0d40f2ee3997489d47403d204a06514ed65373d224b5b43a8ea133f543e5db1"
-dependencies = [
- "anyhow",
- "erased_set",
- "http-body-util",
- "hyper",
- "hyper-util",
- "once_cell",
- "prometheus-client",
- "reqwest",
- "serde",
- "struct_iterable",
- "time 0.3.36",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "iroh-net"
-version = "0.28.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b40e1f1f9029e198c6d05bd232d3239814b0a66ac4668978729b709aeb6a44e2"
-dependencies = [
- "anyhow",
- "backoff",
- "base64 0.22.1",
- "bytes",
- "der",
- "derive_more",
- "duct",
- "futures-buffered",
- "futures-concurrency",
- "futures-lite 2.5.0",
- "futures-sink",
- "futures-util",
- "genawaiter",
- "governor",
- "hex",
- "hickory-proto",
- "hickory-resolver",
- "hostname",
- "http 1.1.0",
- "http-body-util",
- "hyper",
- "hyper-util",
- "igd-next",
- "iroh-base",
- "iroh-metrics",
- "iroh-quinn",
- "iroh-quinn-proto",
- "iroh-quinn-udp",
- "libc",
- "netdev",
- "netlink-packet-core",
- "netlink-packet-route",
- "netlink-sys",
- "netwatch",
- "num_enum",
- "once_cell",
- "parking_lot",
- "pin-project",
- "pkarr",
- "portmapper",
- "postcard",
- "rand 0.8.5",
- "rcgen",
- "reqwest",
- "ring",
- "rtnetlink",
- "rustls",
- "rustls-webpki",
- "serde",
- "smallvec",
- "socket2 0.5.6",
- "strum",
- "stun-rs",
- "surge-ping",
- "thiserror 1.0.58",
- "time 0.3.36",
- "tokio",
- "tokio-rustls",
- "tokio-stream",
- "tokio-tungstenite",
- "tokio-tungstenite-wasm",
- "tokio-util",
- "tracing",
- "tungstenite",
- "url",
- "watchable",
- "webpki-roots",
- "windows 0.51.1",
- "wmi",
- "x509-parser",
- "z32",
-]
-
-[[package]]
-name = "iroh-quinn"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35ba75a5c57cff299d2d7ca1ddee053f66339d1756bd79ec637bcad5aa61100e"
-dependencies = [
- "bytes",
- "iroh-quinn-proto",
- "iroh-quinn-udp",
- "pin-project-lite",
- "rustc-hash 2.0.0",
- "rustls",
- "socket2 0.5.6",
- "thiserror 1.0.58",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "iroh-quinn-proto"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2c869ba52683d3d067c83ab4c00a2fda18eaf13b1434d4c1352f428674d4a5d"
-dependencies = [
- "bytes",
- "rand 0.8.5",
- "ring",
- "rustc-hash 2.0.0",
- "rustls",
- "rustls-platform-verifier",
- "slab",
- "thiserror 1.0.58",
- "tinyvec",
- "tracing",
-]
-
-[[package]]
-name = "iroh-quinn-udp"
-version = "0.5.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfcfc0abc2fdf8cf18a6c72893b7cbebeac2274a3b1306c1760c48c0e10ac5e0"
-dependencies = [
- "libc",
- "once_cell",
- "socket2 0.5.6",
- "tracing",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "iroh-router"
-version = "0.28.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1fd18ec6325dd3f01625f12c01acff50a4374ee1ab708e7b2078885fd63ad30"
-dependencies = [
- "anyhow",
- "futures-buffered",
- "futures-lite 2.5.0",
- "futures-util",
- "iroh-net",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
-name = "iter-read"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a598c1abae8e3456ebda517868b254b6bc2a9bb6501ffd5b9d0875bf332e048b"
-
-[[package]]
-name = "itoa"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
-
-[[package]]
-name = "jni"
-version = "0.19.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
-dependencies = [
- "cesu8",
- "combine",
- "jni-sys",
- "log",
- "thiserror 1.0.58",
- "walkdir",
-]
-
-[[package]]
-name = "jni-sys"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
-
-[[package]]
-name = "js-sys"
-version = "0.3.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
-dependencies = [
- "wasm-bindgen",
-]
-
-[[package]]
-name = "k256"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc"
-dependencies = [
- "cfg-if",
- "ecdsa",
- "elliptic-curve",
- "once_cell",
- "sha2",
- "signature",
-]
-
-[[package]]
-name = "kamadak-exif"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837"
-dependencies = [
- "mutate_once",
-]
-
-[[package]]
-name = "keccak"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768"
-dependencies = [
- "cpufeatures",
-]
-
-[[package]]
-name = "lazy_static"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
-dependencies = [
- "spin 0.5.2",
-]
-
-[[package]]
-name = "lettre"
-version = "0.9.2"
-source = "git+https://github.com/deltachat/lettre?branch=master#96555ec428ac114ecfca9934d2fda34c13737e54"
-dependencies = [
- "fast_chemail",
- "log",
-]
-
-[[package]]
-name = "lettre_email"
-version = "0.9.2"
-source = "git+https://github.com/deltachat/lettre?branch=master#96555ec428ac114ecfca9934d2fda34c13737e54"
-dependencies = [
- "base64 0.11.0",
- "email",
- "lazy_static",
- "lettre",
- "mime",
- "regex",
- "time 0.1.45",
- "uuid 0.8.2",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.164"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
-
-[[package]]
-name = "libm"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
-
-[[package]]
-name = "libsqlite3-sys"
-version = "0.30.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
-dependencies = [
- "cc",
- "openssl-sys",
- "pkg-config",
- "vcpkg",
-]
-
-[[package]]
-name = "link-cplusplus"
-version = "1.0.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "linked-hash-map"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.4.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
-
-[[package]]
-name = "litrs"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
-
-[[package]]
-name = "lock_api"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
-dependencies = [
- "autocfg",
- "scopeguard",
-]
-
-[[package]]
-name = "log"
-version = "0.4.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "loom"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
-dependencies = [
- "cfg-if",
- "generator",
- "scoped-tls",
- "tracing",
- "tracing-subscriber",
-]
-
-[[package]]
-name = "lru"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
-
-[[package]]
-name = "lru-cache"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
-dependencies = [
- "linked-hash-map",
-]
-
-[[package]]
-name = "lru_time_cache"
-version = "0.11.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
-
-[[package]]
-name = "mailparse"
-version = "0.13.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32"
-dependencies = [
- "charset",
- "data-encoding",
- "quoted_printable 0.4.6",
-]
-
-[[package]]
-name = "mailparse"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3da03d5980411a724e8aaf7b61a7b5e386ec55a7fb49ee3d0ff79efc7e5e7c7e"
-dependencies = [
- "charset",
- "data-encoding",
- "quoted_printable 0.5.0",
-]
-
-[[package]]
-name = "mainline"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b751ffb57303217bcae8f490eee6044a5b40eadf6ca05ff476cad37e7b7970d"
-dependencies = [
- "bytes",
- "crc",
- "ed25519-dalek",
- "flume",
- "lru",
- "rand 0.8.5",
- "serde",
- "serde_bencode",
- "serde_bytes",
- "sha1_smol",
- "thiserror 1.0.58",
- "tracing",
-]
-
-[[package]]
-name = "match_cfg"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
-
-[[package]]
-name = "matchers"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
-dependencies = [
- "regex-automata 0.1.10",
-]
-
-[[package]]
-name = "md-5"
-version = "0.10.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca"
-dependencies = [
- "digest",
-]
-
-[[package]]
-name = "md5"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
-
-[[package]]
-name = "memalloc"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1"
-
-[[package]]
-name = "memchr"
-version = "2.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
-
-[[package]]
-name = "mime"
-version = "0.3.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
-
-[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
-name = "miniz_oxide"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
-dependencies = [
- "adler",
-]
-
-[[package]]
-name = "mio"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
-dependencies = [
- "hermit-abi",
- "libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "mutate_once"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b"
-
-[[package]]
-name = "nanorand"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
-dependencies = [
- "getrandom 0.2.11",
-]
-
-[[package]]
-name = "native-tls"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
-dependencies = [
- "lazy_static",
- "libc",
- "log",
- "openssl",
- "openssl-probe",
- "openssl-sys",
- "schannel",
- "security-framework",
- "security-framework-sys",
- "tempfile",
-]
-
-[[package]]
-name = "netdev"
-version = "0.30.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7516ad2c46cc25da098ed7d6b9a0cbe9e1fbffbd04b1596148b95f2841179c83"
-dependencies = [
- "dlopen2",
- "libc",
- "memalloc",
- "netlink-packet-core",
- "netlink-packet-route",
- "netlink-sys",
- "once_cell",
- "system-configuration",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "netlink-packet-core"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4"
-dependencies = [
- "anyhow",
- "byteorder",
- "netlink-packet-utils",
-]
-
-[[package]]
-name = "netlink-packet-route"
-version = "0.17.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66"
-dependencies = [
- "anyhow",
- "bitflags 1.3.2",
- "byteorder",
- "libc",
- "netlink-packet-core",
- "netlink-packet-utils",
-]
-
-[[package]]
-name = "netlink-packet-utils"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34"
-dependencies = [
- "anyhow",
- "byteorder",
- "paste",
- "thiserror 1.0.58",
-]
-
-[[package]]
-name = "netlink-proto"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b33524dc0968bfad349684447bfce6db937a9ac3332a1fe60c0c5a5ce63f21"
-dependencies = [
- "bytes",
- "futures",
- "log",
- "netlink-packet-core",
- "netlink-sys",
- "thiserror 1.0.58",
- "tokio",
-]
-
-[[package]]
-name = "netlink-sys"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411"
-dependencies = [
- "bytes",
- "futures",
- "libc",
- "log",
- "tokio",
-]
-
-[[package]]
-name = "netwatch"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a639d52c0996ac640e2a7052a5265c8f71efdbdadc83188435ffc358b7ca931"
-dependencies = [
- "anyhow",
- "bytes",
- "derive_more",
- "futures-lite 2.5.0",
- "futures-sink",
- "futures-util",
- "libc",
- "netdev",
- "netlink-packet-core",
- "netlink-packet-route",
- "netlink-sys",
- "once_cell",
- "rtnetlink",
- "serde",
- "socket2 0.5.6",
- "thiserror 1.0.58",
- "time 0.3.36",
- "tokio",
- "tracing",
- "windows 0.51.1",
- "wmi",
-]
-
-[[package]]
-name = "nix"
-version = "0.26.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
-dependencies = [
- "bitflags 1.3.2",
- "cfg-if",
- "libc",
-]
-
-[[package]]
-name = "no-std-compat"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
-
-[[package]]
-name = "no-std-net"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
-
-[[package]]
-name = "nom"
-version = "7.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
-dependencies = [
- "memchr",
- "minimal-lexical",
-]
-
-[[package]]
-name = "nonzero_ext"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
-
-[[package]]
-name = "nu-ansi-term"
-version = "0.46.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
-dependencies = [
- "overload",
- "winapi",
-]
-
-[[package]]
-name = "num-bigint"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
-dependencies = [
- "num-integer",
- "num-traits",
-]
-
-[[package]]
-name = "num-bigint-dig"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905"
-dependencies = [
- "byteorder",
- "lazy_static",
- "libm",
- "num-integer",
- "num-iter",
- "num-traits",
- "rand 0.8.5",
- "serde",
- "smallvec",
- "zeroize",
-]
-
-[[package]]
-name = "num-conv"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
-
-[[package]]
-name = "num-derive"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "num-integer"
-version = "0.1.46"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
-dependencies = [
- "num-traits",
-]
-
-[[package]]
-name = "num-iter"
-version = "0.1.43"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
-dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
-]
-
-[[package]]
-name = "num-traits"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
-dependencies = [
- "autocfg",
- "libm",
-]
-
-[[package]]
-name = "num_cpus"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
-dependencies = [
- "hermit-abi",
- "libc",
-]
-
-[[package]]
-name = "num_enum"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
-dependencies = [
- "num_enum_derive",
-]
-
-[[package]]
-name = "num_enum_derive"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
-dependencies = [
- "proc-macro-crate",
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "object"
-version = "0.30.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "239da7f290cfa979f43f85a8efeee9a8a76d0827c356d37f9d3d7254d6b537fb"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "ocb3"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb"
-dependencies = [
- "aead",
- "cipher",
- "ctr",
- "subtle",
-]
-
-[[package]]
-name = "oid-registry"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c958dd45046245b9c3c2547369bb634eb461670b2e7e0de552905801a648d1d"
-dependencies = [
- "asn1-rs",
-]
-
-[[package]]
-name = "once_cell"
-version = "1.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
-
-[[package]]
-name = "opaque-debug"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
-
-[[package]]
-name = "openssl"
-version = "0.10.66"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
-dependencies = [
- "bitflags 2.6.0",
- "cfg-if",
- "foreign-types",
- "libc",
- "once_cell",
- "openssl-macros",
- "openssl-sys",
-]
-
-[[package]]
-name = "openssl-macros"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.107",
-]
-
-[[package]]
-name = "openssl-probe"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
-
-[[package]]
-name = "openssl-src"
-version = "300.3.1+3.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "openssl-sys"
-version = "0.9.103"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
-dependencies = [
- "cc",
- "libc",
- "openssl-src",
- "pkg-config",
- "vcpkg",
-]
-
-[[package]]
-name = "os_pipe"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209"
-dependencies = [
- "libc",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "overload"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
-
-[[package]]
-name = "p256"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc7304b8213f8597952b2f4c3d7f09947d46bb677801df8f287ffd2c4e26f9da"
-dependencies = [
- "ecdsa",
- "elliptic-curve",
- "primeorder",
- "sha2",
-]
-
-[[package]]
-name = "p384"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
-dependencies = [
- "ecdsa",
- "elliptic-curve",
- "primeorder",
- "sha2",
-]
-
-[[package]]
-name = "p521"
-version = "0.13.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
-dependencies = [
- "base16ct",
- "ecdsa",
- "elliptic-curve",
- "primeorder",
- "rand_core 0.6.4",
- "sha2",
-]
-
-[[package]]
-name = "parking"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
-
-[[package]]
-name = "parking_lot"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-sys 0.42.0",
-]
-
-[[package]]
-name = "password-hash"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
-dependencies = [
- "base64ct",
- "rand_core 0.6.4",
- "subtle",
-]
-
-[[package]]
-name = "paste"
-version = "1.0.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
-
-[[package]]
-name = "pem"
-version = "3.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
-dependencies = [
- "base64 0.22.1",
- "serde",
-]
-
-[[package]]
-name = "pem-rfc7468"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
-dependencies = [
- "base64ct",
-]
-
-[[package]]
-name = "percent-encoding"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
-
-[[package]]
-name = "pest"
-version = "2.7.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
-dependencies = [
- "memchr",
- "thiserror 1.0.58",
- "ucd-trie",
-]
-
-[[package]]
-name = "pest_derive"
-version = "2.7.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a"
-dependencies = [
- "pest",
- "pest_generator",
-]
-
-[[package]]
-name = "pest_generator"
-version = "2.7.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183"
-dependencies = [
- "pest",
- "pest_meta",
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "pest_meta"
-version = "2.7.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f"
-dependencies = [
- "once_cell",
- "pest",
- "sha2",
-]
-
-[[package]]
-name = "pgp"
-version = "0.14.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1877a97fd422433220ad272eb008ec55691944b1200e9eb204e3cb2cb69d34e9"
-dependencies = [
- "aes",
- "aes-gcm",
- "aes-kw",
- "argon2",
- "base64 0.22.1",
- "bitfield",
- "block-padding",
- "blowfish",
- "bstr",
- "buffer-redux",
- "byteorder",
- "camellia",
- "cast5",
- "cfb-mode",
- "chrono",
- "cipher",
- "const-oid",
- "crc24",
- "curve25519-dalek",
- "derive_builder",
- "derive_more",
- "des",
- "digest",
- "dsa",
- "eax",
- "ecdsa",
- "ed25519-dalek",
- "elliptic-curve",
- "flate2",
- "generic-array",
- "hex",
- "hkdf",
- "idea",
- "iter-read",
- "k256",
- "log",
- "md-5",
- "nom",
- "num-bigint-dig",
- "num-traits",
- "num_enum",
- "ocb3",
- "p256",
- "p384",
- "p521",
- "rand 0.8.5",
- "ripemd",
- "rsa",
- "sha1",
- "sha1-checked",
- "sha2",
- "sha3",
- "signature",
- "smallvec",
- "thiserror 1.0.58",
- "twofish",
- "x25519-dalek",
- "x448",
- "zeroize",
-]
-
-[[package]]
-name = "pin-project"
-version = "1.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95"
-dependencies = [
- "pin-project-internal",
-]
-
-[[package]]
-name = "pin-project-internal"
-version = "1.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "pin-project-lite"
-version = "0.2.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
-
-[[package]]
-name = "pin-utils"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
-
-[[package]]
-name = "pkarr"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4548c673cbf8c91b69f7a17d3a042710aa73cffe5e82351db5378f26c3be64d8"
-dependencies = [
- "bytes",
- "document-features",
- "dyn-clone",
- "ed25519-dalek",
- "flume",
- "futures",
- "js-sys",
- "lru",
- "mainline",
- "self_cell",
- "simple-dns",
- "thiserror 1.0.58",
- "tracing",
- "ureq",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "z32",
-]
-
-[[package]]
-name = "pkcs1"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
-dependencies = [
- "der",
- "pkcs8",
- "spki",
-]
-
-[[package]]
-name = "pkcs8"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
-dependencies = [
- "der",
- "spki",
-]
-
-[[package]]
-name = "pkg-config"
-version = "0.3.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
-
-[[package]]
-name = "pnet_base"
-version = "0.34.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c"
-dependencies = [
- "no-std-net",
-]
-
-[[package]]
-name = "pnet_macros"
-version = "0.34.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804"
-dependencies = [
- "proc-macro2",
- "quote",
- "regex",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "pnet_macros_support"
-version = "0.34.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56"
-dependencies = [
- "pnet_base",
-]
-
-[[package]]
-name = "pnet_packet"
-version = "0.34.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba"
-dependencies = [
- "glob",
- "pnet_base",
- "pnet_macros",
- "pnet_macros_support",
-]
-
-[[package]]
-name = "png"
-version = "0.17.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
-dependencies = [
- "bitflags 1.3.2",
- "crc32fast",
- "flate2",
- "miniz_oxide",
-]
-
-[[package]]
-name = "poly1305"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
-dependencies = [
- "cpufeatures",
- "opaque-debug",
- "universal-hash",
-]
-
-[[package]]
-name = "polyval"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "opaque-debug",
- "universal-hash",
-]
-
-[[package]]
-name = "portable-atomic"
-version = "1.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265"
-
-[[package]]
-name = "portmapper"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93d60045fdcfe8ff6b781cf1027fdbb08ed319d93aff7da4bedc018e3bc92226"
-dependencies = [
- "anyhow",
- "base64 0.22.1",
- "bytes",
- "derive_more",
- "futures-lite 2.5.0",
- "futures-util",
- "igd-next",
- "iroh-metrics",
- "libc",
- "netwatch",
- "num_enum",
- "rand 0.8.5",
- "serde",
- "smallvec",
- "socket2 0.5.6",
- "thiserror 1.0.58",
- "time 0.3.36",
- "tokio",
- "tokio-util",
- "tracing",
- "url",
-]
-
-[[package]]
-name = "postcard"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfa512cd0d087cc9f99ad30a1bf64795b67871edbead083ffc3a4dfafa59aa00"
-dependencies = [
- "cobs",
- "const_format",
- "postcard-derive",
- "serde",
-]
-
-[[package]]
-name = "postcard-derive"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc4b01218787dd4420daf63875163a787a78294ad48a24e9f6fa8c6507759a79"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.107",
-]
-
-[[package]]
-name = "powerfmt"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
-
-[[package]]
-name = "ppv-lite86"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
-
-[[package]]
-name = "precis-core"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d73e9dd26361c32e7cd13d1032bb01c4e26a23287274e8a4e2f228cf2c9ff77b"
-dependencies = [
- "precis-tools",
- "ucd-parse",
- "unicode-normalization",
-]
-
-[[package]]
-name = "precis-profiles"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bde4bd6624c60cb0abe2bea1dbdbb9085f629a853861e64df4abb099f8076ad4"
-dependencies = [
- "lazy_static",
- "precis-core",
- "precis-tools",
- "unicode-normalization",
-]
-
-[[package]]
-name = "precis-tools"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d07ecadec70b0f560f09abf815ae0ee1a940d38d2354c938ba7229ac7c9f5f52"
-dependencies = [
- "lazy_static",
- "regex",
- "ucd-parse",
-]
-
-[[package]]
-name = "pretty-hex"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
-
-[[package]]
-name = "primeorder"
-version = "0.13.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
-dependencies = [
- "elliptic-curve",
-]
-
-[[package]]
-name = "proc-macro-crate"
-version = "3.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
-dependencies = [
- "toml_edit",
-]
-
-[[package]]
-name = "proc-macro-error"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7"
-dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn 1.0.107",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro-error-attr"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.107",
- "syn-mid",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro-hack"
-version = "0.5.20+deprecated"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "prometheus-client"
-version = "0.22.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca"
-dependencies = [
- "dtoa",
- "itoa",
- "parking_lot",
- "prometheus-client-derive-encode",
-]
-
-[[package]]
-name = "prometheus-client-derive-encode"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "qrcodegen"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
-
-[[package]]
-name = "quanta"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5"
-dependencies = [
- "crossbeam-utils",
- "libc",
- "once_cell",
- "raw-cpuid",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "web-sys",
- "winapi",
-]
-
-[[package]]
-name = "quick-error"
-version = "1.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
-
-[[package]]
-name = "quick-error"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
-
-[[package]]
-name = "quick-xml"
-version = "0.37.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "quinn"
-version = "0.11.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad"
-dependencies = [
- "bytes",
- "pin-project-lite",
- "quinn-proto",
- "quinn-udp",
- "rustc-hash 1.1.0",
- "rustls",
- "thiserror 1.0.58",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "quinn-proto"
-version = "0.11.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d"
-dependencies = [
- "bytes",
- "getrandom 0.2.11",
- "rand 0.8.5",
- "ring",
- "rustc-hash 2.0.0",
- "rustls",
- "rustls-pki-types",
- "slab",
- "thiserror 2.0.6",
- "tinyvec",
- "tracing",
- "web-time",
-]
-
-[[package]]
-name = "quinn-udp"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25a78e6f726d84fcf960409f509ae354a32648f090c8d32a2ea8b1a1bc3bab14"
-dependencies = [
- "libc",
- "once_cell",
- "socket2 0.5.6",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "quoted-string-parser"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dc75379cdb451d001f1cb667a9f74e8b355e9df84cc5193513cbe62b96fc5e9"
-dependencies = [
- "pest",
- "pest_derive",
-]
-
-[[package]]
-name = "quoted_printable"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb"
-
-[[package]]
-name = "quoted_printable"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0"
-
-[[package]]
-name = "radium"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
-
-[[package]]
-name = "rand"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
-dependencies = [
- "getrandom 0.1.16",
- "libc",
- "rand_chacha 0.2.2",
- "rand_core 0.5.1",
- "rand_hc",
-]
-
-[[package]]
-name = "rand"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
-dependencies = [
- "libc",
- "rand_chacha 0.3.1",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.5.1",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
-dependencies = [
- "getrandom 0.1.16",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
-dependencies = [
- "getrandom 0.2.11",
-]
-
-[[package]]
-name = "rand_hc"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
-dependencies = [
- "rand_core 0.5.1",
-]
-
-[[package]]
-name = "ratelimit"
-version = "1.0.0"
-
-[[package]]
-name = "raw-cpuid"
-version = "11.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d"
-dependencies = [
- "bitflags 2.6.0",
-]
-
-[[package]]
-name = "rcgen"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1"
-dependencies = [
- "pem",
- "ring",
- "time 0.3.36",
- "yasna",
-]
-
-[[package]]
-name = "redox_syscall"
-version = "0.2.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
-dependencies = [
- "bitflags 1.3.2",
-]
-
-[[package]]
-name = "regex"
-version = "1.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata 0.4.6",
- "regex-syntax 0.8.2",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.28",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax 0.8.2",
-]
-
-[[package]]
-name = "regex-lite"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
-
-[[package]]
-name = "regex-syntax"
-version = "0.6.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
-
-[[package]]
-name = "reqwest"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
-dependencies = [
- "base64 0.22.1",
- "bytes",
- "futures-core",
- "futures-util",
- "http 1.1.0",
- "http-body",
- "http-body-util",
- "hyper",
- "hyper-rustls",
- "hyper-util",
- "ipnet",
- "js-sys",
- "log",
- "mime",
- "once_cell",
- "percent-encoding",
- "pin-project-lite",
- "quinn",
- "rustls",
- "rustls-pemfile",
- "rustls-pki-types",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "sync_wrapper",
- "tokio",
- "tokio-rustls",
- "tower-service",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "webpki-roots",
- "winreg 0.52.0",
-]
-
-[[package]]
-name = "resolv-conf"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
-dependencies = [
- "hostname",
- "quick-error 1.2.3",
-]
-
-[[package]]
-name = "rfc6979"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
-dependencies = [
- "hmac",
- "subtle",
-]
-
-[[package]]
-name = "ring"
-version = "0.17.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866"
-dependencies = [
- "cc",
- "getrandom 0.2.11",
- "libc",
- "spin 0.9.8",
- "untrusted",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "ring-compat"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccce7bae150b815f0811db41b8312fcb74bffa4cab9cee5429ee00f356dd5bd4"
-dependencies = [
- "aead",
- "digest",
- "ecdsa",
- "ed25519",
- "generic-array",
- "p256",
- "p384",
- "pkcs8",
- "rand_core 0.6.4",
- "ring",
- "signature",
-]
-
-[[package]]
-name = "ripemd"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
-dependencies = [
- "digest",
-]
-
-[[package]]
-name = "rsa"
-version = "0.9.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
-dependencies = [
- "const-oid",
- "digest",
- "num-bigint-dig",
- "num-integer",
- "num-traits",
- "pkcs1",
- "pkcs8",
- "rand_core 0.6.4",
- "sha2",
- "signature",
- "spki",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "rtnetlink"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0"
-dependencies = [
- "futures",
- "log",
- "netlink-packet-core",
- "netlink-packet-route",
- "netlink-packet-utils",
- "netlink-proto",
- "netlink-sys",
- "nix",
- "thiserror 1.0.58",
- "tokio",
-]
-
-[[package]]
-name = "rusqlite"
-version = "0.32.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
-dependencies = [
- "bitflags 2.6.0",
- "fallible-iterator",
- "fallible-streaming-iterator",
- "hashlink",
- "libsqlite3-sys",
- "smallvec",
-]
-
-[[package]]
-name = "rust-hsluv"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efe2374f2385cdd8755a446f80b2a646de603c9d8539ca38734879b5c71e378b"
-
-[[package]]
-name = "rustc-demangle"
-version = "0.1.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
-
-[[package]]
-name = "rustc-hash"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
-
-[[package]]
-name = "rustc-hash"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
-
-[[package]]
-name = "rustc_version"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
-dependencies = [
- "semver",
-]
-
-[[package]]
-name = "rusticata-macros"
-version = "4.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
-dependencies = [
- "nom",
-]
-
-[[package]]
-name = "rustix"
-version = "0.36.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
-dependencies = [
- "bitflags 1.3.2",
- "errno 0.2.8",
- "io-lifetimes",
- "libc",
- "linux-raw-sys 0.1.4",
- "windows-sys 0.42.0",
-]
-
-[[package]]
-name = "rustix"
-version = "0.38.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
-dependencies = [
- "bitflags 2.6.0",
- "errno 0.3.3",
- "libc",
- "linux-raw-sys 0.4.7",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "rustls"
-version = "0.23.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1"
-dependencies = [
- "log",
- "once_cell",
- "ring",
- "rustls-pki-types",
- "rustls-webpki",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "rustls-native-certs"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
-dependencies = [
- "openssl-probe",
- "rustls-pemfile",
- "rustls-pki-types",
- "schannel",
- "security-framework",
-]
-
-[[package]]
-name = "rustls-pemfile"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab"
-dependencies = [
- "base64 0.21.7",
- "rustls-pki-types",
-]
-
-[[package]]
-name = "rustls-pki-types"
-version = "1.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
-dependencies = [
- "web-time",
-]
-
-[[package]]
-name = "rustls-platform-verifier"
-version = "0.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490"
-dependencies = [
- "core-foundation",
- "core-foundation-sys",
- "jni",
- "log",
- "once_cell",
- "rustls",
- "rustls-native-certs",
- "rustls-platform-verifier-android",
- "rustls-webpki",
- "security-framework",
- "security-framework-sys",
- "webpki-roots",
- "winapi",
-]
-
-[[package]]
-name = "rustls-platform-verifier-android"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
-
-[[package]]
-name = "rustls-webpki"
-version = "0.102.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
-dependencies = [
- "ring",
- "rustls-pki-types",
- "untrusted",
-]
-
-[[package]]
-name = "rustversion"
-version = "1.0.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
-
-[[package]]
-name = "ryu"
-version = "1.0.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
-
-[[package]]
-name = "safemem"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
-
-[[package]]
-name = "salsa20"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
-dependencies = [
- "cipher",
-]
-
-[[package]]
-name = "same-file"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
-name = "sanitize-filename"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603"
-dependencies = [
- "lazy_static",
- "regex",
-]
-
-[[package]]
-name = "schannel"
-version = "0.1.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
-dependencies = [
- "lazy_static",
- "windows-sys 0.36.1",
-]
-
-[[package]]
-name = "scoped-tls"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
-
-[[package]]
-name = "scopeguard"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
-
-[[package]]
-name = "scratch"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
-
-[[package]]
-name = "sec1"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48518a2b5775ba8ca5b46596aae011caa431e6ce7e4a67ead66d92f08884220e"
-dependencies = [
- "base16ct",
- "der",
- "generic-array",
- "pkcs8",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "security-framework"
-version = "2.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
-dependencies = [
- "bitflags 2.6.0",
- "core-foundation",
- "core-foundation-sys",
- "libc",
- "num-bigint",
- "security-framework-sys",
-]
-
-[[package]]
-name = "security-framework-sys"
-version = "2.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "self_cell"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6"
-
-[[package]]
-name = "semver"
-version = "1.0.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
-
-[[package]]
-name = "sendfd"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "604b71b8fc267e13bb3023a2c901126c8f349393666a6d98ac1ae5729b701798"
-dependencies = [
- "libc",
- "tokio",
-]
-
-[[package]]
-name = "serde"
-version = "1.0.204"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde-error"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "342110fb7a5d801060c885da03bf91bfa7c7ca936deafcc64bb6706375605d47"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "serde_bencode"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e"
-dependencies = [
- "serde",
- "serde_bytes",
-]
-
-[[package]]
-name = "serde_bytes"
-version = "0.11.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.204"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "serde_json"
-version = "1.0.91"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
-dependencies = [
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
-name = "serde_spanned"
-version = "0.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
-dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
-name = "serdect"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177"
-dependencies = [
- "base16ct",
- "serde",
-]
-
-[[package]]
-name = "sha-1"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "sha1"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "sha1-checked"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423"
-dependencies = [
- "digest",
- "sha1",
- "zeroize",
-]
-
-[[package]]
-name = "sha1_smol"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
-
-[[package]]
-name = "sha2"
-version = "0.10.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "sha3"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9"
-dependencies = [
- "digest",
- "keccak",
-]
-
-[[package]]
-name = "shadowsocks"
-version = "1.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ecb3780dfbc654de9383758015b9bb95c6e32fecace36ebded09d67e854d130"
-dependencies = [
- "aes",
- "async-trait",
- "base64 0.22.1",
- "blake3",
- "byte_string",
- "bytes",
- "cfg-if",
- "futures",
- "libc",
- "log",
- "lru_time_cache",
- "once_cell",
- "percent-encoding",
- "pin-project",
- "rand 0.8.5",
- "sendfd",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "shadowsocks-crypto",
- "socket2 0.5.6",
- "spin 0.9.8",
- "thiserror 1.0.58",
- "tokio",
- "tokio-tfo",
- "url",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "shadowsocks-crypto"
-version = "0.5.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc77ecb3a97509d22751b76665894fcffad2d10df8758f4e3f20c92ccde6bf4f"
-dependencies = [
- "aes",
- "aes-gcm",
- "blake3",
- "bytes",
- "cfg-if",
- "chacha20poly1305",
- "hkdf",
- "md-5",
- "rand 0.8.5",
- "ring-compat",
- "sha1",
-]
-
-[[package]]
-name = "sharded-slab"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
-dependencies = [
- "lazy_static",
-]
-
-[[package]]
-name = "shared_child"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef"
-dependencies = [
- "libc",
- "winapi",
-]
-
-[[package]]
-name = "shlex"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
-[[package]]
-name = "signal-hook-registry"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "signature"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
-dependencies = [
- "digest",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "simple-dns"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958"
-dependencies = [
- "bitflags 2.6.0",
-]
-
-[[package]]
-name = "slab"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "smallvec"
-version = "1.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
-
-[[package]]
-name = "smawk"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
-
-[[package]]
-name = "socket2"
-version = "0.4.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
-dependencies = [
- "libc",
- "winapi",
-]
-
-[[package]]
-name = "socket2"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
-dependencies = [
- "libc",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "spin"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
-
-[[package]]
-name = "spin"
-version = "0.9.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
-dependencies = [
- "lock_api",
-]
-
-[[package]]
-name = "spinning_top"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
-dependencies = [
- "lock_api",
-]
-
-[[package]]
-name = "spki"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
-dependencies = [
- "base64ct",
- "der",
-]
-
-[[package]]
-name = "ssh-cipher"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
-dependencies = [
- "cipher",
- "ssh-encoding",
-]
-
-[[package]]
-name = "ssh-encoding"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
-dependencies = [
- "base64ct",
- "pem-rfc7468",
- "sha2",
-]
-
-[[package]]
-name = "ssh-key"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b0a17fec6ea344bfa1cda3aed2f0696fddc6295cfcc8c454a3bf58b8ffaabeb"
-dependencies = [
- "ed25519-dalek",
- "p256",
- "p384",
- "rand_core 0.6.4",
- "rsa",
- "sec1",
- "sha2",
- "signature",
- "ssh-cipher",
- "ssh-encoding",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "stop-token"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b"
-dependencies = [
- "async-channel 1.8.0",
- "cfg-if",
- "futures-core",
- "pin-project-lite",
-]
-
-[[package]]
-name = "strsim"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
-
-[[package]]
-name = "struct_iterable"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "849a064c6470a650b72e41fa6c057879b68f804d113af92900f27574828e7712"
-dependencies = [
- "struct_iterable_derive",
- "struct_iterable_internal",
-]
-
-[[package]]
-name = "struct_iterable_derive"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8bb939ce88a43ea4e9d012f2f6b4cc789deb2db9d47bad697952a85d6978662c"
-dependencies = [
- "erased-serde",
- "proc-macro2",
- "quote",
- "struct_iterable_internal",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "struct_iterable_internal"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9426b2a0c03e6cc2ea8dbc0168dbbf943f88755e409fb91bcb8f6a268305f4a"
-
-[[package]]
-name = "strum"
-version = "0.26.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
-dependencies = [
- "strum_macros",
-]
-
-[[package]]
-name = "strum_macros"
-version = "0.26.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
-dependencies = [
- "heck 0.5.0",
- "proc-macro2",
- "quote",
- "rustversion",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "stun-rs"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0adebf9fb8fba5c39ee34092b0383f247e4d1255b98fcffec94b4b797b85b677"
-dependencies = [
- "base64 0.22.1",
- "bounded-integer",
- "byteorder",
- "crc",
- "enumflags2",
- "fallible-iterator",
- "hmac-sha1",
- "hmac-sha256",
- "hostname-validator",
- "lazy_static",
- "md5",
- "paste",
- "precis-core",
- "precis-profiles",
- "quoted-string-parser",
- "rand 0.8.5",
-]
-
-[[package]]
-name = "subtle"
-version = "2.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
-
-[[package]]
-name = "surge-ping"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efbf95ce4c7c5b311d2ce3f088af2b93edef0f09727fa50fbe03c7a979afce77"
-dependencies = [
- "hex",
- "parking_lot",
- "pnet_packet",
- "rand 0.8.5",
- "socket2 0.5.6",
- "thiserror 1.0.58",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "syn"
-version = "1.0.107"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.90"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "syn-mid"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.107",
-]
-
-[[package]]
-name = "sync_wrapper"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
-
-[[package]]
-name = "synstructure"
-version = "0.12.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.107",
- "unicode-xid",
-]
-
-[[package]]
-name = "synstructure"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "system-configuration"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42"
-dependencies = [
- "bitflags 2.6.0",
- "core-foundation",
- "system-configuration-sys",
-]
-
-[[package]]
-name = "system-configuration-sys"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "tagger"
-version = "4.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6aaa6f5d645d1dae4cd0286e9f8bf15b75a31656348e5e106eb1a940abd34b63"
-
-[[package]]
-name = "tap"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
-
-[[package]]
-name = "tempfile"
-version = "3.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95"
-dependencies = [
- "cfg-if",
- "fastrand 1.8.0",
- "redox_syscall",
- "rustix 0.36.7",
- "windows-sys 0.42.0",
-]
-
-[[package]]
-name = "termcolor"
-version = "1.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
-name = "textwrap"
-version = "0.16.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
-dependencies = [
- "smawk",
- "unicode-linebreak",
- "unicode-width",
-]
-
-[[package]]
-name = "thiserror"
-version = "1.0.58"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
-dependencies = [
- "thiserror-impl 1.0.58",
-]
-
-[[package]]
-name = "thiserror"
-version = "2.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
-dependencies = [
- "thiserror-impl 2.0.6",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.58"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "2.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "thread_local"
-version = "1.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
-dependencies = [
- "cfg-if",
- "once_cell",
-]
-
-[[package]]
-name = "time"
-version = "0.1.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
-dependencies = [
- "libc",
- "wasi 0.10.0+wasi-snapshot-preview1",
- "winapi",
-]
-
-[[package]]
-name = "time"
-version = "0.3.36"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
-dependencies = [
- "deranged",
- "itoa",
- "num-conv",
- "powerfmt",
- "serde",
- "time-core",
- "time-macros",
-]
-
-[[package]]
-name = "time-core"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
-
-[[package]]
-name = "time-macros"
-version = "0.2.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
-dependencies = [
- "num-conv",
- "time-core",
-]
-
-[[package]]
-name = "tinyvec"
-version = "1.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
-
-[[package]]
-name = "tokio"
-version = "1.41.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
-dependencies = [
- "backtrace",
- "bytes",
- "libc",
- "mio",
- "parking_lot",
- "pin-project-lite",
- "signal-hook-registry",
- "socket2 0.5.6",
- "tokio-macros",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "tokio-io-timeout"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
-dependencies = [
- "pin-project-lite",
- "tokio",
-]
-
-[[package]]
-name = "tokio-macros"
-version = "2.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "tokio-rustls"
-version = "0.26.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
-dependencies = [
- "rustls",
- "rustls-pki-types",
- "tokio",
-]
-
-[[package]]
-name = "tokio-stream"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
-dependencies = [
- "futures-core",
- "pin-project-lite",
- "tokio",
-]
-
-[[package]]
-name = "tokio-tar"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a50188549787c32c1c3d9c8c71ad7e003ccf2f102489c5a96e385c84760477f4"
-dependencies = [
- "filetime",
- "futures-core",
- "libc",
- "redox_syscall",
- "tokio",
- "tokio-stream",
- "xattr",
-]
-
-[[package]]
-name = "tokio-tfo"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb4382c6371e29365853d2b71e915d5398df46312a2158097d8bb3f54d0f1b4"
-dependencies = [
- "cfg-if",
- "futures",
- "libc",
- "log",
- "once_cell",
- "pin-project",
- "socket2 0.5.6",
- "tokio",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "tokio-tungstenite"
-version = "0.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
-dependencies = [
- "futures-util",
- "log",
- "tokio",
- "tungstenite",
-]
-
-[[package]]
-name = "tokio-tungstenite-wasm"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e57a65894797a018b28345fa298a00c450a574aa9671e50b18218a6292a55ac"
-dependencies = [
- "futures-channel",
- "futures-util",
- "http 1.1.0",
- "httparse",
- "js-sys",
- "thiserror 1.0.58",
- "tokio",
- "tokio-tungstenite",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
-name = "tokio-util"
-version = "0.7.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
-dependencies = [
- "bytes",
- "futures-core",
- "futures-io",
- "futures-sink",
- "futures-util",
- "hashbrown 0.14.3",
- "pin-project-lite",
- "tokio",
-]
-
-[[package]]
-name = "toml"
-version = "0.8.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
-dependencies = [
- "serde",
- "serde_spanned",
- "toml_datetime",
- "toml_edit",
-]
-
-[[package]]
-name = "toml_datetime"
-version = "0.6.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "toml_edit"
-version = "0.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
-dependencies = [
- "indexmap",
- "serde",
- "serde_spanned",
- "toml_datetime",
- "winnow",
-]
-
-[[package]]
-name = "tower-service"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
-
-[[package]]
-name = "tracing"
-version = "0.1.40"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
-dependencies = [
- "log",
- "pin-project-lite",
- "tracing-attributes",
- "tracing-core",
-]
-
-[[package]]
-name = "tracing-attributes"
-version = "0.1.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "tracing-core"
-version = "0.1.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
-dependencies = [
- "once_cell",
- "valuable",
-]
-
-[[package]]
-name = "tracing-log"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
-dependencies = [
- "lazy_static",
- "log",
- "tracing-core",
-]
-
-[[package]]
-name = "tracing-subscriber"
-version = "0.3.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
-dependencies = [
- "matchers",
- "nu-ansi-term",
- "once_cell",
- "regex",
- "sharded-slab",
- "smallvec",
- "thread_local",
- "tracing",
- "tracing-core",
- "tracing-log",
-]
-
-[[package]]
-name = "try-lock"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
-
-[[package]]
-name = "ttl_cache"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4189890526f0168710b6ee65ceaedf1460c48a14318ceec933cb26baa492096a"
-dependencies = [
- "linked-hash-map",
-]
-
-[[package]]
-name = "tungstenite"
-version = "0.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
-dependencies = [
- "byteorder",
- "bytes",
- "data-encoding",
- "http 1.1.0",
- "httparse",
- "log",
- "rand 0.8.5",
- "sha1",
- "thiserror 1.0.58",
- "url",
- "utf-8",
-]
-
-[[package]]
-name = "twofish"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013"
-dependencies = [
- "cipher",
-]
-
-[[package]]
-name = "typenum"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
-
-[[package]]
-name = "ucd-parse"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06ff81122fcbf4df4c1660b15f7e3336058e7aec14437c9f85c6b31a0f279b9"
-dependencies = [
- "regex-lite",
-]
-
-[[package]]
-name = "ucd-trie"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
-
-[[package]]
-name = "unicode-bidi"
-version = "0.3.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
-
-[[package]]
-name = "unicode-linebreak"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
-dependencies = [
- "hashbrown 0.12.3",
- "regex",
-]
-
-[[package]]
-name = "unicode-normalization"
-version = "0.1.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
-dependencies = [
- "tinyvec",
-]
-
-[[package]]
-name = "unicode-width"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
-
-[[package]]
-name = "unicode-xid"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
-
-[[package]]
-name = "universal-hash"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
-dependencies = [
- "crypto-common",
- "subtle",
-]
-
-[[package]]
-name = "untrusted"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
-
-[[package]]
-name = "ureq"
-version = "2.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea"
-dependencies = [
- "base64 0.22.1",
- "log",
- "once_cell",
- "rustls",
- "rustls-pki-types",
- "url",
- "webpki-roots",
-]
-
-[[package]]
-name = "url"
-version = "2.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
-dependencies = [
- "form_urlencoded",
- "idna",
- "percent-encoding",
- "serde",
-]
-
-[[package]]
-name = "utf-8"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
-
-[[package]]
-name = "uuid"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
-dependencies = [
- "getrandom 0.2.11",
-]
-
-[[package]]
-name = "uuid"
-version = "1.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
-dependencies = [
- "getrandom 0.2.11",
- "serde",
-]
-
-[[package]]
-name = "valuable"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
-
-[[package]]
-name = "vcpkg"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
-
-[[package]]
-name = "version_check"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
-
-[[package]]
-name = "waker-fn"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
-
-[[package]]
-name = "walkdir"
-version = "2.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
-dependencies = [
- "same-file",
- "winapi-util",
-]
-
-[[package]]
-name = "want"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
-dependencies = [
- "log",
- "try-lock",
-]
-
-[[package]]
-name = "wasi"
-version = "0.9.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
-
-[[package]]
-name = "wasi"
-version = "0.10.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
-
-[[package]]
-name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
-
-[[package]]
-name = "wasm-bindgen"
-version = "0.2.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
-dependencies = [
- "cfg-if",
- "wasm-bindgen-macro",
-]
-
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
-dependencies = [
- "bumpalo",
- "log",
- "once_cell",
- "proc-macro2",
- "quote",
- "syn 2.0.90",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-futures"
-version = "0.4.42"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
-dependencies = [
- "cfg-if",
- "js-sys",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
-dependencies = [
- "quote",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
- "wasm-bindgen-backend",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.92"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
-
-[[package]]
-name = "watchable"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45b42a2f611916b5965120a9cde2b60f2db4454826dd9ad5e6f47c24a5b3b259"
-dependencies = [
- "event-listener 4.0.3",
- "futures-util",
- "parking_lot",
- "thiserror 1.0.58",
-]
-
-[[package]]
-name = "web-sys"
-version = "0.3.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "web-time"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "webpki-roots"
-version = "0.26.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
-dependencies = [
- "rustls-pki-types",
-]
-
-[[package]]
-name = "weezl"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
-
-[[package]]
-name = "widestring"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983"
-
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
-name = "winapi-util"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
-dependencies = [
- "winapi",
-]
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-[[package]]
-name = "windows"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
-dependencies = [
- "windows-targets 0.48.5",
-]
-
-[[package]]
-name = "windows"
-version = "0.51.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9"
-dependencies = [
- "windows-core 0.51.1",
- "windows-targets 0.48.5",
-]
-
-[[package]]
-name = "windows"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
-dependencies = [
- "windows-core 0.52.0",
- "windows-implement",
- "windows-interface",
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-core"
-version = "0.51.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
-dependencies = [
- "windows-targets 0.48.5",
-]
-
-[[package]]
-name = "windows-core"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-implement"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "windows-interface"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.36.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
-dependencies = [
- "windows_aarch64_msvc 0.36.1",
- "windows_i686_gnu 0.36.1",
- "windows_i686_msvc 0.36.1",
- "windows_x86_64_gnu 0.36.1",
- "windows_x86_64_msvc 0.36.1",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
-dependencies = [
- "windows_aarch64_gnullvm 0.42.0",
- "windows_aarch64_msvc 0.42.0",
- "windows_i686_gnu 0.42.0",
- "windows_i686_msvc 0.42.0",
- "windows_x86_64_gnu 0.42.0",
- "windows_x86_64_gnullvm 0.42.0",
- "windows_x86_64_msvc 0.42.0",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
-dependencies = [
- "windows-targets 0.48.5",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
-dependencies = [
- "windows_aarch64_gnullvm 0.48.5",
- "windows_aarch64_msvc 0.48.5",
- "windows_i686_gnu 0.48.5",
- "windows_i686_msvc 0.48.5",
- "windows_x86_64_gnu 0.48.5",
- "windows_x86_64_gnullvm 0.48.5",
- "windows_x86_64_msvc 0.48.5",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
-dependencies = [
- "windows_aarch64_gnullvm 0.52.6",
- "windows_aarch64_msvc 0.52.6",
- "windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm",
- "windows_i686_msvc 0.52.6",
- "windows_x86_64_gnu 0.52.6",
- "windows_x86_64_gnullvm 0.52.6",
- "windows_x86_64_msvc 0.52.6",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.36.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.36.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.36.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.36.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.36.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.42.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-
-[[package]]
-name = "winnow"
-version = "0.5.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "winreg"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
-dependencies = [
- "winapi",
-]
-
-[[package]]
-name = "winreg"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
-dependencies = [
- "cfg-if",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "wmi"
-version = "0.13.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc2f0a4062ca522aad4705a2948fd4061b3857537990202a8ddd5af21607f79a"
-dependencies = [
- "chrono",
- "futures",
- "log",
- "serde",
- "thiserror 1.0.58",
- "windows 0.52.0",
-]
-
-[[package]]
-name = "wyz"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
-dependencies = [
- "tap",
-]
-
-[[package]]
-name = "x25519-dalek"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
-dependencies = [
- "curve25519-dalek",
- "rand_core 0.6.4",
- "serde",
- "zeroize",
-]
-
-[[package]]
-name = "x448"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4cd07d4fae29e07089dbcacf7077cd52dce7760125ca9a4dd5a35ca603ffebb"
-dependencies = [
- "ed448-goldilocks",
- "hex",
- "rand_core 0.5.1",
-]
-
-[[package]]
-name = "x509-parser"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
-dependencies = [
- "asn1-rs",
- "data-encoding",
- "der-parser",
- "lazy_static",
- "nom",
- "oid-registry",
- "rusticata-macros",
- "thiserror 1.0.58",
- "time 0.3.36",
-]
-
-[[package]]
-name = "xattr"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "xml-rs"
-version = "0.8.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193"
-
-[[package]]
-name = "xmltree"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb"
-dependencies = [
- "xml-rs",
-]
-
-[[package]]
-name = "yasna"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
-dependencies = [
- "time 0.3.36",
-]
-
-[[package]]
-name = "z32"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edb37266251c28b03d08162174a91c3a092e3bd4f476f8205ee1c507b78b7bdc"
-
-[[package]]
-name = "zerocopy"
-version = "0.7.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
-dependencies = [
- "zerocopy-derive",
-]
-
-[[package]]
-name = "zerocopy-derive"
-version = "0.7.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.90",
-]
-
-[[package]]
-name = "zeroize"
-version = "1.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
-dependencies = [
- "zeroize_derive",
-]
-
-[[package]]
-name = "zeroize_derive"
-version = "1.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.107",
- "synstructure 0.12.6",
-]
-
-[[package]]
-name = "zune-core"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
-
-[[package]]
-name = "zune-jpeg"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768"
-dependencies = [
- "zune-core",
-]
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index a3eba6256b..2379025316 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -3,28 +3,21 @@ name = "deltachat-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
+license = "MPL-2.0"
[dev-dependencies]
bolero = "0.8"
[dependencies]
-mailparse = "0.13"
+mailparse = { workspace = true }
deltachat = { path = ".." }
format-flowed = { path = "../format-flowed" }
-[workspace]
-members = ["."]
-
[[test]]
name = "fuzz_dateparse"
path = "fuzz_targets/fuzz_dateparse.rs"
harness = false
-[[test]]
-name = "fuzz_simplify"
-path = "fuzz_targets/fuzz_simplify.rs"
-harness = false
-
[[test]]
name = "fuzz_mailparse"
path = "fuzz_targets/fuzz_mailparse.rs"
diff --git a/fuzz/fuzz_targets/fuzz_format_flowed.rs b/fuzz/fuzz_targets/fuzz_format_flowed.rs
index 8f779a4680..e89d8dd749 100644
--- a/fuzz/fuzz_targets/fuzz_format_flowed.rs
+++ b/fuzz/fuzz_targets/fuzz_format_flowed.rs
@@ -9,7 +9,7 @@ fn round_trip(input: &str) -> String {
fn main() {
check!().for_each(|data: &[u8]| {
- if let Ok(input) = std::str::from_utf8(data.into()) {
+ if let Ok(input) = std::str::from_utf8(data) {
let input = input.trim().to_string();
// Only consider inputs that are the result of unformatting format=flowed text.
diff --git a/fuzz/fuzz_targets/fuzz_simplify.rs b/fuzz/fuzz_targets/fuzz_simplify.rs
deleted file mode 100644
index cd0a22352d..0000000000
--- a/fuzz/fuzz_targets/fuzz_simplify.rs
+++ /dev/null
@@ -1,13 +0,0 @@
-use bolero::check;
-
-use deltachat::fuzzing::simplify;
-
-fn main() {
- check!().for_each(|data: &[u8]| match String::from_utf8(data.to_vec()) {
- Ok(input) => {
- simplify(input.clone(), true);
- simplify(input, false);
- }
- Err(_err) => {}
- });
-}
diff --git a/node/.prettierrc.yml b/node/.prettierrc.yml
deleted file mode 100644
index 5ca635d596..0000000000
--- a/node/.prettierrc.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-# .prettierrc
-trailingComma: es5
-tabWidth: 2
-semi: false
-singleQuote: true
-jsxSingleQuote: true
diff --git a/node/CONTRIBUTORS.md b/node/CONTRIBUTORS.md
deleted file mode 100644
index 1c81d4add7..0000000000
--- a/node/CONTRIBUTORS.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# Contributors
-
-| Name | GitHub |
-| :-------------------- | :----------------------------------------------- |
-| **Lars-Magnus Skog** | |
-| **jikstra** | |
-| **Simon Laux** | [**@Simon-Laux**](https://github.com/Simon-Laux) |
-| **Jikstra** | [**@Jikstra**](https://github.com/Jikstra) |
-| **Nico de Haen** | |
-| **B. Petersen** | |
-| **Karissa McKelvey** | [**@karissa**](https://github.com/karissa) |
-| **developer** | |
-| **Alexander Krotov** | |
-| **Floris Bruynooghe** | |
-| **lefherz** | |
-| **Pablo** | [**@pabzm**](https://github.com/pabzm) |
-| **pabzm** | |
-| **holger krekel** | |
-| **Robert Schütz** | |
-| **bb** | |
-| **Charles Paul** | |
diff --git a/node/LICENSE b/node/LICENSE
deleted file mode 100644
index 94a9ed024d..0000000000
--- a/node/LICENSE
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
- .
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
diff --git a/node/README.md b/node/README.md
deleted file mode 100644
index 93a0beb76b..0000000000
--- a/node/README.md
+++ /dev/null
@@ -1,260 +0,0 @@
-# deltachat-node
-
-> node.js bindings for [`deltachat-core-rust`](..)
-
-[](https://www.npmjs.com/package/deltachat-node)
-
-[](https://prettier.io)
-
-`deltachat-node` primarily aims to offer two things:
-
-- A high level JavaScript api with syntactic sugar
-- A low level c binding api around [`deltachat-core-rust`](..)
-
-This code used to live at [`deltachat-node`](https://github.com/deltachat/deltachat-node)
-
-## Table of Contents
-
-Click to expand
-
-- [Install](#install)
-- [Dependencies](#dependencies)
-- [Build from source](#build-from-source)
-- [Usage](#usage)
-- [Developing](#developing)
-- [License](#license)
-
-
-
-## Install
-
-By default the installation will try to use the bundled prebuilds in the
-npm package. If this fails it falls back to compile `../deltachat-core-rust` from
-this repository, using `scripts/rebuild-core.js`.
-
-To install from npm use:
-
-```
-npm install deltachat-node
-```
-
-## Dependencies
-
-- Nodejs >= `v18.0.0`
-- rustup (optional if you can't use the prebuilds)
-
-> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
-
-## Build from source
-
-If you want to build from source, make sure that you have `rustup` installed.
-You can either use `npm install deltachat-node --build-from-source` to force
-building from source or clone this repository and follow this steps:
-
-1. `git clone https://github.com/deltachat/deltachat-core-rust.git`
-2. `cd deltachat-core-rust`
-3. `npm i`
-4. `npm run build`
-
-> Our `package.json` file is located in the root directory of this repository,
-> not inside this folder. (We need this in order to include the rust source
-> code in the npm package.)
-
-### Use a git branch in deltachat-desktop
-
-You can directly install a core branch, but make sure:
-- that you have typescript in your project dependencies, as it is likely required
-- you know that there are **no prebuilds** and so core is built during installation which is why it takes so long
-
-```
-npm install https://github.com/deltachat/deltachat-core-rust.git#branch
-```
-
-If you want prebuilds for a branch that has a core pr, you might find an npm tar.gz package for that branch at .
-The github ci also posts a link to it in the checks for each pr.
-
-### Use build-from-source in deltachat-desktop
-
-If you want to use the manually built node bindings in the desktop client (for
-example), you can follow these instructions:
-
-First clone the
-[deltachat-desktop](https://github.com/deltachat/deltachat-desktop) repository,
-e.g. with `git clone https://github.com/deltachat/deltachat-desktop`.
-
-Then you need to make sure that this directory is referenced correctly in
-deltachat-desktop's package.json. You need to change
-`deltachat-desktop/package.json` like this:
-
-```
-diff --git i/package.json w/package.json
-index 45893894..5154512c 100644
---- i/package.json
-+++ w/package.json
-@@ -83,7 +83,7 @@
- "application-config": "^1.0.1",
- "classnames": "^2.3.1",
- "debounce": "^1.2.0",
-- "deltachat-node": "1.79.3",
-+ "deltachat-node": "file:../deltachat-core-rust/",
- "emoji-js-clean": "^4.0.0",
- "emoji-mart": "^3.0.1",
- "emoji-regex": "^9.2.2",
-```
-
-Then, in the `deltachat-desktop` repository, run:
-
-1. `npm i`
-2. `npm run build`
-3. And `npm run start` to start the newly built client.
-
-### Workaround to build for x86_64 on Apple's M1
-
-deltachat doesn't support universal (fat) binaries (that contain builds for both cpu architectures) yet, until it does you can use the following workaround to get x86_64 builds:
-
-```
-$ fnm install 19 --arch x64
-$ fnm use 19
-$ node -p process.arch
-# result should be x64
-$ rustup target add x86_64-apple-darwin
-$ git apply patches/m1_build_use_x86_64.patch
-$ CARGO_BUILD_TARGET=x86_64-apple-darwin npm run build
-$ npm run test
-```
-
-(when using [fnm](https://github.com/Schniz/fnm) instead of nvm, you can select the architecture)
-If your node and electron are already build for arm64 you can also try building for arm:
-
-```
-$ fnm install 18 --arch arm64
-$ fnm use 18
-$ node -p process.arch
-# result should be arm64
-$ npm_config_arch=arm64 npm run build
-$ npm run test
-```
-
-## Usage
-
-```js
-const { Context } = require('deltachat-node')
-
-const opts = {
- addr: '[email]',
- mail_pw: '[password]',
-}
-
-const contact = '[email]'
-
-async function main() {
- const dc = Context.open('./')
- dc.on('ALL', console.log.bind(null, 'core |'))
-
- try {
- await dc.configure(opts)
- } catch (err) {
- console.error('Failed to configure because of: ', err)
- dc.unref()
- return
- }
-
- dc.startIO()
- console.log('fully configured')
-
- const contactId = dc.createContact('Test', contact)
- const chatId = dc.createChatByContactId(contactId)
- dc.sendMessage(chatId, 'Hi!')
-
- console.log('sent message')
-
- dc.once('DC_EVENT_SMTP_MESSAGE_SENT', async () => {
- console.log('Message sent, shutting down...')
- dc.stopIO()
- console.log('stopped io')
- dc.unref()
- })
-}
-
-main()
-```
-this example can also be found in the examples folder [examples/send_message.js](./examples/send_message.js)
-
-### Generating Docs
-
-We are currently migrating to automatically generated documentation.
-You can find the old documentation at [old_docs](./old_docs).
-
-to generate the documentation, run:
-
-```
-npx typedoc
-```
-
-The resulting documentation can be found in the `docs/` folder.
-An online version can be found under [js.delta.chat](https://js.delta.chat).
-
-## Developing
-
-### Tests and Coverage
-
-Running `npm test` ends with showing a code coverage report, which is produced by [`nyc`](https://github.com/istanbuljs/nyc#readme).
-
-
-
-The coverage report from `nyc` in the console is rather limited. To get a more detailed coverage report you can run `npm run coverage-html-report`. This will produce a html report from the `nyc` data and display it in a browser on your local machine.
-
-To run the integration tests you need to set the `CHATMAIL_DOMAIN` environment variables. E.g.:
-
-```
-$ export CHATMAIL_DOMAIN=chat.example.org
-$ npm run test
-```
-
-### Scripts
-
-We have the following scripts for building, testing and coverage:
-
-- `npm run coverage` Creates a coverage report and passes it to `coveralls`. Only done by `Travis`.
-- `npm run coverage-html-report` Generates a html report from the coverage data and opens it in a browser on the local machine.
-- `npm run generate-constants` Generates `constants.js` and `events.js` based on the `deltachat-core-rust/deltachat-ffi/deltachat.h` header file.
-- `npm install` After dependencies are installed, runs `node-gyp-build` to see if the native code needs to be rebuilt.
-- `npm run build` Rebuilds all code.
-- `npm run build:core` Rebuilds code in `deltachat-core-rust`.
-- `npm run build:bindings` Rebuilds the bindings and links with `deltachat-core-rust`.
-- `ǹpm run clean` Removes all built code
-- `npm run prebuildify` Builds prebuilt binary to `prebuilds/$PLATFORM-$ARCH`. Copies `deltachat.dll` from `deltachat-core-rust` for windows.
-- `npm run download-prebuilds` Downloads all prebuilt binaries from github before `npm publish`.
-- `npm test` Runs `standard` and then the tests in `test/index.js`.
-- `npm run test-integration` Runs the integration tests.
-- `npm run hallmark` Runs `hallmark` on all markdown files.
-
-### Releases
-
-The following steps are needed to make a release:
-
-1. Wait until `pack-module` github action is completed
-2. Run `npm publish https://download.delta.chat/node/deltachat-node-1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
-
-## License
-
-Licensed under `GPL-3.0-or-later`, see [LICENSE](./LICENSE) file for details.
-
-> Copyright © 2018 `DeltaChat` contributors.
->
-> This program is free software: you can redistribute it and/or modify
-> it under the terms of the GNU General Public License as published by
-> the Free Software Foundation, either version 3 of the License, or
-> (at your option) any later version.
->
-> This program is distributed in the hope that it will be useful,
-> but WITHOUT ANY WARRANTY; without even the implied warranty of
-> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-> GNU General Public License for more details.
->
-> You should have received a copy of the GNU General Public License
-> along with this program. If not, see .
-
-[appveyor-shield]: https://ci.appveyor.com/api/projects/status/t0narp672wpbl6pd?svg=true
-
-[appveyor]: https://ci.appveyor.com/project/ralphtheninja/deltachat-node-d4bf8
diff --git a/node/binding.gyp b/node/binding.gyp
deleted file mode 100644
index b0d92eae99..0000000000
--- a/node/binding.gyp
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- # documentation about the format of this file can be found under https://gyp.gsrc.io/docs/InputFormatReference.md
- # Variables can be specified when calling node-gyp as so:
- # node-gyp configure -- -Dvarname=value
- "variables": {
- # Whether to use a system-wide installation of deltachat-core
- # using pkg-config. Set to either "true" or "false".
- "USE_SYSTEM_LIBDELTACHAT%": " {
- console.log('Message sent, shutting down...')
- dc.stopIO()
- console.log('stopped io')
- dc.unref()
- })
-}
-
-main()
diff --git a/node/images/tests.png b/node/images/tests.png
deleted file mode 100644
index 829295b38d..0000000000
Binary files a/node/images/tests.png and /dev/null differ
diff --git a/node/lib/binding.ts b/node/lib/binding.ts
deleted file mode 100644
index 275bf12ffd..0000000000
--- a/node/lib/binding.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { join } from 'path'
-
-/**
- * bindings are not typed yet.
- * if the available function names are required they can be found inside of `../src/module.c`
- */
-export const bindings: any = require('node-gyp-build')(join(__dirname, '../'))
-
-export default bindings
diff --git a/node/lib/chat.ts b/node/lib/chat.ts
deleted file mode 100644
index 49a6b8f520..0000000000
--- a/node/lib/chat.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/* eslint-disable camelcase */
-
-import binding from './binding'
-import rawDebug from 'debug'
-const debug = rawDebug('deltachat:node:chat')
-import { C } from './constants'
-import { integerToHexColor } from './util'
-import { ChatJSON } from './types'
-
-interface NativeChat {}
-/**
- * Wrapper around dc_chat_t*
- */
-
-export class Chat {
- constructor(public dc_chat: NativeChat) {
- debug('Chat constructor')
- if (dc_chat === null) {
- throw new Error('native chat can not be null')
- }
- }
-
- getVisibility():
- | C.DC_CHAT_VISIBILITY_NORMAL
- | C.DC_CHAT_VISIBILITY_ARCHIVED
- | C.DC_CHAT_VISIBILITY_PINNED {
- return binding.dcn_chat_get_visibility(this.dc_chat)
- }
-
- get color(): string {
- return integerToHexColor(binding.dcn_chat_get_color(this.dc_chat))
- }
-
- getId(): number {
- return binding.dcn_chat_get_id(this.dc_chat)
- }
-
- getName(): string {
- return binding.dcn_chat_get_name(this.dc_chat)
- }
-
- getMailinglistAddr(): string {
- return binding.dcn_chat_get_mailinglist_addr(this.dc_chat)
- }
-
- getProfileImage(): string {
- return binding.dcn_chat_get_profile_image(this.dc_chat)
- }
-
- getType(): number {
- return binding.dcn_chat_get_type(this.dc_chat)
- }
-
- isSelfTalk(): boolean {
- return Boolean(binding.dcn_chat_is_self_talk(this.dc_chat))
- }
-
- isContactRequest(): boolean {
- return Boolean(binding.dcn_chat_is_contact_request(this.dc_chat))
- }
-
- isUnpromoted(): boolean {
- return Boolean(binding.dcn_chat_is_unpromoted(this.dc_chat))
- }
-
- isProtected(): boolean {
- return Boolean(binding.dcn_chat_is_protected(this.dc_chat))
- }
-
- get canSend(): boolean {
- return Boolean(binding.dcn_chat_can_send(this.dc_chat))
- }
-
- isDeviceTalk(): boolean {
- return Boolean(binding.dcn_chat_is_device_talk(this.dc_chat))
- }
-
- isSingle(): boolean {
- return this.getType() === C.DC_CHAT_TYPE_SINGLE
- }
-
- isGroup(): boolean {
- return this.getType() === C.DC_CHAT_TYPE_GROUP
- }
-
- isMuted(): boolean {
- return Boolean(binding.dcn_chat_is_muted(this.dc_chat))
- }
-
- toJson(): ChatJSON {
- debug('toJson')
- const visibility = this.getVisibility()
- return {
- archived: visibility === C.DC_CHAT_VISIBILITY_ARCHIVED,
- pinned: visibility === C.DC_CHAT_VISIBILITY_PINNED,
- color: this.color,
- id: this.getId(),
- name: this.getName(),
- mailinglistAddr: this.getMailinglistAddr(),
- profileImage: this.getProfileImage(),
- type: this.getType(),
- isSelfTalk: this.isSelfTalk(),
- isUnpromoted: this.isUnpromoted(),
- isProtected: this.isProtected(),
- canSend: this.canSend,
- isDeviceTalk: this.isDeviceTalk(),
- isContactRequest: this.isContactRequest(),
- muted: this.isMuted(),
- }
- }
-}
diff --git a/node/lib/chatlist.ts b/node/lib/chatlist.ts
deleted file mode 100644
index 73233e0067..0000000000
--- a/node/lib/chatlist.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/* eslint-disable camelcase */
-
-import binding from './binding'
-import { Lot } from './lot'
-import { Chat } from './chat'
-const debug = require('debug')('deltachat:node:chatlist')
-
-interface NativeChatList {}
-/**
- * Wrapper around dc_chatlist_t*
- */
-export class ChatList {
- constructor(private dc_chatlist: NativeChatList) {
- debug('ChatList constructor')
- if (dc_chatlist === null) {
- throw new Error('native chat list can not be null')
- }
- }
-
- getChatId(index: number): number {
- debug(`getChatId ${index}`)
- return binding.dcn_chatlist_get_chat_id(this.dc_chatlist, index)
- }
-
- getCount(): number {
- debug('getCount')
- return binding.dcn_chatlist_get_cnt(this.dc_chatlist)
- }
-
- getMessageId(index: number): number {
- debug(`getMessageId ${index}`)
- return binding.dcn_chatlist_get_msg_id(this.dc_chatlist, index)
- }
-
- getSummary(index: number, chat?: Chat): Lot {
- debug(`getSummary ${index}`)
- const dc_chat = (chat && chat.dc_chat) || null
- return new Lot(
- binding.dcn_chatlist_get_summary(this.dc_chatlist, index, dc_chat)
- )
- }
-}
diff --git a/node/lib/constants.ts b/node/lib/constants.ts
deleted file mode 100644
index 6c58016163..0000000000
--- a/node/lib/constants.ts
+++ /dev/null
@@ -1,361 +0,0 @@
-// Generated!
-
-export enum C {
- DC_CERTCK_ACCEPT_INVALID = 2,
- DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
- DC_CERTCK_AUTO = 0,
- DC_CERTCK_STRICT = 1,
- DC_CHAT_ID_ALLDONE_HINT = 7,
- DC_CHAT_ID_ARCHIVED_LINK = 6,
- DC_CHAT_ID_LAST_SPECIAL = 9,
- DC_CHAT_ID_TRASH = 3,
- DC_CHAT_TYPE_BROADCAST = 160,
- DC_CHAT_TYPE_GROUP = 120,
- DC_CHAT_TYPE_MAILINGLIST = 140,
- DC_CHAT_TYPE_SINGLE = 100,
- DC_CHAT_TYPE_UNDEFINED = 0,
- DC_CHAT_VISIBILITY_ARCHIVED = 1,
- DC_CHAT_VISIBILITY_NORMAL = 0,
- DC_CHAT_VISIBILITY_PINNED = 2,
- DC_CONNECTIVITY_CONNECTED = 4000,
- DC_CONNECTIVITY_CONNECTING = 2000,
- DC_CONNECTIVITY_NOT_CONNECTED = 1000,
- DC_CONNECTIVITY_WORKING = 3000,
- DC_CONTACT_ID_DEVICE = 5,
- DC_CONTACT_ID_INFO = 2,
- DC_CONTACT_ID_LAST_SPECIAL = 9,
- DC_CONTACT_ID_SELF = 1,
- DC_DOWNLOAD_AVAILABLE = 10,
- DC_DOWNLOAD_DONE = 0,
- DC_DOWNLOAD_FAILURE = 20,
- DC_DOWNLOAD_IN_PROGRESS = 1000,
- DC_DOWNLOAD_UNDECIPHERABLE = 30,
- DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
- DC_EVENT_ACCOUNTS_CHANGED = 2302,
- DC_EVENT_ACCOUNTS_ITEM_CHANGED = 2303,
- DC_EVENT_CHANNEL_OVERFLOW = 2400,
- DC_EVENT_CHATLIST_CHANGED = 2300,
- DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
- DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
- DC_EVENT_CHAT_MODIFIED = 2020,
- DC_EVENT_CONFIGURE_PROGRESS = 2041,
- DC_EVENT_CONFIG_SYNCED = 2111,
- DC_EVENT_CONNECTIVITY_CHANGED = 2100,
- DC_EVENT_CONTACTS_CHANGED = 2030,
- DC_EVENT_DELETED_BLOB_FILE = 151,
- DC_EVENT_ERROR = 400,
- DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410,
- DC_EVENT_IMAP_CONNECTED = 102,
- DC_EVENT_IMAP_INBOX_IDLE = 106,
- DC_EVENT_IMAP_MESSAGE_DELETED = 104,
- DC_EVENT_IMAP_MESSAGE_MOVED = 105,
- DC_EVENT_IMEX_FILE_WRITTEN = 2052,
- DC_EVENT_IMEX_PROGRESS = 2051,
- DC_EVENT_INCOMING_MSG = 2005,
- DC_EVENT_INCOMING_MSG_BUNCH = 2006,
- DC_EVENT_INCOMING_REACTION = 2002,
- DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003,
- DC_EVENT_INFO = 100,
- DC_EVENT_LOCATION_CHANGED = 2035,
- DC_EVENT_MSGS_CHANGED = 2000,
- DC_EVENT_MSGS_NOTICED = 2008,
- DC_EVENT_MSG_DELETED = 2016,
- DC_EVENT_MSG_DELIVERED = 2010,
- DC_EVENT_MSG_FAILED = 2012,
- DC_EVENT_MSG_READ = 2015,
- DC_EVENT_NEW_BLOB_FILE = 150,
- DC_EVENT_REACTIONS_CHANGED = 2001,
- DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
- DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
- DC_EVENT_SELFAVATAR_CHANGED = 2110,
- DC_EVENT_SMTP_CONNECTED = 101,
- DC_EVENT_SMTP_MESSAGE_SENT = 103,
- DC_EVENT_WARNING = 300,
- DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
- DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT = 2151,
- DC_EVENT_WEBXDC_REALTIME_DATA = 2150,
- DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
- DC_GCL_ADD_ALLDONE_HINT = 4,
- DC_GCL_ADD_SELF = 2,
- DC_GCL_ARCHIVED_ONLY = 1,
- DC_GCL_FOR_FORWARDING = 8,
- DC_GCL_NO_SPECIALS = 2,
- DC_GCL_VERIFIED_ONLY = 1,
- DC_GCM_ADDDAYMARKER = 1,
- DC_GCM_INFO_ONLY = 2,
- DC_IMEX_EXPORT_BACKUP = 11,
- DC_IMEX_EXPORT_SELF_KEYS = 1,
- DC_IMEX_IMPORT_BACKUP = 12,
- DC_IMEX_IMPORT_SELF_KEYS = 2,
- DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6,
- DC_INFO_EPHEMERAL_TIMER_CHANGED = 10,
- DC_INFO_GROUP_IMAGE_CHANGED = 3,
- DC_INFO_GROUP_NAME_CHANGED = 2,
- DC_INFO_INVALID_UNENCRYPTED_MAIL = 13,
- DC_INFO_LOCATIONSTREAMING_ENABLED = 8,
- DC_INFO_LOCATION_ONLY = 9,
- DC_INFO_MEMBER_ADDED_TO_GROUP = 4,
- DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5,
- DC_INFO_PROTECTION_DISABLED = 12,
- DC_INFO_PROTECTION_ENABLED = 11,
- DC_INFO_SECURE_JOIN_MESSAGE = 7,
- DC_INFO_UNKNOWN = 0,
- DC_INFO_WEBXDC_INFO_MESSAGE = 32,
- DC_KEY_GEN_DEFAULT = 0,
- DC_KEY_GEN_ED25519 = 2,
- DC_KEY_GEN_RSA2048 = 1,
- DC_KEY_GEN_RSA4096 = 3,
- DC_LP_AUTH_NORMAL = 4,
- DC_LP_AUTH_OAUTH2 = 2,
- DC_MEDIA_QUALITY_BALANCED = 0,
- DC_MEDIA_QUALITY_WORSE = 1,
- DC_MSG_AUDIO = 40,
- DC_MSG_FILE = 60,
- DC_MSG_GIF = 21,
- DC_MSG_ID_DAYMARKER = 9,
- DC_MSG_ID_LAST_SPECIAL = 9,
- DC_MSG_ID_MARKER1 = 1,
- DC_MSG_IMAGE = 20,
- DC_MSG_STICKER = 23,
- DC_MSG_TEXT = 10,
- DC_MSG_VCARD = 90,
- DC_MSG_VIDEO = 50,
- DC_MSG_VIDEOCHAT_INVITATION = 70,
- DC_MSG_VOICE = 41,
- DC_MSG_WEBXDC = 80,
- DC_PROVIDER_STATUS_BROKEN = 3,
- DC_PROVIDER_STATUS_OK = 1,
- DC_PROVIDER_STATUS_PREPARATION = 2,
- DC_PUSH_CONNECTED = 2,
- DC_PUSH_HEARTBEAT = 1,
- DC_PUSH_NOT_CONNECTED = 0,
- DC_QR_ACCOUNT = 250,
- DC_QR_ADDR = 320,
- DC_QR_ASK_VERIFYCONTACT = 200,
- DC_QR_ASK_VERIFYGROUP = 202,
- DC_QR_BACKUP = 251,
- DC_QR_BACKUP2 = 252,
- DC_QR_ERROR = 400,
- DC_QR_FPR_MISMATCH = 220,
- DC_QR_FPR_OK = 210,
- DC_QR_FPR_WITHOUT_ADDR = 230,
- DC_QR_LOGIN = 520,
- DC_QR_PROXY = 271,
- DC_QR_REVIVE_VERIFYCONTACT = 510,
- DC_QR_REVIVE_VERIFYGROUP = 512,
- DC_QR_TEXT = 330,
- DC_QR_URL = 332,
- DC_QR_WEBRTC_INSTANCE = 260,
- DC_QR_WITHDRAW_VERIFYCONTACT = 500,
- DC_QR_WITHDRAW_VERIFYGROUP = 502,
- DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
- DC_SHOW_EMAILS_ALL = 2,
- DC_SHOW_EMAILS_OFF = 0,
- DC_SOCKET_AUTO = 0,
- DC_SOCKET_PLAIN = 3,
- DC_SOCKET_SSL = 1,
- DC_SOCKET_STARTTLS = 2,
- DC_STATE_IN_FRESH = 10,
- DC_STATE_IN_NOTICED = 13,
- DC_STATE_IN_SEEN = 16,
- DC_STATE_OUT_DELIVERED = 26,
- DC_STATE_OUT_DRAFT = 19,
- DC_STATE_OUT_FAILED = 24,
- DC_STATE_OUT_MDN_RCVD = 28,
- DC_STATE_OUT_PENDING = 20,
- DC_STATE_OUT_PREPARING = 18,
- DC_STATE_UNDEFINED = 0,
- DC_STR_AC_SETUP_MSG_BODY = 43,
- DC_STR_AC_SETUP_MSG_SUBJECT = 42,
- DC_STR_ADD_MEMBER_BY_OTHER = 129,
- DC_STR_ADD_MEMBER_BY_YOU = 128,
- DC_STR_AEAP_ADDR_CHANGED = 122,
- DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
- DC_STR_ARCHIVEDCHATS = 40,
- DC_STR_AUDIO = 11,
- DC_STR_BACKUP_TRANSFER_MSG_BODY = 163,
- DC_STR_BACKUP_TRANSFER_QR = 162,
- DC_STR_BAD_TIME_MSG_BODY = 85,
- DC_STR_BROADCAST_LIST = 115,
- DC_STR_CANNOT_LOGIN = 60,
- DC_STR_CANTDECRYPT_MSG_BODY = 29,
- DC_STR_CHAT_PROTECTION_DISABLED = 171,
- DC_STR_CHAT_PROTECTION_ENABLED = 170,
- DC_STR_CONFIGURATION_FAILED = 84,
- DC_STR_CONNECTED = 107,
- DC_STR_CONNTECTING = 108,
- DC_STR_CONTACT = 200,
- DC_STR_CONTACT_NOT_VERIFIED = 36,
- DC_STR_CONTACT_SETUP_CHANGED = 37,
- DC_STR_CONTACT_VERIFIED = 35,
- DC_STR_DEVICE_MESSAGES = 68,
- DC_STR_DEVICE_MESSAGES_HINT = 70,
- DC_STR_DOWNLOAD_AVAILABILITY = 100,
- DC_STR_DRAFT = 3,
- DC_STR_E2E_AVAILABLE = 25,
- DC_STR_E2E_PREFERRED = 34,
- DC_STR_ENCRYPTEDMSG = 24,
- DC_STR_ENCR_NONE = 28,
- DC_STR_ENCR_TRANSP = 27,
- DC_STR_EPHEMERAL_DAY = 79,
- DC_STR_EPHEMERAL_DAYS = 95,
- DC_STR_EPHEMERAL_DISABLED = 75,
- DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
- DC_STR_EPHEMERAL_HOUR = 78,
- DC_STR_EPHEMERAL_HOURS = 94,
- DC_STR_EPHEMERAL_MINUTE = 77,
- DC_STR_EPHEMERAL_MINUTES = 93,
- DC_STR_EPHEMERAL_SECONDS = 76,
- DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
- DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
- DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
- DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
- DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
- DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
- DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
- DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
- DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
- DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
- DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
- DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
- DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
- DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
- DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
- DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
- DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
- DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
- DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
- DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
- DC_STR_EPHEMERAL_WEEK = 80,
- DC_STR_EPHEMERAL_WEEKS = 96,
- DC_STR_ERROR = 112,
- DC_STR_ERROR_NO_NETWORK = 87,
- DC_STR_FAILED_SENDING_TO = 74,
- DC_STR_FILE = 12,
- DC_STR_FINGERPRINTS = 30,
- DC_STR_FORWARDED = 97,
- DC_STR_GIF = 23,
- DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
- DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
- DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
- DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
- DC_STR_GROUP_LEFT_BY_OTHER = 133,
- DC_STR_GROUP_LEFT_BY_YOU = 132,
- DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
- DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
- DC_STR_IMAGE = 9,
- DC_STR_INCOMING_MESSAGES = 103,
- DC_STR_INVALID_UNENCRYPTED_MAIL = 174,
- DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
- DC_STR_LOCATION = 66,
- DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
- DC_STR_LOCATION_ENABLED_BY_YOU = 136,
- DC_STR_MESSAGES = 114,
- DC_STR_MESSAGE_ADD_MEMBER = 173,
- DC_STR_MSGACTIONBYME = 63,
- DC_STR_MSGACTIONBYUSER = 62,
- DC_STR_MSGADDMEMBER = 17,
- DC_STR_MSGDELMEMBER = 18,
- DC_STR_MSGGROUPLEFT = 19,
- DC_STR_MSGGRPIMGCHANGED = 16,
- DC_STR_MSGGRPIMGDELETED = 33,
- DC_STR_MSGGRPNAME = 15,
- DC_STR_MSGLOCATIONDISABLED = 65,
- DC_STR_MSGLOCATIONENABLED = 64,
- DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE = 172,
- DC_STR_NOMESSAGES = 1,
- DC_STR_NOT_CONNECTED = 121,
- DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
- DC_STR_ONE_MOMENT = 106,
- DC_STR_OUTGOING_MESSAGES = 104,
- DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
- DC_STR_PART_OF_TOTAL_USED = 116,
- DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
- DC_STR_REACTED_BY = 177,
- DC_STR_READRCPT = 31,
- DC_STR_READRCPT_MAILBODY = 32,
- DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
- DC_STR_REMOVE_MEMBER_BY_YOU = 130,
- DC_STR_REPLY_NOUN = 90,
- DC_STR_SAVED_MESSAGES = 69,
- DC_STR_SECUREJOIN_WAIT = 190,
- DC_STR_SECUREJOIN_WAIT_TIMEOUT = 191,
- DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
- DC_STR_SECURE_JOIN_REPLIES = 118,
- DC_STR_SECURE_JOIN_STARTED = 117,
- DC_STR_SELF = 2,
- DC_STR_SELF_DELETED_MSG_BODY = 91,
- DC_STR_SENDING = 110,
- DC_STR_SERVER_TURNED_OFF = 92,
- DC_STR_SETUP_CONTACT_QR_DESC = 119,
- DC_STR_STICKER = 67,
- DC_STR_STORAGE_ON_DOMAIN = 105,
- DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
- DC_STR_SYNC_MSG_BODY = 102,
- DC_STR_SYNC_MSG_SUBJECT = 101,
- DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
- DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
- DC_STR_UPDATING = 109,
- DC_STR_VIDEO = 10,
- DC_STR_VIDEOCHAT_INVITATION = 82,
- DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
- DC_STR_VOICEMESSAGE = 7,
- DC_STR_WELCOME_MESSAGE = 71,
- DC_STR_YOU_REACTED = 176,
- DC_TEXT1_DRAFT = 1,
- DC_TEXT1_SELF = 3,
- DC_TEXT1_USERNAME = 2,
- DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
- DC_VIDEOCHATTYPE_JITSI = 2,
- DC_VIDEOCHATTYPE_UNKNOWN = 0,
-}
-
-// Generated!
-
-export const EventId2EventName: { [key: number]: string } = {
- 100: 'DC_EVENT_INFO',
- 101: 'DC_EVENT_SMTP_CONNECTED',
- 102: 'DC_EVENT_IMAP_CONNECTED',
- 103: 'DC_EVENT_SMTP_MESSAGE_SENT',
- 104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
- 105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
- 106: 'DC_EVENT_IMAP_INBOX_IDLE',
- 150: 'DC_EVENT_NEW_BLOB_FILE',
- 151: 'DC_EVENT_DELETED_BLOB_FILE',
- 300: 'DC_EVENT_WARNING',
- 400: 'DC_EVENT_ERROR',
- 410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
- 2000: 'DC_EVENT_MSGS_CHANGED',
- 2001: 'DC_EVENT_REACTIONS_CHANGED',
- 2002: 'DC_EVENT_INCOMING_REACTION',
- 2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY',
- 2005: 'DC_EVENT_INCOMING_MSG',
- 2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
- 2008: 'DC_EVENT_MSGS_NOTICED',
- 2010: 'DC_EVENT_MSG_DELIVERED',
- 2012: 'DC_EVENT_MSG_FAILED',
- 2015: 'DC_EVENT_MSG_READ',
- 2016: 'DC_EVENT_MSG_DELETED',
- 2020: 'DC_EVENT_CHAT_MODIFIED',
- 2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
- 2030: 'DC_EVENT_CONTACTS_CHANGED',
- 2035: 'DC_EVENT_LOCATION_CHANGED',
- 2041: 'DC_EVENT_CONFIGURE_PROGRESS',
- 2051: 'DC_EVENT_IMEX_PROGRESS',
- 2052: 'DC_EVENT_IMEX_FILE_WRITTEN',
- 2060: 'DC_EVENT_SECUREJOIN_INVITER_PROGRESS',
- 2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
- 2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
- 2110: 'DC_EVENT_SELFAVATAR_CHANGED',
- 2111: 'DC_EVENT_CONFIG_SYNCED',
- 2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
- 2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
- 2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
- 2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
- 2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
- 2300: 'DC_EVENT_CHATLIST_CHANGED',
- 2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
- 2302: 'DC_EVENT_ACCOUNTS_CHANGED',
- 2303: 'DC_EVENT_ACCOUNTS_ITEM_CHANGED',
- 2400: 'DC_EVENT_CHANNEL_OVERFLOW',
-}
diff --git a/node/lib/contact.ts b/node/lib/contact.ts
deleted file mode 100644
index 41e3e270c7..0000000000
--- a/node/lib/contact.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { integerToHexColor } from './util'
-
-/* eslint-disable camelcase */
-
-import binding from './binding'
-const debug = require('debug')('deltachat:node:contact')
-
-interface NativeContact {}
-/**
- * Wrapper around dc_contact_t*
- */
-export class Contact {
- constructor(public dc_contact: NativeContact) {
- debug('Contact constructor')
- if (dc_contact === null) {
- throw new Error('native contact can not be null')
- }
- }
-
- toJson() {
- debug('toJson')
- return {
- address: this.getAddress(),
- color: this.color,
- authName: this.authName,
- status: this.status,
- displayName: this.getDisplayName(),
- id: this.getId(),
- lastSeen: this.lastSeen,
- name: this.getName(),
- profileImage: this.getProfileImage(),
- nameAndAddr: this.getNameAndAddress(),
- isBlocked: this.isBlocked(),
- isVerified: this.isVerified(),
- }
- }
-
- getAddress(): string {
- return binding.dcn_contact_get_addr(this.dc_contact)
- }
-
- /** Get original contact name.
- * This is the name of the contact as defined by the contact themself.
- * If the contact themself does not define such a name,
- * an empty string is returned. */
- get authName(): string {
- return binding.dcn_contact_get_auth_name(this.dc_contact)
- }
-
- get color(): string {
- return integerToHexColor(binding.dcn_contact_get_color(this.dc_contact))
- }
-
- /**
- * contact's status
- *
- * Status is the last signature received in a message from this contact.
- */
- get status(): string {
- return binding.dcn_contact_get_status(this.dc_contact)
- }
-
- getDisplayName(): string {
- return binding.dcn_contact_get_display_name(this.dc_contact)
- }
-
- getId(): number {
- return binding.dcn_contact_get_id(this.dc_contact)
- }
-
- get lastSeen(): number {
- return binding.dcn_contact_get_last_seen(this.dc_contact)
- }
-
- wasSeenRecently() {
- return Boolean(binding.dcn_contact_was_seen_recently(this.dc_contact))
- }
-
- getName(): string {
- return binding.dcn_contact_get_name(this.dc_contact)
- }
-
- getNameAndAddress(): string {
- return binding.dcn_contact_get_name_n_addr(this.dc_contact)
- }
-
- getProfileImage(): string {
- return binding.dcn_contact_get_profile_image(this.dc_contact)
- }
-
- isBlocked() {
- return Boolean(binding.dcn_contact_is_blocked(this.dc_contact))
- }
-
- isVerified() {
- return Boolean(binding.dcn_contact_is_verified(this.dc_contact))
- }
-}
diff --git a/node/lib/context.ts b/node/lib/context.ts
deleted file mode 100644
index 67055d8e5a..0000000000
--- a/node/lib/context.ts
+++ /dev/null
@@ -1,897 +0,0 @@
-/* eslint-disable camelcase */
-
-import binding from './binding'
-import { C, EventId2EventName } from './constants'
-import { Chat } from './chat'
-import { ChatList } from './chatlist'
-import { Contact } from './contact'
-import { Message } from './message'
-import { Lot } from './lot'
-import { Locations } from './locations'
-import rawDebug from 'debug'
-import { AccountManager } from './deltachat'
-import { join } from 'path'
-import { EventEmitter } from 'stream'
-const debug = rawDebug('deltachat:node:index')
-
-const noop = function () {}
-interface NativeContext {}
-
-/**
- * Wrapper around dcn_context_t*
- *
- * only acts as event emitter when created in standalone mode (without account manager)
- * with `Context.open`
- */
-export class Context extends EventEmitter {
- constructor(
- readonly manager: AccountManager | null,
- private inner_dcn_context: NativeContext,
- readonly account_id: number | null
- ) {
- super()
- debug('DeltaChat constructor')
- if (inner_dcn_context === null) {
- throw new Error('inner_dcn_context can not be null')
- }
- }
-
- /** Opens a standalone context (without an account manager)
- * automatically starts the event handler */
- static open(cwd: string): Context {
- const dbFile = join(cwd, 'db.sqlite')
- const context = new Context(null, binding.dcn_context_new(dbFile), null)
- debug('Opened context')
- function handleCoreEvent(
- eventId: number,
- data1: number,
- data2: number | string
- ) {
- const eventString = EventId2EventName[eventId]
- debug(eventString, data1, data2)
- if (!context.emit) {
- console.log('Received an event but EventEmitter is already destroyed.')
- console.log(eventString, data1, data2)
- return
- }
- context.emit(eventString, data1, data2)
- context.emit('ALL', eventString, data1, data2)
- }
- binding.dcn_start_event_handler(
- context.dcn_context,
- handleCoreEvent.bind(this)
- )
- debug('Started event handler')
- return context
- }
-
- get dcn_context() {
- return this.inner_dcn_context
- }
-
- get is_open() {
- return Boolean(binding.dcn_context_is_open())
- }
-
- open(passphrase?: string) {
- return Boolean(
- binding.dcn_context_open(this.dcn_context, passphrase ? passphrase : '')
- )
- }
-
- unref() {
- binding.dcn_context_unref(this.dcn_context)
- ;(this.inner_dcn_context as any) = null
- }
-
- acceptChat(chatId: number) {
- binding.dcn_accept_chat(this.dcn_context, chatId)
- }
-
- blockChat(chatId: number) {
- binding.dcn_block_chat(this.dcn_context, chatId)
- }
-
- addAddressBook(addressBook: string) {
- debug(`addAddressBook ${addressBook}`)
- return binding.dcn_add_address_book(this.dcn_context, addressBook)
- }
-
- addContactToChat(chatId: number, contactId: number) {
- debug(`addContactToChat ${chatId} ${contactId}`)
- return Boolean(
- binding.dcn_add_contact_to_chat(
- this.dcn_context,
- Number(chatId),
- Number(contactId)
- )
- )
- }
-
- addDeviceMessage(label: string, msg: Message | string) {
- debug(`addDeviceMessage ${label} ${msg}`)
- if (!msg) {
- throw new Error('invalid msg parameter')
- }
- if (typeof label !== 'string') {
- throw new Error('invalid label parameter, must be a string')
- }
- if (typeof msg === 'string') {
- const msgObj = this.messageNew()
- msgObj.setText(msg)
- msg = msgObj
- }
- if (!msg.dc_msg) {
- throw new Error('invalid msg object')
- }
- return binding.dcn_add_device_msg(this.dcn_context, label, msg.dc_msg)
- }
-
- setChatVisibility(
- chatId: number,
- visibility:
- | C.DC_CHAT_VISIBILITY_NORMAL
- | C.DC_CHAT_VISIBILITY_ARCHIVED
- | C.DC_CHAT_VISIBILITY_PINNED
- ) {
- debug(`setChatVisibility ${chatId} ${visibility}`)
- binding.dcn_set_chat_visibility(
- this.dcn_context,
- Number(chatId),
- visibility
- )
- }
-
- blockContact(contactId: number, block: boolean) {
- debug(`blockContact ${contactId} ${block}`)
- binding.dcn_block_contact(
- this.dcn_context,
- Number(contactId),
- block ? 1 : 0
- )
- }
-
- checkQrCode(qrCode: string) {
- debug(`checkQrCode ${qrCode}`)
- const dc_lot = binding.dcn_check_qr(this.dcn_context, qrCode)
- let result = dc_lot ? new Lot(dc_lot) : null
- if (result) {
- return { id: result.getId(), ...result.toJson() }
- }
- return result
- }
-
- configure(opts: any): Promise {
- return new Promise((resolve, reject) => {
- debug('configure')
-
- const onSuccess = () => {
- removeListeners()
- resolve()
- }
- const onFail = (error: string) => {
- removeListeners()
- reject(new Error(error))
- }
-
- let onConfigure: (...args: any[]) => void
- if (this.account_id === null) {
- onConfigure = (data1: number, data2: string) => {
- if (data1 === 0) return onFail(data2)
- else if (data1 === 1000) return onSuccess()
- }
- } else {
- onConfigure = (accountId: number, data1: number, data2: string) => {
- if (this.account_id !== accountId) {
- return
- }
- if (data1 === 0) return onFail(data2)
- else if (data1 === 1000) return onSuccess()
- }
- }
-
- const removeListeners = () => {
- ;(this.manager || this).removeListener(
- 'DC_EVENT_CONFIGURE_PROGRESS',
- onConfigure
- )
- }
-
- const registerListeners = () => {
- ;(this.manager || this).on('DC_EVENT_CONFIGURE_PROGRESS', onConfigure)
- }
-
- registerListeners()
-
- if (!opts) opts = {}
- Object.keys(opts).forEach((key) => {
- const value = opts[key]
- this.setConfig(key, value)
- })
-
- binding.dcn_configure(this.dcn_context)
- })
- }
-
- continueKeyTransfer(messageId: number, setupCode: string) {
- debug(`continueKeyTransfer ${messageId}`)
- return new Promise((resolve, reject) => {
- binding.dcn_continue_key_transfer(
- this.dcn_context,
- Number(messageId),
- setupCode,
- (result: number) => resolve(result === 1)
- )
- })
- }
- /** @returns chatId */
- createBroadcastList(): number {
- debug(`createBroadcastList`)
- return binding.dcn_create_broadcast_list(this.dcn_context)
- }
-
- /** @returns chatId */
- createChatByContactId(contactId: number): number {
- debug(`createChatByContactId ${contactId}`)
- return binding.dcn_create_chat_by_contact_id(
- this.dcn_context,
- Number(contactId)
- )
- }
-
- /** @returns contactId */
- createContact(name: string, addr: string): number {
- debug(`createContact ${name} ${addr}`)
- return binding.dcn_create_contact(this.dcn_context, name, addr)
- }
-
- /**
- *
- * @param chatName The name of the chat that should be created
- * @param is_protected Whether the chat should be protected at creation time
- * @returns chatId
- */
- createGroupChat(chatName: string, is_protected: boolean = false): number {
- debug(`createGroupChat ${chatName} [protected:${is_protected}]`)
- return binding.dcn_create_group_chat(
- this.dcn_context,
- is_protected ? 1 : 0,
- chatName
- )
- }
-
- deleteChat(chatId: number) {
- debug(`deleteChat ${chatId}`)
- binding.dcn_delete_chat(this.dcn_context, Number(chatId))
- }
-
- deleteContact(contactId: number) {
- debug(`deleteContact ${contactId}`)
- return Boolean(
- binding.dcn_delete_contact(this.dcn_context, Number(contactId))
- )
- }
-
- deleteMessages(messageIds: number[]) {
- if (!Array.isArray(messageIds)) {
- messageIds = [messageIds]
- }
- messageIds = messageIds.map((id) => Number(id))
- debug('deleteMessages', messageIds)
- binding.dcn_delete_msgs(this.dcn_context, messageIds)
- }
-
- forwardMessages(messageIds: number[], chatId: number) {
- if (!Array.isArray(messageIds)) {
- messageIds = [messageIds]
- }
- messageIds = messageIds.map((id) => Number(id))
- debug('forwardMessages', messageIds)
- binding.dcn_forward_msgs(this.dcn_context, messageIds, chatId)
- }
-
- getBlobdir(): string {
- debug('getBlobdir')
- return binding.dcn_get_blobdir(this.dcn_context)
- }
-
- getBlockedCount(): number {
- debug('getBlockedCount')
- return binding.dcn_get_blocked_cnt(this.dcn_context)
- }
-
- getBlockedContacts(): number[] {
- debug('getBlockedContacts')
- return binding.dcn_get_blocked_contacts(this.dcn_context)
- }
-
- getChat(chatId: number) {
- debug(`getChat ${chatId}`)
- const dc_chat = binding.dcn_get_chat(this.dcn_context, Number(chatId))
- return dc_chat ? new Chat(dc_chat) : null
- }
-
- getChatContacts(chatId: number): number[] {
- debug(`getChatContacts ${chatId}`)
- return binding.dcn_get_chat_contacts(this.dcn_context, Number(chatId))
- }
-
- getChatIdByContactId(contactId: number): number {
- debug(`getChatIdByContactId ${contactId}`)
- return binding.dcn_get_chat_id_by_contact_id(
- this.dcn_context,
- Number(contactId)
- )
- }
-
- getChatMedia(
- chatId: number,
- msgType1: number,
- msgType2: number,
- msgType3: number
- ): number[] {
- debug(`getChatMedia ${chatId}`)
- return binding.dcn_get_chat_media(
- this.dcn_context,
- Number(chatId),
- msgType1,
- msgType2 || 0,
- msgType3 || 0
- )
- }
-
- getMimeHeaders(messageId: number): string {
- debug(`getMimeHeaders ${messageId}`)
- return binding.dcn_get_mime_headers(this.dcn_context, Number(messageId))
- }
-
- getChatlistItemSummary(chatId: number, messageId: number) {
- debug(`getChatlistItemSummary ${chatId} ${messageId}`)
- return new Lot(
- binding.dcn_chatlist_get_summary2(this.dcn_context, chatId, messageId)
- )
- }
-
- getChatMessages(chatId: number, flags: number, marker1before: number) {
- debug(`getChatMessages ${chatId} ${flags} ${marker1before}`)
- return binding.dcn_get_chat_msgs(
- this.dcn_context,
- Number(chatId),
- flags,
- marker1before
- )
- }
-
- /**
- * Get encryption info for a chat.
- * Get a multi-line encryption info, containing encryption preferences of all members.
- * Can be used to find out why messages sent to group are not encrypted.
- *
- * @param chatId ID of the chat to get the encryption info for.
- * @return Multi-line text, must be released using dc_str_unref() after usage.
- */
- getChatEncrytionInfo(chatId: number): string {
- return binding.dcn_get_chat_encrinfo(this.dcn_context, chatId)
- }
-
- getChats(listFlags: number, queryStr: string, queryContactId: number) {
- debug('getChats')
- const result = []
- const list = this.getChatList(listFlags, queryStr, queryContactId)
- const count = list.getCount()
- for (let i = 0; i < count; i++) {
- result.push(list.getChatId(i))
- }
- return result
- }
-
- getChatList(listFlags: number, queryStr: string, queryContactId: number) {
- listFlags = listFlags || 0
- queryStr = queryStr || ''
- queryContactId = queryContactId || 0
- debug(`getChatList ${listFlags} ${queryStr} ${queryContactId}`)
- return new ChatList(
- binding.dcn_get_chatlist(
- this.dcn_context,
- listFlags,
- queryStr,
- Number(queryContactId)
- )
- )
- }
-
- getConfig(key: string): string {
- debug(`getConfig ${key}`)
- return binding.dcn_get_config(this.dcn_context, key)
- }
-
- getContact(contactId: number) {
- debug(`getContact ${contactId}`)
- const dc_contact = binding.dcn_get_contact(
- this.dcn_context,
- Number(contactId)
- )
- return dc_contact ? new Contact(dc_contact) : null
- }
-
- getContactEncryptionInfo(contactId: number) {
- debug(`getContactEncryptionInfo ${contactId}`)
- return binding.dcn_get_contact_encrinfo(this.dcn_context, Number(contactId))
- }
-
- getContacts(listFlags: number, query: string) {
- listFlags = listFlags || 0
- query = query || ''
- debug(`getContacts ${listFlags} ${query}`)
- return binding.dcn_get_contacts(this.dcn_context, listFlags, query)
- }
-
- wasDeviceMessageEverAdded(label: string) {
- debug(`wasDeviceMessageEverAdded ${label}`)
- const added = binding.dcn_was_device_msg_ever_added(this.dcn_context, label)
- return added === 1
- }
-
- getDraft(chatId: number) {
- debug(`getDraft ${chatId}`)
- const dc_msg = binding.dcn_get_draft(this.dcn_context, Number(chatId))
- return dc_msg ? new Message(dc_msg) : null
- }
-
- getFreshMessageCount(chatId: number): number {
- debug(`getFreshMessageCount ${chatId}`)
- return binding.dcn_get_fresh_msg_cnt(this.dcn_context, Number(chatId))
- }
-
- getFreshMessages() {
- debug('getFreshMessages')
- return binding.dcn_get_fresh_msgs(this.dcn_context)
- }
-
- getInfo() {
- debug('getInfo')
- const info = binding.dcn_get_info(this.dcn_context)
- return AccountManager.parseGetInfo(info)
- }
-
- getMessage(messageId: number) {
- debug(`getMessage ${messageId}`)
- const dc_msg = binding.dcn_get_msg(this.dcn_context, Number(messageId))
- return dc_msg ? new Message(dc_msg) : null
- }
-
- getMessageCount(chatId: number): number {
- debug(`getMessageCount ${chatId}`)
- return binding.dcn_get_msg_cnt(this.dcn_context, Number(chatId))
- }
-
- getMessageInfo(messageId: number): string {
- debug(`getMessageInfo ${messageId}`)
- return binding.dcn_get_msg_info(this.dcn_context, Number(messageId))
- }
-
- getMessageHTML(messageId: number): string {
- debug(`getMessageHTML ${messageId}`)
- return binding.dcn_get_msg_html(this.dcn_context, Number(messageId))
- }
-
- getSecurejoinQrCode(chatId: number): string {
- debug(`getSecurejoinQrCode ${chatId}`)
- return binding.dcn_get_securejoin_qr(this.dcn_context, Number(chatId))
- }
-
- getSecurejoinQrCodeSVG(chatId: number): string {
- debug(`getSecurejoinQrCodeSVG ${chatId}`)
- return binding.dcn_get_securejoin_qr_svg(this.dcn_context, chatId)
- }
-
- startIO(): void {
- debug(`startIO`)
- binding.dcn_start_io(this.dcn_context)
- }
-
- stopIO(): void {
- debug(`stopIO`)
- binding.dcn_stop_io(this.dcn_context)
- }
-
- stopOngoingProcess(): void {
- debug(`stopOngoingProcess`)
- binding.dcn_stop_ongoing_process(this.dcn_context)
- }
-
- /**
- *
- * @deprecated please use `AccountManager.getSystemInfo()` instead
- */
- static getSystemInfo() {
- return AccountManager.getSystemInfo()
- }
-
- getConnectivity(): number {
- return binding.dcn_get_connectivity(this.dcn_context)
- }
-
- getConnectivityHTML(): String {
- return binding.dcn_get_connectivity_html(this.dcn_context)
- }
-
- importExport(what: number, param1: string, param2 = '') {
- debug(`importExport ${what} ${param1} ${param2}`)
- binding.dcn_imex(this.dcn_context, what, param1, param2)
- }
-
- importExportHasBackup(dir: string) {
- debug(`importExportHasBackup ${dir}`)
- return binding.dcn_imex_has_backup(this.dcn_context, dir)
- }
-
- initiateKeyTransfer(): Promise {
- return new Promise((resolve, reject) => {
- debug('initiateKeyTransfer2')
- binding.dcn_initiate_key_transfer(this.dcn_context, resolve)
- })
- }
-
- isConfigured() {
- debug('isConfigured')
- return Boolean(binding.dcn_is_configured(this.dcn_context))
- }
-
- isContactInChat(chatId: number, contactId: number) {
- debug(`isContactInChat ${chatId} ${contactId}`)
- return Boolean(
- binding.dcn_is_contact_in_chat(
- this.dcn_context,
- Number(chatId),
- Number(contactId)
- )
- )
- }
-
- /**
- *
- * @returns resulting chat id or 0 on error
- */
- joinSecurejoin(qrCode: string): number {
- debug(`joinSecurejoin ${qrCode}`)
- return binding.dcn_join_securejoin(this.dcn_context, qrCode)
- }
-
- lookupContactIdByAddr(addr: string): number {
- debug(`lookupContactIdByAddr ${addr}`)
- return binding.dcn_lookup_contact_id_by_addr(this.dcn_context, addr)
- }
-
- markNoticedChat(chatId: number) {
- debug(`markNoticedChat ${chatId}`)
- binding.dcn_marknoticed_chat(this.dcn_context, Number(chatId))
- }
-
- markSeenMessages(messageIds: number[]) {
- if (!Array.isArray(messageIds)) {
- messageIds = [messageIds]
- }
- messageIds = messageIds.map((id) => Number(id))
- debug('markSeenMessages', messageIds)
- binding.dcn_markseen_msgs(this.dcn_context, messageIds)
- }
-
- maybeNetwork() {
- debug('maybeNetwork')
- binding.dcn_maybe_network(this.dcn_context)
- }
-
- messageNew(viewType = C.DC_MSG_TEXT) {
- debug(`messageNew ${viewType}`)
- return new Message(binding.dcn_msg_new(this.dcn_context, viewType))
- }
-
- removeContactFromChat(chatId: number, contactId: number) {
- debug(`removeContactFromChat ${chatId} ${contactId}`)
- return Boolean(
- binding.dcn_remove_contact_from_chat(
- this.dcn_context,
- Number(chatId),
- Number(contactId)
- )
- )
- }
-
- /**
- *
- * @param chatId ID of the chat to search messages in. Set this to 0 for a global search.
- * @param query The query to search for.
- */
- searchMessages(chatId: number, query: string): number[] {
- debug(`searchMessages ${chatId} ${query}`)
- return binding.dcn_search_msgs(this.dcn_context, Number(chatId), query)
- }
-
- sendMessage(chatId: number, msg: string | Message) {
- debug(`sendMessage ${chatId}`)
- if (!msg) {
- throw new Error('invalid msg parameter')
- }
- if (typeof msg === 'string') {
- const msgObj = this.messageNew()
- msgObj.setText(msg)
- msg = msgObj
- }
- if (!msg.dc_msg) {
- throw new Error('invalid msg object')
- }
- return binding.dcn_send_msg(this.dcn_context, Number(chatId), msg.dc_msg)
- }
-
- downloadFullMessage(messageId: number) {
- binding.dcn_download_full_msg(this.dcn_context, messageId)
- }
-
- /**
- *
- * @returns {Promise} Promise that resolves into the resulting message id
- */
- sendVideochatInvitation(chatId: number): Promise {
- debug(`sendVideochatInvitation ${chatId}`)
- return new Promise((resolve, reject) => {
- binding.dcn_send_videochat_invitation(
- this.dcn_context,
- chatId,
- (result: number) => {
- if (result !== 0) {
- resolve(result)
- } else {
- reject(
- 'Videochatinvitation failed to send, see error events for detailed info'
- )
- }
- }
- )
- })
- }
-
- setChatName(chatId: number, name: string) {
- debug(`setChatName ${chatId} ${name}`)
- return Boolean(
- binding.dcn_set_chat_name(this.dcn_context, Number(chatId), name)
- )
- }
-
- getChatEphemeralTimer(chatId: number): number {
- debug(`getChatEphemeralTimer ${chatId}`)
- return binding.dcn_get_chat_ephemeral_timer(
- this.dcn_context,
- Number(chatId)
- )
- }
-
- setChatEphemeralTimer(chatId: number, timer: number) {
- debug(`setChatEphemeralTimer ${chatId} ${timer}`)
- return Boolean(
- binding.dcn_set_chat_ephemeral_timer(
- this.dcn_context,
- Number(chatId),
- Number(timer)
- )
- )
- }
-
- setChatProfileImage(chatId: number, image: string) {
- debug(`setChatProfileImage ${chatId} ${image}`)
- return Boolean(
- binding.dcn_set_chat_profile_image(
- this.dcn_context,
- Number(chatId),
- image || ''
- )
- )
- }
-
- setConfig(key: string, value: string | boolean | number): number {
- debug(`setConfig (string) ${key} ${value}`)
- if (value === null) {
- return binding.dcn_set_config_null(this.dcn_context, key)
- } else {
- if (typeof value === 'boolean') {
- value = value === true ? '1' : '0'
- } else if (typeof value === 'number') {
- value = String(value)
- }
- return binding.dcn_set_config(this.dcn_context, key, value)
- }
- }
-
- setConfigFromQr(qrcodeContent: string): boolean {
- return Boolean(
- binding.dcn_set_config_from_qr(this.dcn_context, qrcodeContent)
- )
- }
-
- estimateDeletionCount(fromServer: boolean, seconds: number): number {
- debug(`estimateDeletionCount fromServer: ${fromServer} seconds: ${seconds}`)
- return binding.dcn_estimate_deletion_cnt(
- this.dcn_context,
- fromServer === true ? 1 : 0,
- seconds
- )
- }
-
- setStockTranslation(stockId: number, stockMsg: string) {
- debug(`setStockTranslation ${stockId} ${stockMsg}`)
- return Boolean(
- binding.dcn_set_stock_translation(
- this.dcn_context,
- Number(stockId),
- stockMsg
- )
- )
- }
-
- setDraft(chatId: number, msg: Message | null) {
- debug(`setDraft ${chatId}`)
- binding.dcn_set_draft(
- this.dcn_context,
- Number(chatId),
- msg ? msg.dc_msg : null
- )
- }
-
- setLocation(latitude: number, longitude: number, accuracy: number) {
- debug(`setLocation ${latitude}`)
- binding.dcn_set_location(
- this.dcn_context,
- Number(latitude),
- Number(longitude),
- Number(accuracy)
- )
- }
-
- /*
- * @param chatId Chat-id to get location information for.
- * 0 to get locations independently of the chat.
- * @param contactId Contact id to get location information for.
- * If also a chat-id is given, this should be a member of the given chat.
- * 0 to get locations independently of the contact.
- * @param timestampFrom Start of timespan to return.
- * Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
- * 0 for "start from the beginning".
- * @param timestampTo End of timespan to return.
- * Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
- * 0 for "all up to now".
- * @return Array of locations, NULL is never returned.
- * The array is sorted descending;
- * the first entry in the array is the location with the newest timestamp.
- *
- * Examples:
- * // get locations from the last hour for a global map
- * getLocations(0, 0, time(NULL)-60*60, 0);
- *
- * // get locations from a contact for a global map
- * getLocations(0, contact_id, 0, 0);
- *
- * // get all locations known for a given chat
- * getLocations(chat_id, 0, 0, 0);
- *
- * // get locations from a single contact for a given chat
- * getLocations(chat_id, contact_id, 0, 0);
- */
-
- getLocations(
- chatId: number,
- contactId: number,
- timestampFrom = 0,
- timestampTo = 0
- ) {
- const locations = new Locations(
- binding.dcn_get_locations(
- this.dcn_context,
- Number(chatId),
- Number(contactId),
- timestampFrom,
- timestampTo
- )
- )
- return locations.toJson()
- }
-
- /**
- *
- * @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
- */
- setChatMuteDuration(chatId: number, duration: number) {
- return Boolean(
- binding.dcn_set_chat_mute_duration(this.dcn_context, chatId, duration)
- )
- }
-
- /** get information about the provider */
- getProviderFromEmail(email: string) {
- debug('DeltaChat.getProviderFromEmail')
- const provider = binding.dcn_provider_new_from_email(
- this.dcn_context,
- email
- )
- if (!provider) {
- return undefined
- }
- return {
- before_login_hint: binding.dcn_provider_get_before_login_hint(provider),
- overview_page: binding.dcn_provider_get_overview_page(provider),
- status: binding.dcn_provider_get_status(provider),
- }
- }
-
- sendWebxdcStatusUpdate(
- msgId: number,
- json: WebxdcSendingStatusUpdate,
- descr: string
- ) {
- return Boolean(
- binding.dcn_send_webxdc_status_update(
- this.dcn_context,
- msgId,
- JSON.stringify(json),
- descr
- )
- )
- }
-
- getWebxdcStatusUpdates(
- msgId: number,
- serial = 0
- ): WebxdcReceivedStatusUpdate[] {
- return JSON.parse(
- binding.dcn_get_webxdc_status_updates(this.dcn_context, msgId, serial)
- )
- }
-
- /** the string contains the binary data, it is an "u8 string", maybe we will use a more efficient type in the future. */
- getWebxdcBlob(message: Message, filename: string): Buffer | null {
- return binding.dcn_msg_get_webxdc_blob(message.dc_msg, filename)
- }
-}
-
-export type WebxdcInfo = {
- name: string
- icon: string
- summary: string
- /**
- * if set by the webxdc, name of the document in edit
- */
- document?: string
-}
-
-type WebxdcSendingStatusUpdate = {
- /** the payload, deserialized json:
- * any javascript primitive, array or object. */
- payload: T
- /** optional, short, informational message that will be added to the chat,
- * eg. "Alice voted" or "Bob scored 123 in MyGame";
- * usually only one line of text is shown,
- * use this option sparingly to not spam the chat. */
- info?: string
- /** optional, short text, shown beside app icon;
- * it is recommended to use some aggregated value,
- * eg. "8 votes", "Highscore: 123" */
- summary?: string
- /**
- * optional, name of the document in edit,
- * must not be used eg. in games where the Webxdc does not create documents
- */
- document?: string
-}
-
-type WebxdcReceivedStatusUpdate = {
- /** the payload, deserialized json */
- payload: T
- /** the serial number of this update. Serials are larger `0` and newer serials have higher numbers. */
- serial: number
- /** the maximum serial currently known.
- * If `max_serial` equals `serial` this update is the last update (until new network messages arrive). */
- max_serial: number
- /** optional, short, informational message. */
- info?: string
- /** optional, short text, shown beside app icon. If there are no updates, an empty JSON-array is returned. */
- summary?: string
-}
diff --git a/node/lib/deltachat.ts b/node/lib/deltachat.ts
deleted file mode 100644
index 6080aacda7..0000000000
--- a/node/lib/deltachat.ts
+++ /dev/null
@@ -1,234 +0,0 @@
-/* eslint-disable camelcase */
-
-import binding from './binding'
-import { EventId2EventName } from './constants'
-import { EventEmitter } from 'events'
-import { existsSync } from 'fs'
-import rawDebug from 'debug'
-import { tmpdir } from 'os'
-import { join } from 'path'
-import { Context } from './context'
-const debug = rawDebug('deltachat:node:index')
-
-const noop = function () {}
-interface NativeAccount {}
-
-/**
- * Wrapper around dcn_account_t*
- */
-export class AccountManager extends EventEmitter {
- dcn_accounts: NativeAccount
- accountDir: string
- jsonRpcStarted = false
-
- constructor(cwd: string, writable = true) {
- super()
- debug('DeltaChat constructor')
-
- this.accountDir = cwd
- this.dcn_accounts = binding.dcn_accounts_new(
- this.accountDir,
- writable ? 1 : 0
- )
- }
-
- getAllAccountIds() {
- return binding.dcn_accounts_get_all(this.dcn_accounts)
- }
-
- selectAccount(account_id: number) {
- return binding.dcn_accounts_select_account(this.dcn_accounts, account_id)
- }
-
- selectedAccount(): number {
- return binding.dcn_accounts_get_selected_account(this.dcn_accounts)
- }
-
- addAccount(): number {
- return binding.dcn_accounts_add_account(this.dcn_accounts)
- }
-
- addClosedAccount(): number {
- return binding.dcn_accounts_add_closed_account(this.dcn_accounts)
- }
-
- removeAccount(account_id: number) {
- return binding.dcn_accounts_remove_account(this.dcn_accounts, account_id)
- }
-
- accountContext(account_id: number) {
- const native_context = binding.dcn_accounts_get_account(
- this.dcn_accounts,
- account_id
- )
- if (native_context === null) {
- throw new Error(
- `could not get context with id ${account_id}, does it even exist? please check your ids`
- )
- }
- return new Context(this, native_context, account_id)
- }
-
- migrateAccount(dbfile: string): number {
- return binding.dcn_accounts_migrate_account(this.dcn_accounts, dbfile)
- }
-
- close() {
- this.stopIO()
- debug('unrefing context')
- binding.dcn_accounts_unref(this.dcn_accounts)
- debug('Unref end')
- }
-
- emit(
- event: string | symbol,
- account_id: number,
- data1: any,
- data2: any
- ): boolean {
- super.emit('ALL', event, account_id, data1, data2)
- return super.emit(event, account_id, data1, data2)
- }
-
- handleCoreEvent(
- eventId: number,
- accountId: number,
- data1: number,
- data2: number | string
- ) {
- const eventString = EventId2EventName[eventId]
- debug('event', eventString, accountId, data1, data2)
- debug(eventString, data1, data2)
- if (!this.emit) {
- console.log('Received an event but EventEmitter is already destroyed.')
- console.log(eventString, data1, data2)
- return
- }
- this.emit(eventString, accountId, data1, data2)
- }
-
- startEvents() {
- if (this.dcn_accounts === null) {
- throw new Error('dcn_account is null')
- }
- binding.dcn_accounts_start_event_handler(
- this.dcn_accounts,
- this.handleCoreEvent.bind(this)
- )
- debug('Started event handler')
- }
-
- startJsonRpcHandler(callback: ((response: string) => void) | null) {
- if (this.dcn_accounts === null) {
- throw new Error('dcn_account is null')
- }
- if (!callback) {
- throw new Error('no callback set')
- }
- if (this.jsonRpcStarted) {
- throw new Error('jsonrpc was started already')
- }
-
- binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, callback.bind(this))
- debug('Started JSON-RPC handler')
- this.jsonRpcStarted = true
- }
-
- jsonRpcRequest(message: string) {
- if (!this.jsonRpcStarted) {
- throw new Error(
- 'jsonrpc is not active, start it with startJsonRpcHandler first'
- )
- }
- binding.dcn_json_rpc_request(this.dcn_accounts, message)
- }
-
- startIO() {
- binding.dcn_accounts_start_io(this.dcn_accounts)
- }
-
- stopIO() {
- binding.dcn_accounts_stop_io(this.dcn_accounts)
- }
-
- static maybeValidAddr(addr: string) {
- debug('DeltaChat.maybeValidAddr')
- if (addr === null) return false
- return Boolean(binding.dcn_maybe_valid_addr(addr))
- }
-
- static parseGetInfo(info: string) {
- debug('static _getInfo')
- const result: { [key: string]: string } = {}
-
- const regex = /^(\w+)=(.*)$/i
- info
- .split('\n')
- .filter(Boolean)
- .forEach((line) => {
- const match = regex.exec(line)
- if (match) {
- result[match[1]] = match[2]
- }
- })
-
- return result
- }
-
- static newTemporary() {
- let directory = null
- while (true) {
- const randomString = Math.random().toString(36).substring(2, 5)
- directory = join(tmpdir(), 'deltachat-' + randomString)
- if (!existsSync(directory)) break
- }
- const dc = new AccountManager(directory)
- const accountId = dc.addAccount()
- const context = dc.accountContext(accountId)
- return { dc, context, accountId, directory }
- }
-
- static getSystemInfo() {
- debug('DeltaChat.getSystemInfo')
- const { dc, context } = AccountManager.newTemporary()
- const info = AccountManager.parseGetInfo(
- binding.dcn_get_info(context.dcn_context)
- )
- const {
- deltachat_core_version,
- sqlite_version,
- sqlite_thread_safe,
- libetpan_version,
- openssl_version,
- compile_date,
- arch,
- } = info
- const result = {
- deltachat_core_version,
- sqlite_version,
- sqlite_thread_safe,
- libetpan_version,
- openssl_version,
- compile_date,
- arch,
- }
- context.unref()
- dc.close()
- return result
- }
-
- /** get information about the provider
- *
- * This function creates a temporary context to be standalone,
- * if possible use `Context.getProviderFromEmail` instead. (otherwise potential proxy settings are not used)
- * @deprecated
- */
- static getProviderFromEmail(email: string) {
- debug('DeltaChat.getProviderFromEmail')
- const { dc, context } = AccountManager.newTemporary()
- const provider = context.getProviderFromEmail(email)
- context.unref()
- dc.close()
- return provider
- }
-}
diff --git a/node/lib/index.ts b/node/lib/index.ts
deleted file mode 100644
index 6b179dbeb8..0000000000
--- a/node/lib/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { AccountManager } from './deltachat'
-
-export default AccountManager
-
-export { Context } from './context'
-export { Chat } from './chat'
-export { ChatList } from './chatlist'
-export { C } from './constants'
-export { Contact } from './contact'
-export { AccountManager as DeltaChat }
-export { Locations } from './locations'
-export { Lot } from './lot'
-export {
- Message,
- MessageState,
- MessageViewType,
- MessageDownloadState,
-} from './message'
-
-export * from './types'
diff --git a/node/lib/locations.ts b/node/lib/locations.ts
deleted file mode 100644
index 6e197a1338..0000000000
--- a/node/lib/locations.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/* eslint-disable camelcase */
-
-const binding = require('../binding')
-const debug = require('debug')('deltachat:node:locations')
-
-interface NativeLocations {}
-/**
- * Wrapper around dc_location_t*
- */
-export class Locations {
- constructor(public dc_locations: NativeLocations) {
- debug('Locations constructor')
- if (dc_locations === null) {
- throw new Error('dc_locations can not be null')
- }
- }
-
- locationToJson(index: number) {
- debug('locationToJson')
- return {
- accuracy: this.getAccuracy(index),
- latitude: this.getLatitude(index),
- longitude: this.getLongitude(index),
- timestamp: this.getTimestamp(index),
- contactId: this.getContactId(index),
- msgId: this.getMsgId(index),
- chatId: this.getChatId(index),
- isIndependent: this.isIndependent(index),
- marker: this.getMarker(index),
- }
- }
-
- toJson(): ReturnType[] {
- debug('toJson')
- const locations = []
- const count = this.getCount()
- for (let index = 0; index < count; index++) {
- locations.push(this.locationToJson(index))
- }
- return locations
- }
-
- getCount(): number {
- return binding.dcn_array_get_cnt(this.dc_locations)
- }
-
- getAccuracy(index: number): number {
- return binding.dcn_array_get_accuracy(this.dc_locations, index)
- }
-
- getLatitude(index: number): number {
- return binding.dcn_array_get_latitude(this.dc_locations, index)
- }
-
- getLongitude(index: number): number {
- return binding.dcn_array_get_longitude(this.dc_locations, index)
- }
-
- getTimestamp(index: number): number {
- return binding.dcn_array_get_timestamp(this.dc_locations, index)
- }
-
- getMsgId(index: number): number {
- return binding.dcn_array_get_msg_id(this.dc_locations, index)
- }
-
- getContactId(index: number): number {
- return binding.dcn_array_get_contact_id(this.dc_locations, index)
- }
-
- getChatId(index: number): number {
- return binding.dcn_array_get_chat_id(this.dc_locations, index)
- }
-
- isIndependent(index: number): boolean {
- return binding.dcn_array_is_independent(this.dc_locations, index)
- }
-
- getMarker(index: number): string {
- return binding.dcn_array_get_marker(this.dc_locations, index)
- }
-}
diff --git a/node/lib/lot.ts b/node/lib/lot.ts
deleted file mode 100644
index 4da0bccab2..0000000000
--- a/node/lib/lot.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/* eslint-disable camelcase */
-
-const binding = require('../binding')
-const debug = require('debug')('deltachat:node:lot')
-
-interface NativeLot {}
-/**
- * Wrapper around dc_lot_t*
- */
-export class Lot {
- constructor(public dc_lot: NativeLot) {
- debug('Lot constructor')
- if (dc_lot === null) {
- throw new Error('dc_lot can not be null')
- }
- }
-
- toJson() {
- debug('toJson')
- return {
- state: this.getState(),
- text1: this.getText1(),
- text1Meaning: this.getText1Meaning(),
- text2: this.getText2(),
- timestamp: this.getTimestamp(),
- }
- }
-
- getId(): number {
- return binding.dcn_lot_get_id(this.dc_lot)
- }
-
- getState(): number {
- return binding.dcn_lot_get_state(this.dc_lot)
- }
-
- getText1(): string {
- return binding.dcn_lot_get_text1(this.dc_lot)
- }
-
- getText1Meaning(): string {
- return binding.dcn_lot_get_text1_meaning(this.dc_lot)
- }
-
- getText2(): string {
- return binding.dcn_lot_get_text2(this.dc_lot)
- }
-
- getTimestamp(): number {
- return binding.dcn_lot_get_timestamp(this.dc_lot)
- }
-}
diff --git a/node/lib/message.ts b/node/lib/message.ts
deleted file mode 100644
index c4036ad6b4..0000000000
--- a/node/lib/message.ts
+++ /dev/null
@@ -1,366 +0,0 @@
-/* eslint-disable camelcase */
-
-import binding from './binding'
-import { C } from './constants'
-import { Lot } from './lot'
-import { Chat } from './chat'
-import { WebxdcInfo } from './context'
-const debug = require('debug')('deltachat:node:message')
-
-export enum MessageDownloadState {
- Available = C.DC_DOWNLOAD_AVAILABLE,
- Done = C.DC_DOWNLOAD_DONE,
- Failure = C.DC_DOWNLOAD_FAILURE,
- InProgress = C.DC_DOWNLOAD_IN_PROGRESS,
-}
-
-/**
- * Helper class for message states so you can do e.g.
- *
- * if (msg.getState().isPending()) { .. }
- *
- */
-export class MessageState {
- constructor(public state: number) {
- debug(`MessageState constructor ${state}`)
- }
-
- isUndefined() {
- return this.state === C.DC_STATE_UNDEFINED
- }
-
- isFresh() {
- return this.state === C.DC_STATE_IN_FRESH
- }
-
- isNoticed() {
- return this.state === C.DC_STATE_IN_NOTICED
- }
-
- isSeen() {
- return this.state === C.DC_STATE_IN_SEEN
- }
-
- isPending() {
- return this.state === C.DC_STATE_OUT_PENDING
- }
-
- isFailed() {
- return this.state === C.DC_STATE_OUT_FAILED
- }
-
- isDelivered() {
- return this.state === C.DC_STATE_OUT_DELIVERED
- }
-
- isReceived() {
- return this.state === C.DC_STATE_OUT_MDN_RCVD
- }
-}
-
-/**
- * Helper class for message types so you can do e.g.
- *
- * if (msg.getViewType().isVideo()) { .. }
- *
- */
-export class MessageViewType {
- constructor(public viewType: number) {
- debug(`MessageViewType constructor ${viewType}`)
- }
-
- isText() {
- return this.viewType === C.DC_MSG_TEXT
- }
-
- isImage() {
- return this.viewType === C.DC_MSG_IMAGE || this.viewType === C.DC_MSG_GIF
- }
-
- isGif() {
- return this.viewType === C.DC_MSG_GIF
- }
-
- isAudio() {
- return this.viewType === C.DC_MSG_AUDIO || this.viewType === C.DC_MSG_VOICE
- }
-
- isVoice() {
- return this.viewType === C.DC_MSG_VOICE
- }
-
- isVideo() {
- return this.viewType === C.DC_MSG_VIDEO
- }
-
- isFile() {
- return this.viewType === C.DC_MSG_FILE
- }
-
- isVideochatInvitation() {
- return this.viewType === C.DC_MSG_VIDEOCHAT_INVITATION
- }
-}
-
-interface NativeMessage {}
-/**
- * Wrapper around dc_msg_t*
- */
-export class Message {
- constructor(public dc_msg: NativeMessage) {
- debug('Message constructor')
- if (dc_msg === null) {
- throw new Error('dc_msg can not be null')
- }
- }
-
- toJson() {
- debug('toJson')
- const quotedMessage = this.getQuotedMessage()
- const viewType = binding.dcn_msg_get_viewtype(this.dc_msg)
- return {
- chatId: this.getChatId(),
- webxdcInfo: viewType == C.DC_MSG_WEBXDC ? this.webxdcInfo : null,
- downloadState: this.downloadState,
- duration: this.getDuration(),
- file: this.getFile(),
- fromId: this.getFromId(),
- id: this.getId(),
- quotedText: this.getQuotedText(),
- quotedMessageId: quotedMessage ? quotedMessage.getId() : null,
- receivedTimestamp: this.getReceivedTimestamp(),
- sortTimestamp: this.getSortTimestamp(),
- text: this.getText(),
- timestamp: this.getTimestamp(),
- hasLocation: this.hasLocation(),
- hasHTML: this.hasHTML,
- viewType,
- state: binding.dcn_msg_get_state(this.dc_msg),
- hasDeviatingTimestamp: this.hasDeviatingTimestamp(),
- showPadlock: this.getShowpadlock(),
- summary: this.getSummary().toJson(),
- subject: this.subject,
- isSetupmessage: this.isSetupmessage(),
- isInfo: this.isInfo(),
- isForwarded: this.isForwarded(),
- dimensions: {
- height: this.getHeight(),
- width: this.getWidth(),
- },
- videochatType: this.getVideochatType(),
- videochatUrl: this.getVideochatUrl(),
- overrideSenderName: this.overrideSenderName,
- parentId: this.parent?.getId(),
- }
- }
-
- getChatId(): number {
- return binding.dcn_msg_get_chat_id(this.dc_msg)
- }
-
- get webxdcInfo(): WebxdcInfo | null {
- let info = binding.dcn_msg_get_webxdc_info(this.dc_msg)
- return info
- ? JSON.parse(binding.dcn_msg_get_webxdc_info(this.dc_msg))
- : null
- }
-
- get downloadState(): MessageDownloadState {
- return binding.dcn_msg_get_download_state(this.dc_msg)
- }
-
- get parent(): Message | null {
- let msg = binding.dcn_msg_get_parent(this.dc_msg)
- return msg ? new Message(msg) : null
- }
-
- getDuration(): number {
- return binding.dcn_msg_get_duration(this.dc_msg)
- }
-
- getFile(): string {
- return binding.dcn_msg_get_file(this.dc_msg)
- }
-
- getFilebytes(): number {
- return binding.dcn_msg_get_filebytes(this.dc_msg)
- }
-
- getFilemime(): string {
- return binding.dcn_msg_get_filemime(this.dc_msg)
- }
-
- getFilename(): string {
- return binding.dcn_msg_get_filename(this.dc_msg)
- }
-
- getFromId(): number {
- return binding.dcn_msg_get_from_id(this.dc_msg)
- }
-
- getHeight(): number {
- return binding.dcn_msg_get_height(this.dc_msg)
- }
-
- getId(): number {
- return binding.dcn_msg_get_id(this.dc_msg)
- }
-
- getQuotedText(): string {
- return binding.dcn_msg_get_quoted_text(this.dc_msg)
- }
-
- getQuotedMessage(): Message | null {
- const dc_msg = binding.dcn_msg_get_quoted_msg(this.dc_msg)
- return dc_msg ? new Message(dc_msg) : null
- }
-
- getReceivedTimestamp(): number {
- return binding.dcn_msg_get_received_timestamp(this.dc_msg)
- }
-
- getSetupcodebegin() {
- return binding.dcn_msg_get_setupcodebegin(this.dc_msg)
- }
-
- getShowpadlock() {
- return Boolean(binding.dcn_msg_get_showpadlock(this.dc_msg))
- }
-
- getSortTimestamp(): number {
- return binding.dcn_msg_get_sort_timestamp(this.dc_msg)
- }
-
- getState() {
- return new MessageState(binding.dcn_msg_get_state(this.dc_msg))
- }
-
- getSummary(chat?: Chat) {
- const dc_chat = (chat && chat.dc_chat) || null
- return new Lot(binding.dcn_msg_get_summary(this.dc_msg, dc_chat))
- }
-
- get subject(): string {
- return binding.dcn_msg_get_subject(this.dc_msg)
- }
-
- getSummarytext(approxCharacters: number): string {
- approxCharacters = approxCharacters || 0
- return binding.dcn_msg_get_summarytext(this.dc_msg, approxCharacters)
- }
-
- getText(): string {
- return binding.dcn_msg_get_text(this.dc_msg)
- }
-
- getTimestamp(): number {
- return binding.dcn_msg_get_timestamp(this.dc_msg)
- }
-
- getViewType() {
- return new MessageViewType(binding.dcn_msg_get_viewtype(this.dc_msg))
- }
-
- getVideochatType(): number {
- return binding.dcn_msg_get_videochat_type(this.dc_msg)
- }
-
- getVideochatUrl(): string {
- return binding.dcn_msg_get_videochat_url(this.dc_msg)
- }
-
- getWidth(): number {
- return binding.dcn_msg_get_width(this.dc_msg)
- }
-
- get overrideSenderName(): string {
- return binding.dcn_msg_get_override_sender_name(this.dc_msg)
- }
-
- hasDeviatingTimestamp() {
- return binding.dcn_msg_has_deviating_timestamp(this.dc_msg)
- }
-
- hasLocation() {
- return Boolean(binding.dcn_msg_has_location(this.dc_msg))
- }
-
- get hasHTML() {
- return Boolean(binding.dcn_msg_has_html(this.dc_msg))
- }
-
- isDeadDrop() {
- // TODO: Fix
- //return this.getChatId() === C.DC_CHAT_ID_DEADDROP
- return false
- }
-
- isForwarded() {
- return Boolean(binding.dcn_msg_is_forwarded(this.dc_msg))
- }
-
- isInfo() {
- return Boolean(binding.dcn_msg_is_info(this.dc_msg))
- }
-
- isSent() {
- return Boolean(binding.dcn_msg_is_sent(this.dc_msg))
- }
-
- isSetupmessage() {
- return Boolean(binding.dcn_msg_is_setupmessage(this.dc_msg))
- }
-
- latefilingMediasize(width: number, height: number, duration: number) {
- binding.dcn_msg_latefiling_mediasize(this.dc_msg, width, height, duration)
- }
-
- setDimension(width: number, height: number) {
- binding.dcn_msg_set_dimension(this.dc_msg, width, height)
- return this
- }
-
- setDuration(duration: number) {
- binding.dcn_msg_set_duration(this.dc_msg, duration)
- return this
- }
-
- setFile(file: string, mime?: string) {
- if (typeof file !== 'string') throw new Error('Missing filename')
- binding.dcn_msg_set_file(this.dc_msg, file, mime || '')
- return this
- }
-
- setLocation(longitude: number, latitude: number) {
- binding.dcn_msg_set_location(this.dc_msg, longitude, latitude)
- return this
- }
-
- setQuote(quotedMessage: Message | null) {
- binding.dcn_msg_set_quote(this.dc_msg, quotedMessage?.dc_msg)
- return this
- }
-
- setText(text: string) {
- binding.dcn_msg_set_text(this.dc_msg, text)
- return this
- }
-
- setHTML(html: string) {
- binding.dcn_msg_set_html(this.dc_msg, html)
- return this
- }
-
- setOverrideSenderName(senderName: string) {
- binding.dcn_msg_set_override_sender_name(this.dc_msg, senderName)
- return this
- }
-
- /** Force the message to be sent in plain text.
- *
- * This API is for bots, there is no need to expose it in the UI.
- */
- forcePlaintext() {
- binding.dcn_msg_force_plaintext(this.dc_msg)
- }
-}
diff --git a/node/lib/types.ts b/node/lib/types.ts
deleted file mode 100644
index 6bb54d9f86..0000000000
--- a/node/lib/types.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { C } from './constants'
-
-export type ChatTypes =
- | C.DC_CHAT_TYPE_GROUP
- | C.DC_CHAT_TYPE_MAILINGLIST
- | C.DC_CHAT_TYPE_SINGLE
- | C.DC_CHAT_TYPE_UNDEFINED
-
-export interface ChatJSON {
- archived: boolean
- pinned: boolean
- color: string
- id: number
- name: string
- mailinglistAddr: string
- profileImage: string
- type: number
- isSelfTalk: boolean
- isUnpromoted: boolean
- isProtected: boolean
- canSend: boolean
- isDeviceTalk: boolean
- isContactRequest: boolean
- muted: boolean
-}
diff --git a/node/lib/util.ts b/node/lib/util.ts
deleted file mode 100644
index 7a8c4ba6b0..0000000000
--- a/node/lib/util.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * @param integerColor expects a 24bit rgb integer (left to right: 8bits red, 8bits green, 8bits blue)
- */
-export function integerToHexColor(integerColor: number) {
- return '#' + (integerColor + 16777216).toString(16).substring(1)
-}
diff --git a/node/patches/m1_build_use_x86_64.patch b/node/patches/m1_build_use_x86_64.patch
deleted file mode 100644
index cf52ea2968..0000000000
--- a/node/patches/m1_build_use_x86_64.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git i/node/binding.gyp w/node/binding.gyp
-index b0d92eae..c5e504fa 100644
---- i/node/binding.gyp
-+++ w/node/binding.gyp
-@@ -43,7 +43,7 @@
- "include_dirs": ["../deltachat-ffi"],
- "ldflags": ["-Wl,-Bsymbolic"], # Prevent sqlite3 from electron from overriding sqlcipher
- "libraries": [
-- "../../target/release/libdeltachat.a",
-+ "../../target/x86_64-apple-darwin/release/libdeltachat.a",
- "-ldl",
- ],
- "conditions": [],
diff --git a/node/scripts/common.js b/node/scripts/common.js
deleted file mode 100644
index af085cb591..0000000000
--- a/node/scripts/common.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const spawnSync = require('child_process').spawnSync
-
-const verbose = isVerbose()
-
-function spawn (cmd, args, opts) {
- log(`>> spawn: ${cmd} ${args.join(' ')}`)
- const result = spawnSync(cmd, args, opts)
- if (result.status === null) {
- console.error(`Could not find ${cmd}`)
- process.exit(1)
- } else if (result.status !== 0) {
- console.error(`${cmd} failed with code ${result.status}`)
- process.exit(1)
- }
-}
-
-function log (...args) {
- if (verbose) console.log(...args)
-}
-
-function isVerbose () {
- const loglevel = process.env.npm_config_loglevel
- return loglevel === 'verbose' || process.env.CI === 'true'
-}
-
-module.exports = { spawn, log, isVerbose, verbose }
diff --git a/node/scripts/generate-constants.js b/node/scripts/generate-constants.js
deleted file mode 100755
index 7e821dc0ad..0000000000
--- a/node/scripts/generate-constants.js
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/env node
-const fs = require('fs')
-const path = require('path')
-
-const data = []
-const header = path.resolve(__dirname, '../../deltachat-ffi/deltachat.h')
-
-console.log('Generating constants...')
-
-const header_data = fs.readFileSync(header, 'UTF-8')
-const regex = /^#define\s+(\w+)\s+(\w+)/gm
-while (null != (match = regex.exec(header_data))) {
- const key = match[1]
- const value = parseInt(match[2])
- if (!isNaN(value)) {
- data.push({ key, value })
- }
-}
-
-delete header_data
-
-const constants = data
- .filter(
- ({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
- )
- .sort((lhs, rhs) => {
- if (lhs.key < rhs.key) return -1
- else if (lhs.key > rhs.key) return 1
- return 0
- })
- .map((row) => {
- return ` ${row.key}: ${row.value}`
- })
- .join(',\n')
-
-const events = data
- .sort((lhs, rhs) => {
- if (lhs.value < rhs.value) return -1
- else if (lhs.value > rhs.value) return 1
- return 0
- })
- .filter((i) => {
- return i.key.startsWith('DC_EVENT_')
- })
- .map((i) => {
- return ` ${i.value}: '${i.key}'`
- })
- .join(',\n')
-
-// backwards compat
-fs.writeFileSync(
- path.resolve(__dirname, '../constants.js'),
- `// Generated!\n\nmodule.exports = {\n${constants}\n}\n`
-)
-// backwards compat
-fs.writeFileSync(
- path.resolve(__dirname, '../events.js'),
- `/* eslint-disable quotes */\n// Generated!\n\nmodule.exports = {\n${events}\n}\n`
-)
-
-fs.writeFileSync(
- path.resolve(__dirname, '../lib/constants.ts'),
- `// Generated!\n\nexport enum C {\n${constants.replace(/:/g, ' =')},\n}\n
-// Generated!\n\nexport const EventId2EventName: { [key: number]: string } = {\n${events},\n}\n`
-)
diff --git a/node/scripts/install.js b/node/scripts/install.js
deleted file mode 100644
index 3f320be4a3..0000000000
--- a/node/scripts/install.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const {execSync} = require('child_process')
-const {existsSync} = require('fs')
-const {join} = require('path')
-
-const run = (cmd) => {
- console.log('[i] running `' + cmd + '`')
- execSync(cmd, {stdio: 'inherit'})
-}
-
-// Build bindings
-if (process.env.USE_SYSTEM_LIBDELTACHAT === 'true') {
- console.log('[i] USE_SYSTEM_LIBDELTACHAT is true, rebuilding c bindings and using pkg-config to retrieve lib paths and cflags of libdeltachat')
- run('npm run build:bindings:c:c')
-} else {
- console.log('[i] Building rust core & c bindings, if possible use prebuilds')
- run('npm run install:prebuilds')
-}
-
-if (!existsSync(join(__dirname, '..', 'dist'))) {
- console.log('[i] Didn\'t find already built typescript bindings. Trying to transpile them. If this fail, make sure typescript is installed ;)')
- run('npm run build:bindings:ts')
-}
diff --git a/node/scripts/postLinksToDetails.js b/node/scripts/postLinksToDetails.js
deleted file mode 100644
index 83b70037ed..0000000000
--- a/node/scripts/postLinksToDetails.js
+++ /dev/null
@@ -1,47 +0,0 @@
-const { readFileSync } = require('fs')
-
-const sha = JSON.parse(
- readFileSync(process.env['GITHUB_EVENT_PATH'], 'utf8')
-).pull_request.head.sha
-
-const base_url =
- 'https://download.delta.chat/node/'
-
-const GITHUB_API_URL =
- 'https://api.github.com/repos/deltachat/deltachat-core-rust/statuses/' + sha
-
-const file_url = process.env['URL']
-const GITHUB_TOKEN = process.env['GITHUB_TOKEN']
-const context = process.env['MSG_CONTEXT']
-
-const STATUS_DATA = {
- state: 'success',
- description: '⏩ Click on "Details" to download →',
- context: context || 'Download the node-bindings.tar.gz',
- target_url: base_url + file_url,
-}
-
-const http = require('https')
-
-const options = {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'User-Agent': 'github-action ci for deltachat deskop',
- authorization: 'Bearer ' + GITHUB_TOKEN,
- },
-}
-
-const req = http.request(GITHUB_API_URL, options, function(res) {
- var chunks = []
- res.on('data', function(chunk) {
- chunks.push(chunk)
- })
- res.on('end', function() {
- var body = Buffer.concat(chunks)
- console.log(body.toString())
- })
-})
-
-req.write(JSON.stringify(STATUS_DATA))
-req.end()
diff --git a/node/scripts/postinstall.js b/node/scripts/postinstall.js
deleted file mode 100644
index 15eb241ad5..0000000000
--- a/node/scripts/postinstall.js
+++ /dev/null
@@ -1,57 +0,0 @@
-const fs = require('fs')
-const path = require('path')
-
-if (process.platform !== 'win32') {
- console.log('postinstall: not windows, so skipping!')
- process.exit(0)
-}
-
-const from = path.resolve(
- __dirname,
- '..',
- '..',
- 'target',
- 'release',
- 'deltachat.dll'
-)
-
-const getDestination = () => {
- const argv = process.argv
- if (argv.length === 3 && argv[2] === '--prebuild') {
- return path.resolve(
- __dirname,
- '..',
- 'prebuilds',
- 'win32-x64',
- 'deltachat.dll'
- )
- } else {
- return path.resolve(
- __dirname,
- '..',
- 'build',
- 'Release',
- 'deltachat.dll'
- )
- }
-}
-
-const dest = getDestination()
-
-copy(from, dest, (err) => {
- if (err) throw err
- console.log(`postinstall: copied ${from} to ${dest}`)
-})
-
-function copy (from, to, cb) {
- fs.stat(from, (err, st) => {
- if (err) return cb(err)
- fs.readFile(from, (err, buf) => {
- if (err) return cb(err)
- fs.writeFile(to, buf, (err) => {
- if (err) return cb(err)
- fs.chmod(to, st.mode, cb)
- })
- })
- })
-}
diff --git a/node/scripts/rebuild-core.js b/node/scripts/rebuild-core.js
deleted file mode 100644
index 80dedcce5a..0000000000
--- a/node/scripts/rebuild-core.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const path = require('path')
-const { spawn } = require('./common')
-const opts = {
- cwd: path.resolve(__dirname, '../..'),
- stdio: 'inherit'
-}
-
-const buildArgs = [
- 'build',
- '--release',
- '--features',
- 'vendored',
- '-p',
- 'deltachat_ffi'
-]
-
-spawn('cargo', buildArgs, opts)
diff --git a/node/src/module.c b/node/src/module.c
deleted file mode 100644
index 02dda50c1b..0000000000
--- a/node/src/module.c
+++ /dev/null
@@ -1,3600 +0,0 @@
-#define NAPI_VERSION 4
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include "napi-macros-extensions.h"
-
-//#define DEBUG
-
-#ifdef DEBUG
-#define TRACE(fmt, ...) fprintf(stderr, "> module.c:%d %s() " fmt "\n", __LINE__, __func__, ##__VA_ARGS__)
-#else
-#define TRACE(fmt, ...)
-#endif
-
-/**
- * Custom context
- */
-typedef struct dcn_context_t {
- dc_context_t* dc_context;
- napi_threadsafe_function threadsafe_event_handler;
- uv_thread_t event_handler_thread;
- int gc;
-} dcn_context_t;
-
-/**
- * Custom accounts
- */
-typedef struct dcn_accounts_t {
- dc_accounts_t* dc_accounts;
- napi_threadsafe_function threadsafe_event_handler;
- uv_thread_t event_handler_thread;
- napi_threadsafe_function threadsafe_jsonrpc_handler;
- uv_thread_t jsonrpc_thread;
- dc_jsonrpc_instance_t* jsonrpc_instance;
- int gc;
-} dcn_accounts_t;
-
-
-
-
-/**
- * Finalize functions. These are called once the corresponding
- * external is garbage collected on the JavaScript side.
- */
-
-static void finalize_chat(napi_env env, void* data, void* hint) {
- if (data) {
- dc_chat_t* chat = (dc_chat_t*)data;
- //TRACE("cleaning up chat %d", dc_chat_get_id(chat));
- dc_chat_unref(chat);
- }
-}
-
-static void finalize_chatlist(napi_env env, void* data, void* hint) {
- if (data) {
- //TRACE("cleaning up chatlist object");
- dc_chatlist_unref((dc_chatlist_t*)data);
- }
-}
-
-static void finalize_contact(napi_env env, void* data, void* hint) {
- if (data) {
- dc_contact_t* contact = (dc_contact_t*)data;
- //TRACE("cleaning up contact %d", dc_contact_get_id(contact));
- dc_contact_unref(contact);
- }
-}
-
-static void finalize_lot(napi_env env, void* data, void* hint) {
- if (data) {
- //TRACE("cleaning up lot");
- dc_lot_unref((dc_lot_t*)data);
- }
-}
-
-static void finalize_array(napi_env env, void* data, void* hint) {
- if (data) {
- //TRACE("cleaning up array");
- dc_array_unref((dc_array_t*)data);
- }
-}
-
-static void finalize_msg(napi_env env, void* data, void* hint) {
- if (data) {
- dc_msg_t* msg = (dc_msg_t*)data;
- //TRACE("cleaning up message %d", dc_msg_get_id(msg));
- dc_msg_unref(msg);
- }
-}
-
-static void finalize_provider(napi_env env, void* data, void* hint) {
- if (data) {
- dc_provider_t* provider = (dc_provider_t*)data;
- //TRACE("cleaning up provider");
- dc_provider_unref(provider);
- }
-}
-
-/**
- * Helpers.
- */
-
-static uint32_t* js_array_to_uint32(napi_env env, napi_value js_array, uint32_t* length) {
- *length = 0;
- NAPI_STATUS_THROWS(napi_get_array_length(env, js_array, length));
-
- uint32_t* array = calloc(*length, sizeof(uint32_t));
-
- for (uint32_t i = 0; i < *length; i++) {
- napi_value napi_element;
- NAPI_STATUS_THROWS(napi_get_element(env, js_array, i, &napi_element));
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, napi_element, &array[i]));
- }
-
- return array;
-}
-
-static napi_value dc_array_to_js_array(napi_env env, dc_array_t* array) {
- napi_value js_array;
-
- const int length = dc_array_get_cnt(array);
- NAPI_STATUS_THROWS(napi_create_array_with_length(env, length, &js_array));
-
- if (length > 0) {
- for (int i = 0; i < length; i++) {
- const uint32_t id = dc_array_get_id(array, i);
- napi_value napi_id;
- NAPI_STATUS_THROWS(napi_create_uint32(env, id, &napi_id));
- NAPI_STATUS_THROWS(napi_set_element(env, js_array, i, napi_id));
- }
- }
-
- return js_array;
-}
-
-/**
- * Main context.
- */
-
-NAPI_METHOD(dcn_context_new) {
- NAPI_ARGV(1);
-
- NAPI_ARGV_UTF8_MALLOC(db_file, 0);
-
- TRACE("creating new dc_context");
-
- dcn_context_t* dcn_context = calloc(1, sizeof(dcn_context_t));
- dcn_context->dc_context = dc_context_new(NULL, db_file, NULL);
-
-
- napi_value result;
- NAPI_STATUS_THROWS(napi_create_external(env, dcn_context,
- NULL, NULL, &result));
- return result;
-}
-
-NAPI_METHOD(dcn_context_new_closed) {
- NAPI_ARGV(1);
-
- NAPI_ARGV_UTF8_MALLOC(db_file, 0);
-
- TRACE("creating new closed dc_context");
-
- dcn_context_t* dcn_context = calloc(1, sizeof(dcn_context_t));
- dcn_context->dc_context = dc_context_new_closed(db_file);
-
-
- napi_value result;
- NAPI_STATUS_THROWS(napi_create_external(env, dcn_context,
- NULL, NULL, &result));
- return result;
-}
-
-NAPI_METHOD(dcn_context_open) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(passphrase, 1);
-
- int result = dc_context_open(dcn_context->dc_context, passphrase);
- free(passphrase);
-
- NAPI_RETURN_UINT32(result);
-}
-
-NAPI_METHOD(dcn_context_is_open) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- int result = dc_context_is_open(dcn_context->dc_context);
-
- NAPI_RETURN_UINT32(result);
-}
-
-/**
- * Event struct for calling back to JavaScript
- */
-typedef struct dcn_event_t {
- int event;
- uintptr_t data1_int;
- uintptr_t data2_int;
- char* data1_str;
- char* data2_str;
-} dcn_event_t;
-
-
-static void event_handler_thread_func(void* arg)
-{
- dcn_context_t* dcn_context = (dcn_context_t*)arg;
- dc_context_t* dc_context = dcn_context->dc_context;
-
-
- TRACE("event_handler_thread_func starting");
-
-
- dc_event_emitter_t* emitter = dc_get_event_emitter(dc_context);
- dc_event_t* event;
- while (true) {
- if (emitter == NULL) {
- TRACE("event emitter is null, bailing");
- break;
- }
-
- event = dc_get_next_event(emitter);
- if (event == NULL) {
- TRACE("event is null, bailing");
- break;
- }
-
- if (!dcn_context->threadsafe_event_handler) {
- TRACE("threadsafe_event_handler not set, bailing");
- break;
- }
-
- // Don't process events if we're being garbage collected!
- if (dcn_context->gc == 1) {
- TRACE("dc_context has been destroyed, bailing");
- break;
- }
-
-
- napi_status status = napi_call_threadsafe_function(dcn_context->threadsafe_event_handler, event, napi_tsfn_blocking);
-
- if (status == napi_closing) {
- TRACE("JS function got released, bailing");
- break;
- }
- }
-
- dc_event_emitter_unref(emitter);
-
- TRACE("event_handler_thread_func ended");
-
- napi_release_threadsafe_function(dcn_context->threadsafe_event_handler, napi_tsfn_release);
-}
-
-static void call_js_event_handler(napi_env env, napi_value js_callback, void* _context, void* data)
-{
- dc_event_t* dc_event = (dc_event_t*)data;
-
- napi_value global;
- napi_status status = napi_get_global(env, &global);
-
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to get global");
- }
-
-#define CALL_JS_CALLBACK_ARGC 3
-
- const int argc = CALL_JS_CALLBACK_ARGC;
- napi_value argv[CALL_JS_CALLBACK_ARGC];
-
- const int event_id = dc_event_get_id(dc_event);
-
- status = napi_create_int32(env, event_id, &argv[0]);
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to create argv[0] for event_handler arguments");
- }
-
- status = napi_create_int32(env, dc_event_get_data1_int(dc_event), &argv[1]);
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to create argv[1] for event_handler arguments");
- }
-
- if DC_EVENT_DATA2_IS_STRING(event_id) {
- char* data2_string = dc_event_get_data2_str(dc_event);
- // Quick fix for https://github.com/deltachat/deltachat-core-rust/issues/1949
- if (data2_string != 0) {
- status = napi_create_string_utf8(env, data2_string, NAPI_AUTO_LENGTH, &argv[2]);
- } else {
- status = napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &argv[2]);
- }
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to create argv[2] for event_handler arguments");
- }
- free(data2_string);
- } else {
- status = napi_create_int32(env, dc_event_get_data2_int(dc_event), &argv[2]);
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to create argv[2] for event_handler arguments");
- }
- }
-
- dc_event_unref(dc_event);
- dc_event = NULL;
-
- TRACE("calling back into js");
-
- napi_value result;
- status = napi_call_function(
- env,
- global,
- js_callback,
- argc,
- argv,
- &result);
-
- if (status != napi_ok) {
- TRACE("Unable to call event_handler callback2");
- const napi_extended_error_info* error_result;
- NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result));
- }
-}
-
-
-NAPI_METHOD(dcn_start_event_handler) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- napi_value callback = argv[1];
-
- TRACE("calling..");
- napi_value async_resource_name;
- NAPI_STATUS_THROWS(napi_create_string_utf8(env, "dc_event_callback", NAPI_AUTO_LENGTH, &async_resource_name));
-
- TRACE("creating threadsafe function..");
-
- NAPI_STATUS_THROWS(napi_create_threadsafe_function(
- env,
- callback,
- 0,
- async_resource_name,
- 1000, // max_queue_size
- 1,
- NULL,
- NULL,
- dcn_context,
- call_js_event_handler,
- &dcn_context->threadsafe_event_handler));
- TRACE("done");
-
- dcn_context->gc = 0;
- TRACE("creating uv thread..");
- uv_thread_create(&dcn_context->event_handler_thread, event_handler_thread_func, dcn_context);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-
-NAPI_METHOD(dcn_context_unref) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- TRACE("Unrefing dc_context");
- dcn_context->gc = 1;
- if (dcn_context->event_handler_thread != 0) {
- dc_stop_io(dcn_context->dc_context);
- uv_thread_join(&dcn_context->event_handler_thread);
- dcn_context->event_handler_thread = 0;
- }
- dc_context_unref(dcn_context->dc_context);
- dcn_context->dc_context = NULL;
-
- NAPI_RETURN_UNDEFINED();
-
-}
-
-/**
- * Static functions
- */
-
-NAPI_METHOD(dcn_maybe_valid_addr) {
- NAPI_ARGV(1);
- NAPI_ARGV_UTF8_MALLOC(addr, 0);
-
- //TRACE("calling..");
- int result = dc_may_be_valid_addr(addr);
- //TRACE("result %d", result);
-
- free(addr);
-
- NAPI_RETURN_INT32(result);
-}
-
-/**
- * dcn_context_t
- */
-
-NAPI_METHOD(dcn_add_address_book) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(address_book, 1);
-
- //TRACE("calling..");
- int result = dc_add_address_book(dcn_context->dc_context, address_book);
- //TRACE("result %d", result);
-
- free(address_book);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_add_contact_to_chat) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_UINT32(contact_id, 2);
-
- //TRACE("calling..");
- int result = dc_add_contact_to_chat(dcn_context->dc_context,
- chat_id, contact_id);
- //TRACE("result %d", result);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_add_device_msg) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
-
- NAPI_ARGV_UTF8_MALLOC(label, 1);
-
- //TRACE("calling..");
- dc_msg_t* dc_msg = NULL;
- napi_get_value_external(env, argv[2], (void**)&dc_msg);
-
- uint32_t msg_id = dc_add_device_msg(dcn_context->dc_context, label, dc_msg);
-
- free(label);
- //TRACE("done");
-
- NAPI_RETURN_UINT32(msg_id);
-}
-
-NAPI_METHOD(dcn_block_contact) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(contact_id, 1);
- NAPI_ARGV_INT32(new_blocking, 2);
-
- //TRACE("calling..");
- dc_block_contact(dcn_context->dc_context, contact_id, new_blocking);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_check_qr) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(qr, 1);
-
- //TRACE("calling..");
- dc_lot_t* lot = dc_check_qr(dcn_context->dc_context, qr);
-
- free(qr);
-
- napi_value result;
- if (lot == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- NAPI_STATUS_THROWS(napi_create_external(env, lot,
- finalize_lot,
- NULL, &result));
- }
- //TRACE("done");
-
- return result;
-}
-
-
-NAPI_METHOD(dcn_configure) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- TRACE("calling..");
- dc_configure(dcn_context->dc_context);
- TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_accept_chat) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- dc_accept_chat(dcn_context->dc_context, chat_id);
-
- NAPI_RETURN_UNDEFINED()
-}
-
-NAPI_METHOD(dcn_block_chat) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- dc_block_chat(dcn_context->dc_context, chat_id);
-
- NAPI_RETURN_UNDEFINED()
-}
-
-NAPI_ASYNC_CARRIER_BEGIN(dcn_continue_key_transfer)
- int msg_id;
- char* setup_code;
- int result;
-NAPI_ASYNC_CARRIER_END(dcn_continue_key_transfer)
-
-
-NAPI_ASYNC_EXECUTE(dcn_continue_key_transfer) {
- NAPI_ASYNC_GET_CARRIER(dcn_continue_key_transfer)
- carrier->result = dc_continue_key_transfer(carrier->dcn_context->dc_context,
- carrier->msg_id, carrier->setup_code);
-}
-
-NAPI_ASYNC_COMPLETE(dcn_continue_key_transfer) {
- NAPI_ASYNC_GET_CARRIER(dcn_continue_key_transfer)
- if (status != napi_ok) {
- napi_throw_type_error(env, NULL, "Execute callback failed.");
- return;
- }
-
-#define DCN_CONTINUE_KEY_TRANSFER_CALLBACK_ARGC 1
-
- const int argc = DCN_CONTINUE_KEY_TRANSFER_CALLBACK_ARGC;
- napi_value argv[DCN_CONTINUE_KEY_TRANSFER_CALLBACK_ARGC];
- NAPI_STATUS_THROWS(napi_create_int32(env, carrier->result, &argv[0]));
-
- NAPI_ASYNC_CALL_AND_DELETE_CB()
- dc_str_unref(carrier->setup_code);
- free(carrier);
-}
-
-NAPI_METHOD(dcn_continue_key_transfer) {
- NAPI_ARGV(4);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(msg_id, 1);
- NAPI_ARGV_UTF8_MALLOC(setup_code, 2);
- NAPI_ASYNC_NEW_CARRIER(dcn_continue_key_transfer);
- carrier->msg_id = msg_id;
- carrier->setup_code = setup_code;
-
- NAPI_ASYNC_QUEUE_WORK(dcn_continue_key_transfer, argv[3]);
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_join_securejoin) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(qr_code, 1);
-
- uint32_t chat_id = dc_join_securejoin(dcn_context->dc_context, qr_code);
-
- NAPI_RETURN_UINT32(chat_id);
-}
-
-NAPI_METHOD(dcn_create_chat_by_contact_id) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_INT32(contact_id, 1);
-
- //TRACE("calling..");
- uint32_t chat_id = dc_create_chat_by_contact_id(dcn_context->dc_context, contact_id);
- //TRACE("result %d", chat_id);
-
- NAPI_RETURN_UINT32(chat_id);
-}
-
-NAPI_METHOD(dcn_create_broadcast_list) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- //TRACE("calling..");
- uint32_t chat_id = dc_create_broadcast_list(dcn_context->dc_context);
- //TRACE("result %d", chat_id);
-
- NAPI_RETURN_UINT32(chat_id);
-}
-
-NAPI_METHOD(dcn_create_contact) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(name, 1);
- NAPI_ARGV_UTF8_MALLOC(addr, 2);
-
- //TRACE("calling..");
- uint32_t contact_id = dc_create_contact(dcn_context->dc_context, name, addr);
- //TRACE("result %d", contact_id);
-
- free(name);
- free(addr);
-
- NAPI_RETURN_UINT32(contact_id);
-}
-
-NAPI_METHOD(dcn_create_group_chat) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_INT32(protect, 1);
- NAPI_ARGV_UTF8_MALLOC(chat_name, 2);
-
- //TRACE("calling..");
- uint32_t chat_id = dc_create_group_chat(dcn_context->dc_context, protect, chat_name);
- //TRACE("result %d", chat_id);
-
- free(chat_name);
-
- NAPI_RETURN_UINT32(chat_id);
-}
-
-NAPI_METHOD(dcn_delete_chat) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- //TRACE("calling..");
- dc_delete_chat(dcn_context->dc_context, chat_id);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_delete_contact) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(contact_id, 1);
-
- //TRACE("calling..");
- int result = dc_delete_contact(dcn_context->dc_context, contact_id);
- //TRACE("result %d", result);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_delete_msgs) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- napi_value js_array = argv[1];
-
- //TRACE("calling..");
- uint32_t length;
- uint32_t* msg_ids = js_array_to_uint32(env, js_array, &length);
- dc_delete_msgs(dcn_context->dc_context, msg_ids, length);
- free(msg_ids);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_forward_msgs) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- napi_value js_array = argv[1];
- NAPI_ARGV_UINT32(chat_id, 2);
-
- //TRACE("calling..");
- uint32_t length;
- uint32_t* msg_ids = js_array_to_uint32(env, js_array, &length);
- dc_forward_msgs(dcn_context->dc_context, msg_ids, length, chat_id);
- free(msg_ids);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_get_blobdir) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- //TRACE("calling..");
- char* blobdir = dc_get_blobdir(dcn_context->dc_context);
- //TRACE("result %s", blobdir);
-
- NAPI_RETURN_AND_UNREF_STRING(blobdir);
-}
-
-NAPI_METHOD(dcn_get_blocked_cnt) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- //TRACE("calling..");
- int blocked_cnt = dc_get_blocked_cnt(dcn_context->dc_context);
- //TRACE("result %d", blocked_cnt);
-
- NAPI_RETURN_INT32(blocked_cnt);
-}
-
-NAPI_METHOD(dcn_get_blocked_contacts) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- //TRACE("calling..");
- dc_array_t* contacts = dc_get_blocked_contacts(dcn_context->dc_context);
- napi_value js_array = dc_array_to_js_array(env, contacts);
- dc_array_unref(contacts);
- //TRACE("done");
-
- return js_array;
-}
-
-NAPI_METHOD(dcn_get_chat) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- //TRACE("calling..");
- napi_value result;
- dc_chat_t* chat = dc_get_chat(dcn_context->dc_context, chat_id);
-
- if (chat == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- NAPI_STATUS_THROWS(napi_create_external(env, chat, finalize_chat,
- NULL, &result));
- }
- //TRACE("done");
-
- return result;
-}
-
-NAPI_METHOD(dcn_get_chat_contacts) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- //TRACE("calling..");
- dc_array_t* contacts = dc_get_chat_contacts(dcn_context->dc_context, chat_id);
- napi_value js_array = dc_array_to_js_array(env, contacts);
- dc_array_unref(contacts);
- //TRACE("done");
-
- return js_array;
-}
-
-NAPI_METHOD(dcn_get_chat_encrinfo) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- char *value = dc_get_chat_encrinfo(dcn_context->dc_context, chat_id);
- NAPI_RETURN_AND_UNREF_STRING(value);
-}
-
-NAPI_METHOD(dcn_get_chat_id_by_contact_id) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(contact_id, 1);
-
- //TRACE("calling..");
- uint32_t chat_id = dc_get_chat_id_by_contact_id(dcn_context->dc_context,
- contact_id);
- //TRACE("result %d", chat_id);
-
- NAPI_RETURN_UINT32(chat_id);
-}
-
-NAPI_METHOD(dcn_get_chat_media) {
- NAPI_ARGV(5);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_INT32(msg_type1, 2);
- NAPI_ARGV_INT32(msg_type2, 3);
- NAPI_ARGV_INT32(msg_type3, 4);
-
- //TRACE("calling..");
- dc_array_t* msg_ids = dc_get_chat_media(dcn_context->dc_context,
- chat_id,
- msg_type1,
- msg_type2,
- msg_type3);
- napi_value js_array = dc_array_to_js_array(env, msg_ids);
- dc_array_unref(msg_ids);
- //TRACE("done");
-
- return js_array;
-}
-
-NAPI_METHOD(dcn_get_mime_headers) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(msg_id, 1);
-
- //TRACE("calling..");
- char* headers = dc_get_mime_headers(dcn_context->dc_context, msg_id);
- //TRACE("result %s", headers);
-
- NAPI_RETURN_AND_UNREF_STRING(headers);
-}
-
-NAPI_METHOD(dcn_get_chat_msgs) {
- NAPI_ARGV(4);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_UINT32(flags, 2);
- NAPI_ARGV_UINT32(marker1before, 3);
-
- //TRACE("calling..");
- dc_array_t* msg_ids = dc_get_chat_msgs(dcn_context->dc_context,
- chat_id,
- flags,
- marker1before);
- napi_value js_array = dc_array_to_js_array(env, msg_ids);
- dc_array_unref(msg_ids);
- //TRACE("done");
-
- return js_array;
-}
-
-NAPI_METHOD(dcn_get_chatlist) {
- NAPI_ARGV(4);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_INT32(listflags, 1);
- NAPI_ARGV_UTF8_MALLOC(query, 2);
- NAPI_ARGV_UINT32(query_contact_id, 3);
-
- //TRACE("calling..");
- dc_chatlist_t* chatlist = dc_get_chatlist(dcn_context->dc_context,
- listflags,
- query && query[0] ? query : NULL,
- query_contact_id);
-
- free(query);
-
- napi_value result;
- NAPI_STATUS_THROWS(napi_create_external(env,
- chatlist,
- finalize_chatlist,
- NULL,
- &result));
- //TRACE("done");
-
- return result;
-}
-
-NAPI_METHOD(dcn_get_config) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(key, 1);
-
- //TRACE("calling..");
- char *value = dc_get_config(dcn_context->dc_context, key);
- //TRACE("result %s", value);
-
- free(key);
-
- NAPI_RETURN_AND_UNREF_STRING(value);
-}
-
-NAPI_METHOD(dcn_get_contact) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(contact_id, 1);
-
- //TRACE("calling..");
- napi_value result;
- dc_contact_t* contact = dc_get_contact(dcn_context->dc_context, contact_id);
-
- if (contact == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- NAPI_STATUS_THROWS(napi_create_external(env, contact,
- finalize_contact,
- NULL, &result));
- }
- //TRACE("done");
-
- return result;
-}
-
-NAPI_METHOD(dcn_get_contact_encrinfo) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(contact_id, 1);
-
- //TRACE("calling..");
- char* encr_info = dc_get_contact_encrinfo(dcn_context->dc_context,
- contact_id);
- //TRACE("result %s", encr_info);
-
- NAPI_RETURN_AND_UNREF_STRING(encr_info);
-}
-
-NAPI_METHOD(dcn_get_contacts) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(listflags, 1);
- NAPI_ARGV_UTF8_MALLOC(query, 2);
-
- //TRACE("calling..");
- dc_array_t* contacts = dc_get_contacts(dcn_context->dc_context, listflags,
- query && query[0] ? query : NULL);
- napi_value js_array = dc_array_to_js_array(env, contacts);
- free(query);
- dc_array_unref(contacts);
- //TRACE("done");
-
- return js_array;
-}
-
-NAPI_METHOD(dcn_get_connectivity) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- int connectivity = dc_get_connectivity(dcn_context->dc_context);
- NAPI_RETURN_INT32(connectivity);
-}
-
-NAPI_METHOD(dcn_get_connectivity_html) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- char* connectivity = dc_get_connectivity_html(dcn_context->dc_context);
- NAPI_RETURN_AND_UNREF_STRING(connectivity);
-}
-
-NAPI_METHOD(dcn_was_device_msg_ever_added) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
-
- NAPI_ARGV_UTF8_MALLOC(label, 1);
-
- //TRACE("calling..");
-
- uint32_t added = dc_was_device_msg_ever_added(dcn_context->dc_context, label);
-
- free(label);
- //TRACE("done");
-
- NAPI_RETURN_UINT32(added);
-}
-
-NAPI_METHOD(dcn_get_draft) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- //TRACE("calling..");
- napi_value result;
- dc_msg_t* draft = dc_get_draft(dcn_context->dc_context, chat_id);
-
- if (draft == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- NAPI_STATUS_THROWS(napi_create_external(env, draft, finalize_msg,
- NULL, &result));
- }
- //TRACE("done");
-
- return result;
-}
-
-NAPI_METHOD(dcn_get_fresh_msg_cnt) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- //TRACE("calling..");
- int msg_cnt = dc_get_fresh_msg_cnt(dcn_context->dc_context, chat_id);
- //TRACE("result %d", msg_cnt);
-
- NAPI_RETURN_INT32(msg_cnt);
-}
-
-NAPI_METHOD(dcn_get_fresh_msgs) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- //TRACE("calling..");
- dc_array_t* msg_ids = dc_get_fresh_msgs(dcn_context->dc_context);
- napi_value js_array = dc_array_to_js_array(env, msg_ids);
- dc_array_unref(msg_ids);
- //TRACE("done");
-
- return js_array;
-}
-
-NAPI_METHOD(dcn_get_info) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- //TRACE("calling..");
- char *str = dc_get_info(dcn_context->dc_context);
- //TRACE("result %s", str);
-
- NAPI_RETURN_AND_UNREF_STRING(str);
-}
-
-NAPI_METHOD(dcn_get_msg) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(msg_id, 1);
-
- //TRACE("calling..");
- napi_value result;
- dc_msg_t* msg = dc_get_msg(dcn_context->dc_context, msg_id);
-
- if (msg == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- NAPI_STATUS_THROWS(napi_create_external(env, msg, finalize_msg,
- NULL, &result));
- }
- //TRACE("done");
-
- return result;
-}
-
-NAPI_METHOD(dcn_get_msg_cnt) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- //TRACE("calling..");
- int msg_cnt = dc_get_msg_cnt(dcn_context->dc_context, chat_id);
- //TRACE("result %d", msg_cnt);
-
- NAPI_RETURN_INT32(msg_cnt);
-}
-
-NAPI_METHOD(dcn_get_msg_info) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(msg_id, 1);
-
- //TRACE("calling..");
- char* msg_info = dc_get_msg_info(dcn_context->dc_context, msg_id);
- //TRACE("result %s", msg_info);
-
- NAPI_RETURN_AND_UNREF_STRING(msg_info);
-}
-
-
-NAPI_METHOD(dcn_get_msg_html) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(msg_id, 1);
-
- //TRACE("calling..");
- char* msg_html = dc_get_msg_html(dcn_context->dc_context, msg_id);
- //TRACE("result %s", msg_html);
-
- NAPI_RETURN_AND_UNREF_STRING(msg_html);
-}
-
-NAPI_METHOD(dcn_set_chat_visibility) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_INT32(visibility, 2);
- //TRACE("calling..");
- dc_set_chat_visibility(dcn_context->dc_context,
- chat_id,
- visibility);
- //TRACE("result %d", next_id);
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_get_securejoin_qr) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(group_chat_id, 1);
-
- //TRACE("calling..");
- char* code = dc_get_securejoin_qr(dcn_context->dc_context,
- group_chat_id);
- //TRACE("result %s", code);
-
- NAPI_RETURN_AND_UNREF_STRING(code);
-}
-
-NAPI_METHOD(dcn_get_securejoin_qr_svg) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(group_chat_id, 1);
-
- //TRACE("calling..");
- char* svg = dc_get_securejoin_qr_svg(dcn_context->dc_context, group_chat_id);
- //TRACE("result %s", code);
-
- NAPI_RETURN_AND_UNREF_STRING(svg);
-}
-
-NAPI_METHOD(dcn_imex) {
- NAPI_ARGV(4);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_INT32(what, 1);
- NAPI_ARGV_UTF8_MALLOC(param1, 2);
- NAPI_ARGV_UTF8_MALLOC(param2, 3);
-
- TRACE("calling..");
- dc_imex(dcn_context->dc_context,
- what,
- param1,
- param2 && param2[0] ? param2 : NULL);
-
- free(param1);
- free(param2);
- TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_imex_has_backup) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(dir_name, 1);
-
- //TRACE("calling..");
- char* file = dc_imex_has_backup(dcn_context->dc_context, dir_name);
- //TRACE("result %s", file);
-
- free(dir_name);
-
- NAPI_RETURN_AND_UNREF_STRING(file);
-}
-
-NAPI_ASYNC_CARRIER_BEGIN(dcn_initiate_key_transfer)
- char* result;
-NAPI_ASYNC_CARRIER_END(dcn_initiate_key_transfer)
-
-NAPI_ASYNC_EXECUTE(dcn_initiate_key_transfer) {
- NAPI_ASYNC_GET_CARRIER(dcn_initiate_key_transfer);
- carrier->result = dc_initiate_key_transfer(carrier->dcn_context->dc_context);
-}
-
-NAPI_ASYNC_COMPLETE(dcn_initiate_key_transfer) {
- NAPI_ASYNC_GET_CARRIER(dcn_initiate_key_transfer);
- if (status != napi_ok) {
- napi_throw_type_error(env, NULL, "Execute callback failed.");
- return;
- }
-
-#define DCN_INITIATE_KEY_TRANSFER_CALLBACK_ARGC 1
-
- const int argc = DCN_INITIATE_KEY_TRANSFER_CALLBACK_ARGC;
- napi_value argv[DCN_INITIATE_KEY_TRANSFER_CALLBACK_ARGC];
-
- if (carrier->result) {
- NAPI_STATUS_THROWS(napi_create_string_utf8(env, carrier->result, NAPI_AUTO_LENGTH, &argv[0]));
- } else {
- NAPI_STATUS_THROWS(napi_get_null(env, &argv[0]));
- }
-
- NAPI_ASYNC_CALL_AND_DELETE_CB();
- dc_str_unref(carrier->result);
- free(carrier);
-}
-
-NAPI_METHOD(dcn_initiate_key_transfer) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
-
- NAPI_ASYNC_NEW_CARRIER(dcn_initiate_key_transfer);
-
- NAPI_ASYNC_QUEUE_WORK(dcn_initiate_key_transfer, argv[1]);
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_is_configured) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- //TRACE("calling..");
- int result = dc_is_configured(dcn_context->dc_context);
- //TRACE("result %d", result);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_is_contact_in_chat) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_UINT32(contact_id, 2);
-
- //TRACE("calling..");
- int result = dc_is_contact_in_chat(dcn_context->dc_context,
- chat_id, contact_id);
- //TRACE("result %d", result);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_lookup_contact_id_by_addr) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(addr, 1);
-
- //TRACE("calling..");
- uint32_t res = dc_lookup_contact_id_by_addr(dcn_context->dc_context, addr);
- //TRACE("result %d", res);
-
- free(addr);
-
- NAPI_RETURN_UINT32(res);
-}
-
-NAPI_METHOD(dcn_marknoticed_chat) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- //TRACE("calling..");
- dc_marknoticed_chat(dcn_context->dc_context, chat_id);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_download_full_msg) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(msg_id, 1);
-
- //TRACE("calling..");
- dc_download_full_msg(dcn_context->dc_context, msg_id);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_markseen_msgs) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- napi_value js_array = argv[1];
-
- //TRACE("calling..");
- uint32_t length;
- uint32_t* msg_ids = js_array_to_uint32(env, js_array, &length);
- dc_markseen_msgs(dcn_context->dc_context, msg_ids, length);
- free(msg_ids);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_maybe_network) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- //TRACE("calling..");
- dc_maybe_network(dcn_context->dc_context);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_msg_new) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_INT32(viewtype, 1);
-
- //TRACE("calling..");
- napi_value result;
- dc_msg_t* msg = dc_msg_new(dcn_context->dc_context, viewtype);
-
- NAPI_STATUS_THROWS(napi_create_external(env, msg, finalize_msg,
- NULL, &result));
- //TRACE("done");
-
- return result;
-}
-
-
-NAPI_METHOD(dcn_remove_contact_from_chat) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_UINT32(contact_id, 2);
-
- //TRACE("calling..");
- int result = dc_remove_contact_from_chat(dcn_context->dc_context,
- chat_id, contact_id);
- //TRACE("result %d", result);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_search_msgs) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_UTF8_MALLOC(query, 2);
-
- //TRACE("calling..");
- dc_array_t* msg_ids = dc_search_msgs(dcn_context->dc_context,
- chat_id, query);
- napi_value js_array = dc_array_to_js_array(env, msg_ids);
- dc_array_unref(msg_ids);
- free(query);
- //TRACE("done");
-
- return js_array;
-}
-
-NAPI_METHOD(dcn_send_msg) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- //TRACE("calling..");
- dc_msg_t* dc_msg = NULL;
- napi_get_value_external(env, argv[2], (void**)&dc_msg);
-
- uint32_t msg_id = dc_send_msg(dcn_context->dc_context, chat_id, dc_msg);
- //TRACE("done");
-
- NAPI_RETURN_UINT32(msg_id);
-}
-
-NAPI_ASYNC_CARRIER_BEGIN(dcn_send_videochat_invitation)
- int chat_id;
- int result;
-NAPI_ASYNC_CARRIER_END(dcn_send_videochat_invitation)
-
-NAPI_ASYNC_EXECUTE(dcn_send_videochat_invitation) {
- NAPI_ASYNC_GET_CARRIER(dcn_send_videochat_invitation)
- carrier->result = dc_send_videochat_invitation(
- carrier->dcn_context->dc_context,
- carrier->chat_id
- );
-}
-
-NAPI_ASYNC_COMPLETE(dcn_send_videochat_invitation) {
- NAPI_ASYNC_GET_CARRIER(dcn_send_videochat_invitation)
- if (status != napi_ok) {
- napi_throw_type_error(env, NULL, "Execute callback failed.");
- return;
- }
-
-#define DCN_SEND_VIDEO_CHAT_CALLBACK_ARGC 1
-
- const int argc = DCN_SEND_VIDEO_CHAT_CALLBACK_ARGC;
- napi_value argv[DCN_SEND_VIDEO_CHAT_CALLBACK_ARGC];
- NAPI_STATUS_THROWS(napi_create_int32(env, carrier->result, &argv[0]));
-
- NAPI_ASYNC_CALL_AND_DELETE_CB()
- free(carrier);
-}
-
-NAPI_METHOD(dcn_send_videochat_invitation) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ASYNC_NEW_CARRIER(dcn_send_videochat_invitation);
- carrier->chat_id = chat_id;
-
- NAPI_ASYNC_QUEUE_WORK(dcn_send_videochat_invitation, argv[2]);
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_set_chat_name) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_UTF8_MALLOC(name, 2);
-
- //TRACE("calling..");
- int result = dc_set_chat_name(dcn_context->dc_context,
- chat_id,
- name);
- //TRACE("result %d", result);
-
- free(name);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_get_chat_ephemeral_timer) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- uint32_t result = dc_get_chat_ephemeral_timer(dcn_context->dc_context,
- chat_id);
- NAPI_RETURN_UINT32(result);
-}
-
-NAPI_METHOD(dcn_set_chat_ephemeral_timer) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_UINT32(timer, 2);
-
- int result = dc_set_chat_ephemeral_timer(dcn_context->dc_context,
- chat_id,
- timer);
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_set_chat_profile_image) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_UTF8_MALLOC(image, 2);
-
- //TRACE("calling..");
- int result = dc_set_chat_profile_image(dcn_context->dc_context,
- chat_id,
- image && image[0] ? image : NULL);
- //TRACE("result %d", result);
-
- free(image);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_set_chat_mute_duration) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
- NAPI_ARGV_INT32(duration, 2);
-
- //TRACE("calling..");
- int result = dc_set_chat_mute_duration(dcn_context->dc_context,
- chat_id,
- duration);
- //TRACE("result %d", result);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_set_config) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(key, 1);
- NAPI_ARGV_UTF8_MALLOC(value, 2);
-
- //TRACE("calling..");
- int status = dc_set_config(dcn_context->dc_context, key, value);
- //TRACE("result %d", status);
-
- free(key);
- free(value);
-
- NAPI_RETURN_INT32(status);
-}
-
-NAPI_METHOD(dcn_set_config_null) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(key, 1);
-
- //TRACE("calling..");
- int status = dc_set_config(dcn_context->dc_context, key, NULL);
- //TRACE("result %d", status);
-
- free(key);
-
- NAPI_RETURN_INT32(status);
-}
-
-NAPI_METHOD(dcn_set_config_from_qr) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(qr, 1);
-
- //TRACE("calling..");
- int status = dc_set_config_from_qr(dcn_context->dc_context, qr);
- //TRACE("result %d", status);
-
- free(qr);
-
- NAPI_RETURN_INT32(status);
-}
-
-NAPI_METHOD(dcn_estimate_deletion_cnt) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_INT32(from_server, 1);
- NAPI_ARGV_INT32(seconds, 2);
-
- int result = dc_estimate_deletion_cnt (dcn_context->dc_context, from_server, seconds);
-
- NAPI_RETURN_INT32(result);
-}
-
-
-NAPI_METHOD(dcn_set_draft) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(chat_id, 1);
-
- //TRACE("calling..");
- dc_msg_t* dc_msg = NULL;
- napi_get_value_external(env, argv[2], (void**)&dc_msg);
-
- dc_set_draft(dcn_context->dc_context, chat_id, dc_msg);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_set_stock_translation) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(stock_id, 1);
- NAPI_ARGV_UTF8_MALLOC(stock_msg, 2);
-
- int result = dc_set_stock_translation(dcn_context->dc_context, stock_id, stock_msg);
- free(stock_msg);
- NAPI_RETURN_INT32(result);
-}
-
-
-NAPI_METHOD(dcn_start_io) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- TRACE("calling..");
- TRACE("done");
-
- dc_start_io(dcn_context->dc_context);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_stop_io) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- dc_stop_io(dcn_context->dc_context);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_stop_ongoing_process) {
- NAPI_ARGV(1);
- NAPI_DCN_CONTEXT();
-
- //TRACE("calling..");
- dc_stop_ongoing_process(dcn_context->dc_context);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-/**
- * dc_chat_t
- */
-
-NAPI_METHOD(dcn_chat_get_color) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- uint32_t color = dc_chat_get_color(dc_chat);
- //TRACE("result %d", color);
-
- NAPI_RETURN_UINT32(color);
-}
-
-NAPI_METHOD(dcn_chat_get_visibility) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- uint32_t visibility = dc_chat_get_visibility(dc_chat);
- //TRACE("result %d", color);
-
- NAPI_RETURN_UINT32(visibility);
-}
-
-NAPI_METHOD(dcn_chat_get_id) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- uint32_t chat_id = dc_chat_get_id(dc_chat);
- //TRACE("result %d", chat_id);
-
- NAPI_RETURN_UINT32(chat_id);
-}
-
-NAPI_METHOD(dcn_chat_get_name) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- char* name = dc_chat_get_name(dc_chat);
- //TRACE("result %s", name);
-
- NAPI_RETURN_AND_UNREF_STRING(name);
-}
-
-NAPI_METHOD(dcn_chat_get_mailinglist_addr) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- char* addr = dc_chat_get_mailinglist_addr(dc_chat);
- //TRACE("result %s", name);
-
- NAPI_RETURN_AND_UNREF_STRING(addr);
-}
-
-
-NAPI_METHOD(dcn_chat_get_profile_image) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- char* profile_image = dc_chat_get_profile_image(dc_chat);
- //TRACE("result %s", profile_image);
-
- NAPI_RETURN_AND_UNREF_STRING(profile_image);
-}
-
-NAPI_METHOD(dcn_chat_get_type) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- int type = dc_chat_get_type(dc_chat);
- //TRACE("result %d", type);
-
- NAPI_RETURN_INT32(type);
-}
-
-NAPI_METHOD(dcn_chat_is_self_talk) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- int is_self_talk = dc_chat_is_self_talk(dc_chat);
- //TRACE("result %d", is_self_talk);
-
- NAPI_RETURN_INT32(is_self_talk);
-}
-
-NAPI_METHOD(dcn_chat_is_unpromoted) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- int is_unpromoted = dc_chat_is_unpromoted(dc_chat);
- //TRACE("result %d", is_unpromoted);
-
- NAPI_RETURN_INT32(is_unpromoted);
-}
-
-NAPI_METHOD(dcn_chat_can_send) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- int can_send = dc_chat_can_send(dc_chat);
- //TRACE("result %d", can_send);
-
- NAPI_RETURN_INT32(can_send);
-}
-
-NAPI_METHOD(dcn_chat_is_protected) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- int is_protected = dc_chat_is_protected(dc_chat);
- //TRACE("result %d", is_protected);
-
- NAPI_RETURN_INT32(is_protected);
-}
-
-NAPI_METHOD(dcn_chat_is_device_talk) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- int is_device_talk = dc_chat_is_device_talk(dc_chat);
- //TRACE("result %d", is_device_talk);
-
- NAPI_RETURN_INT32(is_device_talk);
-}
-
-NAPI_METHOD(dcn_chat_is_muted) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- int is_muted = dc_chat_is_muted(dc_chat);
- //TRACE("result %d", is_muted);
-
- NAPI_RETURN_INT32(is_muted);
-}
-
-NAPI_METHOD(dcn_chat_is_contact_request) {
- NAPI_ARGV(1);
- NAPI_DC_CHAT();
-
- //TRACE("calling..");
- int is_contact_request = dc_chat_is_contact_request(dc_chat);
- //TRACE("result %d", is_muted);
-
- NAPI_RETURN_INT32(is_contact_request);
-}
-
-
-
-/**
- * dc_chatlist_t
- */
-
-NAPI_METHOD(dcn_chatlist_get_chat_id) {
- NAPI_ARGV(2);
- NAPI_DC_CHATLIST();
- NAPI_ARGV_INT32(index, 1);
-
- //TRACE("calling..");
- uint32_t chat_id = dc_chatlist_get_chat_id(dc_chatlist, index);
- //TRACE("result %d", chat_id);
-
- NAPI_RETURN_UINT32(chat_id);
-}
-
-NAPI_METHOD(dcn_chatlist_get_cnt) {
- NAPI_ARGV(1);
- NAPI_DC_CHATLIST();
-
- //TRACE("calling..");
- int count = dc_chatlist_get_cnt(dc_chatlist);
- //TRACE("result %d", count);
-
- NAPI_RETURN_INT32(count);
-}
-
-NAPI_METHOD(dcn_chatlist_get_msg_id) {
- NAPI_ARGV(2);
- NAPI_DC_CHATLIST();
- NAPI_ARGV_INT32(index, 1);
-
- //TRACE("calling..");
- uint32_t message_id = dc_chatlist_get_msg_id(dc_chatlist, index);
- //TRACE("result %d", message_id);
-
- NAPI_RETURN_UINT32(message_id);
-}
-
-NAPI_METHOD(dcn_chatlist_get_summary) {
- NAPI_ARGV(3);
- NAPI_DC_CHATLIST();
- NAPI_ARGV_INT32(index, 1);
-
- //TRACE("calling..");
- dc_chat_t* dc_chat = NULL;
- napi_get_value_external(env, argv[2], (void**)&dc_chat);
-
- dc_lot_t* summary = dc_chatlist_get_summary(dc_chatlist, index, dc_chat);
-
- napi_value result;
- if (summary == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- NAPI_STATUS_THROWS(napi_create_external(env, summary,
- finalize_lot,
- NULL, &result));
- }
- //TRACE("done");
-
- return result;
-}
-
-NAPI_METHOD(dcn_chatlist_get_summary2) {
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_INT32(chat_id, 1);
- NAPI_ARGV_INT32(message_id, 2);
-
- //TRACE("calling..");
- dc_lot_t* summary = dc_chatlist_get_summary2(dcn_context->dc_context, chat_id, message_id);
-
- napi_value result;
- if (summary == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- NAPI_STATUS_THROWS(napi_create_external(env, summary,
- finalize_lot,
- NULL, &result));
- }
- //TRACE("done");
-
- return result;
-}
-
-/**
- * dc_contact_t
- */
-
-NAPI_METHOD(dcn_contact_get_addr) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
-
- //TRACE("calling..");
- char* addr = dc_contact_get_addr(dc_contact);
- //TRACE("result %s", addr);
-
- NAPI_RETURN_AND_UNREF_STRING(addr);
-}
-
-NAPI_METHOD(dcn_contact_get_auth_name) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
-
- char* auth_name = dc_contact_get_auth_name(dc_contact);
-
- NAPI_RETURN_AND_UNREF_STRING(auth_name);
-}
-
-NAPI_METHOD(dcn_contact_get_color) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
-
- //TRACE("calling..");
- uint32_t color = dc_contact_get_color(dc_contact);
- //TRACE("result %d", color);
-
- NAPI_RETURN_UINT32(color);
-}
-
-NAPI_METHOD(dcn_contact_get_display_name) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
-
- //TRACE("calling..");
- char* display_name = dc_contact_get_display_name(dc_contact);
- //TRACE("result %s", display_name);
-
- NAPI_RETURN_AND_UNREF_STRING(display_name);
-}
-
-NAPI_METHOD(dcn_contact_get_id) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
-
- //TRACE("calling..");
- uint32_t contact_id = dc_contact_get_id(dc_contact);
- //TRACE("result %d", contact_id);
-
- NAPI_RETURN_UINT32(contact_id);
-}
-
-NAPI_METHOD(dcn_contact_get_name) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
-
- //TRACE("calling..");
- char* name = dc_contact_get_name(dc_contact);
- //TRACE("result %s", name);
-
- NAPI_RETURN_AND_UNREF_STRING(name);
-}
-
-NAPI_METHOD(dcn_contact_get_name_n_addr) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
-
- //TRACE("calling..");
- char* name_n_addr = dc_contact_get_name_n_addr(dc_contact);
- //TRACE("result %s", name_n_addr);
-
- NAPI_RETURN_AND_UNREF_STRING(name_n_addr);
-}
-
-NAPI_METHOD(dcn_contact_get_profile_image) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
-
- //TRACE("calling..");
- char* profile_image = dc_contact_get_profile_image(dc_contact);
- //TRACE("result %s", profile_image);
-
- NAPI_RETURN_AND_UNREF_STRING(profile_image);
-}
-
-NAPI_METHOD(dcn_contact_get_status) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
- char* status = dc_contact_get_status(dc_contact);
- NAPI_RETURN_AND_UNREF_STRING(status);
-}
-
-NAPI_METHOD(dcn_contact_get_last_seen) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
- int64_t timestamp = dc_contact_get_last_seen(dc_contact);
- NAPI_RETURN_INT64(timestamp);
-}
-
-NAPI_METHOD(dcn_contact_was_seen_recently) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
- int seen_recently = dc_contact_was_seen_recently(dc_contact);
- NAPI_RETURN_UINT32(seen_recently);
-}
-
-NAPI_METHOD(dcn_contact_is_blocked) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
-
- //TRACE("calling..");
- int is_blocked = dc_contact_is_blocked(dc_contact);
- //TRACE("result %d", is_blocked);
-
- NAPI_RETURN_UINT32(is_blocked);
-}
-
-NAPI_METHOD(dcn_contact_is_verified) {
- NAPI_ARGV(1);
- NAPI_DC_CONTACT();
-
- //TRACE("calling..");
- int is_verified = dc_contact_is_verified(dc_contact);
- //TRACE("result %d", is_verified);
-
- NAPI_RETURN_UINT32(is_verified);
-}
-
-/**
- * dc_lot_t
- */
-
-NAPI_METHOD(dcn_lot_get_id) {
- NAPI_ARGV(1);
- NAPI_DC_LOT();
-
- //TRACE("calling..");
- uint32_t id = dc_lot_get_id(dc_lot);
- //TRACE("result %d", id);
-
- NAPI_RETURN_UINT32(id);
-}
-
-NAPI_METHOD(dcn_lot_get_state) {
- NAPI_ARGV(1);
- NAPI_DC_LOT();
-
- //TRACE("calling..");
- int state = dc_lot_get_state(dc_lot);
- //TRACE("result %d", state);
-
- NAPI_RETURN_INT32(state);
-}
-
-NAPI_METHOD(dcn_lot_get_text1) {
- NAPI_ARGV(1);
- NAPI_DC_LOT();
-
- //TRACE("calling..");
- char* text1 = dc_lot_get_text1(dc_lot);
- //TRACE("result %s", text1);
-
- NAPI_RETURN_AND_UNREF_STRING(text1);
-}
-
-NAPI_METHOD(dcn_lot_get_text1_meaning) {
- NAPI_ARGV(1);
- NAPI_DC_LOT();
-
- //TRACE("calling..");
- int text1_meaning = dc_lot_get_text1_meaning(dc_lot);
- //TRACE("result %d", text1_meaning);
-
- NAPI_RETURN_INT32(text1_meaning);
-}
-
-NAPI_METHOD(dcn_lot_get_text2) {
- NAPI_ARGV(1);
- NAPI_DC_LOT();
-
- //TRACE("calling..");
- char* text2 = dc_lot_get_text2(dc_lot);
- //TRACE("result %s", text2);
-
- NAPI_RETURN_AND_UNREF_STRING(text2);
-}
-
-NAPI_METHOD(dcn_lot_get_timestamp) {
- NAPI_ARGV(1);
- NAPI_DC_LOT();
-
- //TRACE("calling..");
- int timestamp = dc_lot_get_timestamp(dc_lot);
- //TRACE("result %d", timestamp);
-
- NAPI_RETURN_INT32(timestamp);
-}
-
-/**
- * dc_msg_t
- */
-
-NAPI_METHOD(dcn_msg_get_parent) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- napi_value result;
- dc_msg_t* msg = dc_msg_get_parent(dc_msg);
-
- if (msg == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- NAPI_STATUS_THROWS(napi_create_external(env, msg, finalize_msg,
- NULL, &result));
- }
-
- return result;
-}
-
-NAPI_METHOD(dcn_msg_get_download_state) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- uint32_t download_state = dc_msg_get_download_state(dc_msg);
- //TRACE("result %d", download_state);
-
- NAPI_RETURN_UINT32(download_state);
-}
-
-NAPI_METHOD(dcn_msg_get_chat_id) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- uint32_t chat_id = dc_msg_get_chat_id(dc_msg);
- //TRACE("result %d", chat_id);
-
- NAPI_RETURN_UINT32(chat_id);
-}
-
-NAPI_METHOD(dcn_msg_get_duration) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int duration = dc_msg_get_duration(dc_msg);
- //TRACE("result %d", duration);
-
- NAPI_RETURN_INT32(duration);
-}
-
-NAPI_METHOD(dcn_msg_get_file) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- char* file = dc_msg_get_file(dc_msg);
- //TRACE("result %s", file);
-
- NAPI_RETURN_AND_UNREF_STRING(file);
-}
-
-NAPI_METHOD(dcn_msg_get_filebytes) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- uint32_t filebytes = dc_msg_get_filebytes(dc_msg);
- //TRACE("result %d", filebytes);
-
- NAPI_RETURN_INT32(filebytes);
-}
-
-NAPI_METHOD(dcn_msg_get_filemime) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- char* filemime = dc_msg_get_filemime(dc_msg);
- //TRACE("result %s", filemime);
-
- NAPI_RETURN_AND_UNREF_STRING(filemime);
-}
-
-NAPI_METHOD(dcn_msg_get_filename) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- char* filename = dc_msg_get_filename(dc_msg);
- //TRACE("result %s", filename);
-
- NAPI_RETURN_AND_UNREF_STRING(filename);
-}
-
-NAPI_METHOD(dcn_msg_get_from_id) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- uint32_t contact_id = dc_msg_get_from_id(dc_msg);
- //TRACE("result %d", contact_id);
-
- NAPI_RETURN_UINT32(contact_id);
-}
-
-NAPI_METHOD(dcn_msg_get_height) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int height = dc_msg_get_height(dc_msg);
- //TRACE("result %d", height);
-
- NAPI_RETURN_INT32(height);
-}
-
-NAPI_METHOD(dcn_msg_get_id) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- uint32_t msg_id = dc_msg_get_id(dc_msg);
- //TRACE("result %d", msg_id);
-
- NAPI_RETURN_UINT32(msg_id);
-}
-
-NAPI_METHOD(dcn_msg_get_override_sender_name) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- char* override_sender_name = dc_msg_get_override_sender_name(dc_msg);
- //TRACE("result %s", override_sender_name);
-
- NAPI_RETURN_AND_UNREF_STRING(override_sender_name);
-}
-
-NAPI_METHOD(dcn_msg_get_quoted_text) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- char* text = dc_msg_get_quoted_text(dc_msg);
- //TRACE("result %s", text);
-
- NAPI_RETURN_AND_UNREF_STRING(text);
-}
-
-NAPI_METHOD(dcn_msg_get_quoted_msg) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- napi_value result;
- dc_msg_t* msg = dc_msg_get_quoted_msg(dc_msg);
-
- if (msg == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- NAPI_STATUS_THROWS(napi_create_external(env, msg, finalize_msg,
- NULL, &result));
- }
-
- return result;
-}
-
-NAPI_METHOD(dcn_msg_get_received_timestamp) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int timestamp = dc_msg_get_received_timestamp(dc_msg);
- //TRACE("result %d", timestamp);
-
- NAPI_RETURN_INT32(timestamp);
-}
-
-
-NAPI_METHOD(dcn_msg_get_setupcodebegin) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- char* setupcodebegin = dc_msg_get_setupcodebegin(dc_msg);
- //TRACE("result %s", setupcodebegin);
-
- NAPI_RETURN_AND_UNREF_STRING(setupcodebegin);
-}
-
-NAPI_METHOD(dcn_msg_get_showpadlock) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int showpadlock = dc_msg_get_showpadlock(dc_msg);
- //TRACE("result %d", showpadlock);
-
- NAPI_RETURN_INT32(showpadlock);
-}
-
-NAPI_METHOD(dcn_msg_get_sort_timestamp) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int timestamp = dc_msg_get_sort_timestamp(dc_msg);
- //TRACE("result %d", timestamp);
-
- NAPI_RETURN_INT32(timestamp);
-}
-
-NAPI_METHOD(dcn_msg_get_state) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int state = dc_msg_get_state(dc_msg);
- //TRACE("result %d", state);
-
- NAPI_RETURN_INT32(state);
-}
-
-NAPI_METHOD(dcn_msg_get_summary) {
- NAPI_ARGV(2);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- dc_chat_t* dc_chat = NULL;
- napi_get_value_external(env, argv[1], (void**)&dc_chat);
-
- dc_lot_t* summary = dc_msg_get_summary(dc_msg, dc_chat);
-
- napi_value result;
- NAPI_STATUS_THROWS(napi_create_external(env, summary,
- finalize_lot,
- NULL, &result));
- //TRACE("done");
-
- return result;
-}
-
-NAPI_METHOD(dcn_msg_get_summarytext) {
- NAPI_ARGV(2);
- NAPI_DC_MSG();
- NAPI_ARGV_INT32(approx_characters, 1);
-
- //TRACE("calling..");
- char* summarytext = dc_msg_get_summarytext(dc_msg, approx_characters);
- //TRACE("result %s", summarytext);
-
- NAPI_RETURN_AND_UNREF_STRING(summarytext);
-}
-
-NAPI_METHOD(dcn_msg_get_subject) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- char* subject = dc_msg_get_subject(dc_msg);
- //TRACE("result %s", subject);
-
- NAPI_RETURN_AND_UNREF_STRING(subject);
-}
-
-NAPI_METHOD(dcn_msg_get_text) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- char* text = dc_msg_get_text(dc_msg);
- //TRACE("result %s", text);
-
- NAPI_RETURN_AND_UNREF_STRING(text);
-}
-
-NAPI_METHOD(dcn_msg_get_timestamp) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int timestamp = dc_msg_get_timestamp(dc_msg);
- //TRACE("result %d", timestamp);
-
- NAPI_RETURN_INT32(timestamp);
-}
-
-NAPI_METHOD(dcn_msg_get_viewtype) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int type = dc_msg_get_viewtype(dc_msg);
- //TRACE("result %d", type);
-
- NAPI_RETURN_INT32(type);
-}
-
-NAPI_METHOD(dcn_msg_get_videochat_type) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
- int type = dc_msg_get_videochat_type(dc_msg);
- NAPI_RETURN_INT32(type);
-}
-
-NAPI_METHOD(dcn_msg_get_videochat_url) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
- char* url = dc_msg_get_videochat_url(dc_msg);
- NAPI_RETURN_AND_UNREF_STRING(url);
-}
-
-NAPI_METHOD(dcn_msg_get_width) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int width = dc_msg_get_width(dc_msg);
- //TRACE("result %d", width);
-
- NAPI_RETURN_INT32(width);
-}
-
-NAPI_METHOD(dcn_msg_get_webxdc_info){
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- char* result_json = dc_msg_get_webxdc_info(dc_msg);
-
- NAPI_RETURN_AND_UNREF_STRING(result_json);
-}
-
-NAPI_METHOD(dcn_msg_has_deviating_timestamp) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int has_deviating_timestamp = dc_msg_has_deviating_timestamp(dc_msg);
- //TRACE("result %d", has_deviating_timestamp);
-
- NAPI_RETURN_INT32(has_deviating_timestamp);
-}
-
-NAPI_METHOD(dcn_msg_has_location) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int has_location = dc_msg_has_location(dc_msg);
- //TRACE("result %d", has_location);
-
- NAPI_RETURN_INT32(has_location);
-}
-
-NAPI_METHOD(dcn_msg_has_html) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int has_html = dc_msg_has_html(dc_msg);
- //TRACE("result %d", has_html);
-
- NAPI_RETURN_INT32(has_html);
-}
-
-NAPI_METHOD(dcn_msg_is_forwarded) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int is_forwarded = dc_msg_is_forwarded(dc_msg);
- //TRACE("result %d", is_forwarded);
-
- NAPI_RETURN_INT32(is_forwarded);
-}
-
-NAPI_METHOD(dcn_msg_is_info) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int is_info = dc_msg_is_info(dc_msg);
- //TRACE("result %d", is_info);
-
- NAPI_RETURN_INT32(is_info);
-}
-
-NAPI_METHOD(dcn_msg_is_sent) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int is_sent = dc_msg_is_sent(dc_msg);
- //TRACE("result %d", is_sent);
-
- NAPI_RETURN_INT32(is_sent);
-}
-
-NAPI_METHOD(dcn_msg_is_setupmessage) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
-
- //TRACE("calling..");
- int is_setupmessage = dc_msg_is_setupmessage(dc_msg);
- //TRACE("result %d", is_setupmessage);
-
- NAPI_RETURN_INT32(is_setupmessage);
-}
-
-NAPI_METHOD(dcn_msg_latefiling_mediasize) {
- NAPI_ARGV(4);
- NAPI_DC_MSG();
- NAPI_ARGV_INT32(width, 1);
- NAPI_ARGV_INT32(height, 2);
- NAPI_ARGV_INT32(duration, 3);
-
- //TRACE("calling..");
- dc_msg_latefiling_mediasize(dc_msg, width, height, duration);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-
-NAPI_METHOD(dcn_msg_force_plaintext) {
- NAPI_ARGV(1);
- NAPI_DC_MSG();
- dc_msg_force_plaintext(dc_msg);
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_msg_set_dimension) {
- NAPI_ARGV(3);
- NAPI_DC_MSG();
- NAPI_ARGV_INT32(width, 1);
- NAPI_ARGV_INT32(height, 2);
-
- //TRACE("calling..");
- dc_msg_set_dimension(dc_msg, width, height);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_msg_set_duration) {
- NAPI_ARGV(2);
- NAPI_DC_MSG();
- NAPI_ARGV_INT32(duration, 1);
-
- //TRACE("calling..");
- dc_msg_set_duration(dc_msg, duration);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_msg_set_override_sender_name) {
- NAPI_ARGV(2);
- NAPI_DC_MSG();
- NAPI_ARGV_UTF8_MALLOC(override_sender_name, 1);
-
- //TRACE("calling..");
- dc_msg_set_override_sender_name(dc_msg, override_sender_name);
- //TRACE("done");
-
- free(override_sender_name);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_msg_set_file) {
- NAPI_ARGV(3);
- NAPI_DC_MSG();
- NAPI_ARGV_UTF8_MALLOC(file, 1);
- NAPI_ARGV_UTF8_MALLOC(filemime, 2);
-
- //TRACE("calling..");
- dc_msg_set_file(dc_msg, file, filemime && filemime[0] ? filemime : NULL);
- //TRACE("done");
-
- free(file);
- free(filemime);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_msg_set_html) {
- NAPI_ARGV(2);
- NAPI_DC_MSG();
- NAPI_ARGV_UTF8_MALLOC(html, 1);
-
- //TRACE("calling..");
- dc_msg_set_html(dc_msg, html);
- //TRACE("done");
-
- free(html);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_msg_set_quote) {
- NAPI_ARGV(2);
- NAPI_ARGV_DC_MSG(dc_msg, 0)
-
- dc_msg_t* dc_msg_quote = NULL;
- napi_get_value_external(env, argv[1], (void**)&dc_msg_quote);
-
- dc_msg_set_quote(dc_msg, dc_msg_quote);
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_msg_set_text) {
- NAPI_ARGV(2);
- NAPI_DC_MSG();
- NAPI_ARGV_UTF8_MALLOC(text, 1);
-
- //TRACE("calling..");
- dc_msg_set_text(dc_msg, text);
- //TRACE("done");
-
- free(text);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-/**
- * locations
- */
-
-NAPI_METHOD(dcn_msg_set_location) {
- NAPI_ARGV(3);
- NAPI_DC_MSG();
- NAPI_ARGV_DOUBLE(latitude, 1);
- NAPI_ARGV_DOUBLE(longitude, 2);
-
- //TRACE("calling..");
- dc_msg_set_location(dc_msg, latitude, longitude);
- //TRACE("done");
-
- NAPI_RETURN_UNDEFINED();
-}
-
-/**
- * locations
- */
-
-NAPI_METHOD(dcn_set_location) {
- NAPI_ARGV(4);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_DOUBLE(latitude, 1);
- NAPI_ARGV_DOUBLE(longitude, 2);
- NAPI_ARGV_DOUBLE(accuracy, 3);
-
- //TRACE("calling..");
- int result = dc_set_location(dcn_context->dc_context, latitude, longitude, accuracy);
- //TRACE("result %d", result);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_get_locations) {
- NAPI_ARGV(5);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_INT32(chat_id, 1);
- NAPI_ARGV_INT32(contact_id, 2);
- NAPI_ARGV_INT32(timestamp_from, 3);
- NAPI_ARGV_INT32(timestamp_to, 4);
-
- //TRACE("calling..");
- dc_array_t* locations = dc_get_locations(dcn_context->dc_context,
- chat_id,
- contact_id,
- timestamp_from,
- timestamp_to);
-
- napi_value napi_locations;
- NAPI_STATUS_THROWS(napi_create_external(env, locations,
- finalize_array,
- NULL, &napi_locations));
- //TRACE("done");
-
- return napi_locations;
-}
-
-NAPI_METHOD(dcn_array_get_cnt) {
- NAPI_ARGV(1);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t size = dc_array_get_cnt(dc_array);
-
- napi_value napi_size;
- NAPI_STATUS_THROWS(napi_create_uint32(env, size, &napi_size));
- //TRACE("done");
-
- return napi_size;
-}
-
-NAPI_METHOD(dcn_array_get_id) {
- NAPI_ARGV(2);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t index;
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index));
-
- uint32_t id = dc_array_get_id(dc_array, index);
-
- napi_value napi_id;
- NAPI_STATUS_THROWS(napi_create_uint32(env, id, &napi_id));
- //TRACE("done");
-
- return napi_id;
-}
-
-NAPI_METHOD(dcn_array_get_accuracy) {
- NAPI_ARGV(2);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t index;
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index));
-
- double accuracy = dc_array_get_accuracy(dc_array, index);
-
- napi_value napi_accuracy;
- NAPI_STATUS_THROWS(napi_create_double(env, accuracy, &napi_accuracy));
- //TRACE("done");
-
- return napi_accuracy;
-}
-
-NAPI_METHOD(dcn_array_get_longitude) {
- NAPI_ARGV(2);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t index;
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index));
-
- double longitude = dc_array_get_longitude(dc_array, index);
-
- napi_value napi_longitude;
- NAPI_STATUS_THROWS(napi_create_double(env, longitude, &napi_longitude));
- //TRACE("done");
-
- return napi_longitude;
-}
-
-NAPI_METHOD(dcn_array_get_latitude) {
- NAPI_ARGV(2);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t index;
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index));
-
- double latitude = dc_array_get_latitude(dc_array, index);
-
- napi_value napi_latitude;
- NAPI_STATUS_THROWS(napi_create_double(env, latitude, &napi_latitude));
- //TRACE("done");
-
- return napi_latitude;
-}
-
-NAPI_METHOD(dcn_array_get_timestamp) {
- NAPI_ARGV(2);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t index;
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index));
-
- int timestamp = dc_array_get_timestamp(dc_array, index);
-
- napi_value napi_timestamp;
- NAPI_STATUS_THROWS(napi_create_int64(env, timestamp, &napi_timestamp));
- //TRACE("done");
-
- return napi_timestamp;
-}
-
-NAPI_METHOD(dcn_array_get_msg_id) {
- NAPI_ARGV(2);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t index;
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index));
-
- uint32_t msg_id = dc_array_get_msg_id(dc_array, index);
-
- napi_value napi_msg_id;
- NAPI_STATUS_THROWS(napi_create_uint32(env, msg_id, &napi_msg_id));
- //TRACE("done");
-
- return napi_msg_id;
-}
-
-NAPI_METHOD(dcn_array_is_independent) {
- NAPI_ARGV(2);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t index;
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index));
-
- int result = dc_array_is_independent(dc_array, index);
- //TRACE("result %d", result);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_array_get_marker) {
- NAPI_ARGV(2);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t index;
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index));
-
- char* marker = dc_array_get_marker(dc_array, index);
- //TRACE("result %s", marker);
-
- NAPI_RETURN_AND_UNREF_STRING(marker);
-}
-
-NAPI_METHOD(dcn_array_get_contact_id) {
- NAPI_ARGV(2);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t index;
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index));
-
- uint32_t contact_id = dc_array_get_contact_id(dc_array, index);
-
- napi_value napi_contact_id;
- NAPI_STATUS_THROWS(napi_create_uint32(env, contact_id, &napi_contact_id));
- //TRACE("done");
-
- return napi_contact_id;
-}
-
-NAPI_METHOD(dcn_array_get_chat_id) {
- NAPI_ARGV(2);
- NAPI_DC_ARRAY();
-
- //TRACE("calling..");
- uint32_t index;
- NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &index));
-
- uint32_t chat_id = dc_array_get_chat_id(dc_array, index);
-
- napi_value napi_chat_id;
- NAPI_STATUS_THROWS(napi_create_uint32(env, chat_id, &napi_chat_id));
- //TRACE("done");
-
- return napi_chat_id;
-}
-
-NAPI_METHOD(dcn_provider_new_from_email) {
- NAPI_ARGV(2);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UTF8_MALLOC(email, 1)
-
- //TRACE("calling..");
- napi_value result;
- dc_provider_t* provider = dc_provider_new_from_email(dcn_context->dc_context, email);
-
- if (provider == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- NAPI_STATUS_THROWS(napi_create_external(env, provider, finalize_provider,
- NULL, &result));
- }
- //TRACE("done");
-
- return result;
-}
-
-NAPI_METHOD(dcn_provider_get_overview_page) {
- NAPI_ARGV(1);
- NAPI_DC_PROVIDER();
-
- //TRACE("calling..");
- char* overview_page = dc_provider_get_overview_page(dc_provider);
- //TRACE("result %s", overview_page);
-
- NAPI_RETURN_AND_UNREF_STRING(overview_page);
-}
-
-NAPI_METHOD(dcn_provider_get_before_login_hint) {
- NAPI_ARGV(1);
- NAPI_DC_PROVIDER();
-
- //TRACE("calling..");
- char* before_login_hint = dc_provider_get_before_login_hint(dc_provider);
- //TRACE("result %s", before_login_hint);
-
- NAPI_RETURN_AND_UNREF_STRING(before_login_hint);
-}
-
-NAPI_METHOD(dcn_provider_get_status) {
- NAPI_ARGV(1);
- NAPI_DC_PROVIDER();
-
- //TRACE("calling..");
- int status = dc_provider_get_status(dc_provider);
- //TRACE("result %s", status);
-
- NAPI_RETURN_INT32(status)
-}
-
-// webxdc
-
-NAPI_METHOD(dcn_send_webxdc_status_update){
- NAPI_ARGV(4);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(msg_id, 1);
- NAPI_ARGV_UTF8_MALLOC(json, 2);
- NAPI_ARGV_UTF8_MALLOC(descr, 3);
-
- int result = dc_send_webxdc_status_update(dcn_context->dc_context, msg_id, json, descr);
- free(json);
- free(descr);
-
- NAPI_RETURN_UINT32(result);
-}
-
-NAPI_METHOD(dcn_get_webxdc_status_updates){
- NAPI_ARGV(3);
- NAPI_DCN_CONTEXT();
- NAPI_ARGV_UINT32(msg_id, 1);
- NAPI_ARGV_UINT32(serial, 2);
-
- char* result_json = dc_get_webxdc_status_updates(dcn_context->dc_context, msg_id, serial);
-
- NAPI_RETURN_AND_UNREF_STRING(result_json);
-}
-
-NAPI_METHOD(dcn_msg_get_webxdc_blob){
- NAPI_ARGV(2);
- NAPI_DC_MSG();
- NAPI_ARGV_UTF8_MALLOC(filename, 1);
-
- size_t size;
- char* data = dc_msg_get_webxdc_blob(dc_msg, filename, &size);
- free(filename);
-
- napi_value jsbuffer;
- if (data == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &jsbuffer));
- } else {
- // https://nodejs.org/api/n-api.html#napi_create_buffer_copy
- NAPI_STATUS_THROWS(napi_create_buffer_copy(env,
- size,
- data,
- NULL,
- &jsbuffer))
- dc_str_unref(data);
- }
-
- return jsbuffer;
-}
-
-
-// dc_accounts_*
-
-NAPI_METHOD(dcn_accounts_new) {
- NAPI_ARGV(2);
- NAPI_ARGV_UTF8_MALLOC(dir, 0);
- NAPI_ARGV_INT32(writable, 1);
- TRACE("calling..");
-
- dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t));
- if (dcn_accounts == NULL) {
- napi_throw_error(env, NULL, "dcn_accounts is null"); \
- }
-
-
- dcn_accounts->dc_accounts = dc_accounts_new(dir, writable);
-
- napi_value result;
- NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts,
- NULL, NULL, &result));
- return result;
-}
-
-
-NAPI_METHOD(dcn_accounts_unref) {
- NAPI_ARGV(1);
- NAPI_DCN_ACCOUNTS();
-
-
- TRACE("Unrefing dc_accounts");
- dcn_accounts->gc = 1;
- if (dcn_accounts->event_handler_thread != 0) {
- dc_accounts_stop_io(dcn_accounts->dc_accounts);
- uv_thread_join(&dcn_accounts->event_handler_thread);
- dcn_accounts->event_handler_thread = 0;
- }
- if (dcn_accounts->jsonrpc_instance) {
- dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, "{}");
- uv_thread_join(&dcn_accounts->jsonrpc_thread);
- dc_jsonrpc_unref(dcn_accounts->jsonrpc_instance);
- dcn_accounts->jsonrpc_instance = NULL;
- }
- dc_accounts_unref(dcn_accounts->dc_accounts);
- dcn_accounts->dc_accounts = NULL;
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_accounts_add_account) {
- NAPI_ARGV(1);
- NAPI_DCN_ACCOUNTS();
-
- int account_id = dc_accounts_add_account(dcn_accounts->dc_accounts);
-
- NAPI_RETURN_UINT32(account_id);
-}
-
-NAPI_METHOD(dcn_accounts_add_closed_account) {
- NAPI_ARGV(1);
- NAPI_DCN_ACCOUNTS();
-
- int account_id = dc_accounts_add_closed_account(dcn_accounts->dc_accounts);
-
- NAPI_RETURN_UINT32(account_id);
-}
-
-
-NAPI_METHOD(dcn_accounts_migrate_account) {
- NAPI_ARGV(2);
- NAPI_DCN_ACCOUNTS();
- NAPI_ARGV_UTF8_MALLOC(dbfile, 1);
-
- uint32_t account_id = dc_accounts_migrate_account(dcn_accounts->dc_accounts, dbfile);
-
- NAPI_RETURN_UINT32(account_id);
-}
-
-NAPI_METHOD(dcn_accounts_remove_account) {
- NAPI_ARGV(2);
- NAPI_DCN_ACCOUNTS();
- NAPI_ARGV_UINT32(account_id, 1);
-
- int result = dc_accounts_remove_account(dcn_accounts->dc_accounts, account_id);
-
- NAPI_RETURN_INT32(result);
-}
-
-NAPI_METHOD(dcn_accounts_get_all) {
- NAPI_ARGV(1);
- NAPI_DCN_ACCOUNTS();
-
- dc_array_t* accounts = dc_accounts_get_all(dcn_accounts->dc_accounts);
- napi_value js_array = dc_array_to_js_array(env, accounts);
- dc_array_unref(accounts);
-
- return js_array;
-}
-
-NAPI_METHOD(dcn_accounts_get_account) {
- NAPI_ARGV(2);
- NAPI_DCN_ACCOUNTS();
- NAPI_ARGV_UINT32(account_id, 1);
-
- dc_context_t* account_context = dc_accounts_get_account(dcn_accounts->dc_accounts, account_id);
-
-
- napi_value result;
- if (account_context == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- dcn_context_t* dcn_context = calloc(1, sizeof(dcn_context_t));
- dcn_context->dc_context = account_context;
-
- NAPI_STATUS_THROWS(napi_create_external(env, dcn_context,
- NULL, NULL, &result));
- }
-
- return result;
-}
-
-NAPI_METHOD(dcn_accounts_get_selected_account) {
- NAPI_ARGV(1);
- NAPI_DCN_ACCOUNTS();
-
- dc_context_t* account_context = dc_accounts_get_selected_account(dcn_accounts->dc_accounts);
-
-
- napi_value result;
- if (account_context == NULL) {
- NAPI_STATUS_THROWS(napi_get_null(env, &result));
- } else {
- dcn_context_t* dcn_context = calloc(1, sizeof(dcn_context_t));
- dcn_context->dc_context = account_context;
-
- NAPI_STATUS_THROWS(napi_create_external(env, dcn_context,
- NULL, NULL, &result));
- }
-
- return result;
-}
-
-NAPI_METHOD(dcn_accounts_select_account) {
- NAPI_ARGV(2);
- NAPI_DCN_ACCOUNTS();
- NAPI_ARGV_UINT32(account_id, 1);
-
- int result = dc_accounts_select_account(dcn_accounts->dc_accounts, account_id);
- NAPI_RETURN_UINT32(result);
-}
-
-NAPI_METHOD(dcn_accounts_start_io) {
- NAPI_ARGV(1);
- NAPI_DCN_ACCOUNTS();
- TRACE("calling...");
- dc_accounts_start_io(dcn_accounts->dc_accounts);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_accounts_stop_io) {
- NAPI_ARGV(1);
- NAPI_DCN_ACCOUNTS();
-
- dc_accounts_stop_io(dcn_accounts->dc_accounts);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-
-NAPI_METHOD(dcn_accounts_maybe_network) {
- NAPI_ARGV(1);
- NAPI_DCN_ACCOUNTS();
-
- dc_accounts_maybe_network(dcn_accounts->dc_accounts);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-
-NAPI_METHOD(dcn_accounts_maybe_network_lost) {
- NAPI_ARGV(1);
- NAPI_DCN_ACCOUNTS();
-
- dc_accounts_maybe_network_lost(dcn_accounts->dc_accounts);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-static void accounts_event_handler_thread_func(void* arg)
-{
- dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg;
-
- TRACE("event_handler_thread_func starting");
-
- dc_event_emitter_t * dc_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
- dc_event_t* event;
- while (true) {
- if (dc_event_emitter == NULL) {
- TRACE("event emitter is null, bailing");
- break;
- }
- event = dc_get_next_event(dc_event_emitter);
- if (event == NULL) {
- TRACE("no more events");
- break;
- }
-
- if (!dcn_accounts->threadsafe_event_handler) {
- TRACE("threadsafe_event_handler not set, bailing");
- break;
- }
-
- // Don't process events if we're being garbage collected!
- if (dcn_accounts->gc == 1) {
- TRACE("dc_accounts has been destroyed, bailing");
- break;
- }
-
-
- napi_status status = napi_call_threadsafe_function(dcn_accounts->threadsafe_event_handler, event, napi_tsfn_blocking);
-
- if (status == napi_closing) {
- TRACE("JS function got released, bailing");
- break;
- }
- }
-
- dc_event_emitter_unref(dc_event_emitter);
-
- TRACE("event_handler_thread_func ended");
-
- napi_release_threadsafe_function(dcn_accounts->threadsafe_event_handler, napi_tsfn_release);
-}
-
-static void call_accounts_js_event_handler(napi_env env, napi_value js_callback, void* _context, void* data)
-{
- dc_event_t* dc_event = (dc_event_t*)data;
-
- napi_value global;
- napi_status status = napi_get_global(env, &global);
-
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to get global");
- }
-
-
-#define CALL_JS_CALLBACK_ACCOUNTS_ARGC 4
-
- const int argc = CALL_JS_CALLBACK_ACCOUNTS_ARGC;
- napi_value argv[CALL_JS_CALLBACK_ACCOUNTS_ARGC];
-
- const int event_id = dc_event_get_id(dc_event);
-
- status = napi_create_uint32(env, event_id, &argv[0]);
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to create argv[0] for event_handler arguments");
- }
-
- const int account_id = dc_event_get_account_id(dc_event);
- status = napi_create_uint32(env, account_id, &argv[1]);
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to create argv[1] for event_handler arguments");
- }
-
-
- status = napi_create_int32(env, dc_event_get_data1_int(dc_event), &argv[2]);
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to create argv[2] for event_handler arguments");
- }
-
- if DC_EVENT_DATA2_IS_STRING(event_id) {
- char* data2_string = dc_event_get_data2_str(dc_event);
- // Quick fix for https://github.com/deltachat/deltachat-core-rust/issues/1949
- if (data2_string != 0) {
- status = napi_create_string_utf8(env, data2_string, NAPI_AUTO_LENGTH, &argv[3]);
- } else {
- status = napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &argv[3]);
- }
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to create argv[3] for event_handler arguments");
- }
- dc_str_unref(data2_string);
- } else {
- status = napi_create_int32(env, dc_event_get_data2_int(dc_event), &argv[3]);
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to create argv[3] for event_handler arguments");
- }
- }
-
- dc_event_unref(dc_event);
- dc_event = NULL;
-
- TRACE("calling back into js");
-
- napi_value result;
- status = napi_call_function(
- env,
- global,
- js_callback,
- argc,
- argv,
- &result);
-
- if (status != napi_ok) {
- TRACE("Unable to call event_handler callback2");
- const napi_extended_error_info* error_result;
- NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result));
- }
-}
-
-NAPI_METHOD(dcn_accounts_start_event_handler) {
- NAPI_ARGV(2);
- NAPI_DCN_ACCOUNTS();
- napi_value callback = argv[1];
-
- TRACE("calling..");
- napi_value async_resource_name;
- NAPI_STATUS_THROWS(napi_create_string_utf8(env, "dc_accounts_event_callback", NAPI_AUTO_LENGTH, &async_resource_name));
-
- TRACE("creating threadsafe function..");
-
- NAPI_STATUS_THROWS(napi_create_threadsafe_function(
- env,
- callback,
- 0,
- async_resource_name,
- 1000, // max_queue_size
- 1,
- NULL,
- NULL,
- dcn_accounts,
- call_accounts_js_event_handler,
- &dcn_accounts->threadsafe_event_handler));
- TRACE("done");
-
- dcn_accounts->gc = 0;
- TRACE("creating uv thread..");
- uv_thread_create(&dcn_accounts->event_handler_thread, accounts_event_handler_thread_func, dcn_accounts);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-// JSON RPC
-
-static void accounts_jsonrpc_thread_func(void* arg)
-{
- dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg;
- TRACE("accounts_jsonrpc_thread_func starting");
- char* response;
- while (true) {
- response = dc_jsonrpc_next_response(dcn_accounts->jsonrpc_instance);
- if (response == NULL) {
- // done or broken
- break;
- }
-
- if (!dcn_accounts->threadsafe_jsonrpc_handler) {
- TRACE("threadsafe_jsonrpc_handler not set, bailing");
- break;
- }
- // Don't process events if we're being garbage collected!
- if (dcn_accounts->gc == 1) {
- TRACE("dc_accounts has been destroyed, bailing");
- break;
- }
-
- napi_status status = napi_call_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, response, napi_tsfn_blocking);
-
- if (status == napi_closing) {
- TRACE("JS function got released, bailing");
- break;
- }
- }
- TRACE("accounts_jsonrpc_thread_func ended");
- napi_release_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, napi_tsfn_release);
-}
-
-static void call_accounts_js_jsonrpc_handler(napi_env env, napi_value js_callback, void* _context, void* data)
-{
- char* response = (char*)data;
- napi_value global;
- napi_status status = napi_get_global(env, &global);
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to get global");
- }
-
- napi_value argv[1];
- if (response != 0) {
- status = napi_create_string_utf8(env, response, NAPI_AUTO_LENGTH, &argv[0]);
- } else {
- status = napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &argv[0]);
- }
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to create argv for js jsonrpc_handler arguments");
- }
- dc_str_unref(response);
-
- TRACE("calling back into js");
- napi_value result;
- status = napi_call_function(
- env,
- global,
- js_callback,
- 1,
- argv,
- &result);
- if (status != napi_ok) {
- TRACE("Unable to call jsonrpc_handler callback2");
- const napi_extended_error_info* error_result;
- NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result));
- }
-}
-
-NAPI_METHOD(dcn_accounts_start_jsonrpc) {
- NAPI_ARGV(2);
- NAPI_DCN_ACCOUNTS();
- napi_value callback = argv[1];
-
- TRACE("calling..");
- napi_value async_resource_name;
- NAPI_STATUS_THROWS(napi_create_string_utf8(env, "dc_accounts_jsonrpc_callback", NAPI_AUTO_LENGTH, &async_resource_name));
-
- TRACE("creating threadsafe function..");
-
- NAPI_STATUS_THROWS(napi_create_threadsafe_function(
- env,
- callback,
- 0,
- async_resource_name,
- 1000, // max_queue_size
- 1,
- NULL,
- NULL,
- NULL,
- call_accounts_js_jsonrpc_handler,
- &dcn_accounts->threadsafe_jsonrpc_handler));
- TRACE("done");
-
- dcn_accounts->gc = 0;
- dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts);
-
- TRACE("creating uv thread..");
- uv_thread_create(&dcn_accounts->jsonrpc_thread, accounts_jsonrpc_thread_func, dcn_accounts);
-
- NAPI_RETURN_UNDEFINED();
-}
-
-NAPI_METHOD(dcn_json_rpc_request) {
- NAPI_ARGV(2);
- NAPI_DCN_ACCOUNTS();
- if (!dcn_accounts->jsonrpc_instance) {
- const char* msg = "dcn_accounts->jsonrpc_instance is null, have you called dcn_accounts_start_jsonrpc()?";
- NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg));
- }
- NAPI_ARGV_UTF8_MALLOC(request, 1);
- dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, request);
- free(request);
- NAPI_RETURN_UNDEFINED();
-}
-
-
-NAPI_INIT() {
- /**
- * Accounts
- */
-
- NAPI_EXPORT_FUNCTION(dcn_accounts_new);
- NAPI_EXPORT_FUNCTION(dcn_accounts_unref);
- NAPI_EXPORT_FUNCTION(dcn_accounts_add_account);
- NAPI_EXPORT_FUNCTION(dcn_accounts_add_closed_account);
- NAPI_EXPORT_FUNCTION(dcn_accounts_migrate_account);
- NAPI_EXPORT_FUNCTION(dcn_accounts_remove_account);
- NAPI_EXPORT_FUNCTION(dcn_accounts_get_all);
- NAPI_EXPORT_FUNCTION(dcn_accounts_get_account);
- NAPI_EXPORT_FUNCTION(dcn_accounts_get_selected_account);
- NAPI_EXPORT_FUNCTION(dcn_accounts_select_account);
- NAPI_EXPORT_FUNCTION(dcn_accounts_start_io);
- NAPI_EXPORT_FUNCTION(dcn_accounts_stop_io);
- NAPI_EXPORT_FUNCTION(dcn_accounts_maybe_network);
- NAPI_EXPORT_FUNCTION(dcn_accounts_maybe_network_lost);
-
- NAPI_EXPORT_FUNCTION(dcn_accounts_start_event_handler);
-
-
- /**
- * Main context
- */
-
- NAPI_EXPORT_FUNCTION(dcn_context_new);
- NAPI_EXPORT_FUNCTION(dcn_context_new_closed);
- NAPI_EXPORT_FUNCTION(dcn_context_open);
- NAPI_EXPORT_FUNCTION(dcn_context_is_open);
- NAPI_EXPORT_FUNCTION(dcn_context_unref);
- NAPI_EXPORT_FUNCTION(dcn_start_event_handler);
-
- /**
- * Static functions
- */
-
- NAPI_EXPORT_FUNCTION(dcn_maybe_valid_addr);
-
- /**
- * dcn_context_t
- */
-
- NAPI_EXPORT_FUNCTION(dcn_add_address_book);
- NAPI_EXPORT_FUNCTION(dcn_add_contact_to_chat);
- NAPI_EXPORT_FUNCTION(dcn_add_device_msg);
- NAPI_EXPORT_FUNCTION(dcn_block_contact);
- NAPI_EXPORT_FUNCTION(dcn_check_qr);
- NAPI_EXPORT_FUNCTION(dcn_configure);
- NAPI_EXPORT_FUNCTION(dcn_continue_key_transfer);
- NAPI_EXPORT_FUNCTION(dcn_create_chat_by_contact_id);
- NAPI_EXPORT_FUNCTION(dcn_create_broadcast_list);
- NAPI_EXPORT_FUNCTION(dcn_create_contact);
- NAPI_EXPORT_FUNCTION(dcn_create_group_chat);
- NAPI_EXPORT_FUNCTION(dcn_delete_chat);
- NAPI_EXPORT_FUNCTION(dcn_delete_contact);
- NAPI_EXPORT_FUNCTION(dcn_delete_msgs);
- NAPI_EXPORT_FUNCTION(dcn_forward_msgs);
- NAPI_EXPORT_FUNCTION(dcn_get_blobdir);
- NAPI_EXPORT_FUNCTION(dcn_get_blocked_cnt);
- NAPI_EXPORT_FUNCTION(dcn_get_blocked_contacts);
- NAPI_EXPORT_FUNCTION(dcn_get_chat);
- NAPI_EXPORT_FUNCTION(dcn_get_chat_contacts);
- NAPI_EXPORT_FUNCTION(dcn_get_chat_encrinfo);
- NAPI_EXPORT_FUNCTION(dcn_get_chat_id_by_contact_id);
- NAPI_EXPORT_FUNCTION(dcn_get_chat_media);
- NAPI_EXPORT_FUNCTION(dcn_get_mime_headers);
- NAPI_EXPORT_FUNCTION(dcn_get_chat_msgs);
- NAPI_EXPORT_FUNCTION(dcn_get_chatlist);
- NAPI_EXPORT_FUNCTION(dcn_get_config);
- NAPI_EXPORT_FUNCTION(dcn_get_contact);
- NAPI_EXPORT_FUNCTION(dcn_get_contact_encrinfo);
- NAPI_EXPORT_FUNCTION(dcn_get_contacts);
- NAPI_EXPORT_FUNCTION(dcn_get_connectivity);
- NAPI_EXPORT_FUNCTION(dcn_get_connectivity_html);
- NAPI_EXPORT_FUNCTION(dcn_was_device_msg_ever_added);
- NAPI_EXPORT_FUNCTION(dcn_get_draft);
- NAPI_EXPORT_FUNCTION(dcn_get_fresh_msg_cnt);
- NAPI_EXPORT_FUNCTION(dcn_get_fresh_msgs);
- NAPI_EXPORT_FUNCTION(dcn_get_info);
- NAPI_EXPORT_FUNCTION(dcn_get_msg);
- NAPI_EXPORT_FUNCTION(dcn_get_msg_cnt);
- NAPI_EXPORT_FUNCTION(dcn_get_msg_info);
- NAPI_EXPORT_FUNCTION(dcn_get_msg_html);
- NAPI_EXPORT_FUNCTION(dcn_set_chat_visibility);
- NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr);
- NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr_svg);
- NAPI_EXPORT_FUNCTION(dcn_imex);
- NAPI_EXPORT_FUNCTION(dcn_imex_has_backup);
- NAPI_EXPORT_FUNCTION(dcn_initiate_key_transfer);
- NAPI_EXPORT_FUNCTION(dcn_is_configured);
- NAPI_EXPORT_FUNCTION(dcn_is_contact_in_chat);
-
-
-
- NAPI_EXPORT_FUNCTION(dcn_accept_chat);
- NAPI_EXPORT_FUNCTION(dcn_block_chat);
- NAPI_EXPORT_FUNCTION(dcn_join_securejoin);
- NAPI_EXPORT_FUNCTION(dcn_lookup_contact_id_by_addr);
- NAPI_EXPORT_FUNCTION(dcn_marknoticed_chat);
- NAPI_EXPORT_FUNCTION(dcn_download_full_msg);
- NAPI_EXPORT_FUNCTION(dcn_markseen_msgs);
- NAPI_EXPORT_FUNCTION(dcn_maybe_network);
- NAPI_EXPORT_FUNCTION(dcn_msg_new);
- NAPI_EXPORT_FUNCTION(dcn_remove_contact_from_chat);
- NAPI_EXPORT_FUNCTION(dcn_search_msgs);
- NAPI_EXPORT_FUNCTION(dcn_send_msg);
- NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation);
- NAPI_EXPORT_FUNCTION(dcn_set_chat_name);
- NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer);
- NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer);
- NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image);
- NAPI_EXPORT_FUNCTION(dcn_set_chat_mute_duration);
- NAPI_EXPORT_FUNCTION(dcn_set_config);
- NAPI_EXPORT_FUNCTION(dcn_set_config_null);
- NAPI_EXPORT_FUNCTION(dcn_set_config_from_qr);
- NAPI_EXPORT_FUNCTION(dcn_estimate_deletion_cnt);
- NAPI_EXPORT_FUNCTION(dcn_set_draft);
- NAPI_EXPORT_FUNCTION(dcn_set_stock_translation);
- NAPI_EXPORT_FUNCTION(dcn_start_io);
- NAPI_EXPORT_FUNCTION(dcn_stop_io);
- NAPI_EXPORT_FUNCTION(dcn_stop_ongoing_process);
-
- /**
- * dc_chat_t
- */
-
- NAPI_EXPORT_FUNCTION(dcn_chat_get_color);
- NAPI_EXPORT_FUNCTION(dcn_chat_get_visibility);
- NAPI_EXPORT_FUNCTION(dcn_chat_get_id);
- NAPI_EXPORT_FUNCTION(dcn_chat_get_name);
- NAPI_EXPORT_FUNCTION(dcn_chat_get_mailinglist_addr);
- NAPI_EXPORT_FUNCTION(dcn_chat_get_profile_image);
- NAPI_EXPORT_FUNCTION(dcn_chat_get_type);
- NAPI_EXPORT_FUNCTION(dcn_chat_is_self_talk);
- NAPI_EXPORT_FUNCTION(dcn_chat_is_unpromoted);
- NAPI_EXPORT_FUNCTION(dcn_chat_can_send);
- NAPI_EXPORT_FUNCTION(dcn_chat_is_protected);
- NAPI_EXPORT_FUNCTION(dcn_chat_is_device_talk);
- NAPI_EXPORT_FUNCTION(dcn_chat_is_muted);
- NAPI_EXPORT_FUNCTION(dcn_chat_is_contact_request);
-
- /**
- * dc_chatlist_t
- */
-
- NAPI_EXPORT_FUNCTION(dcn_chatlist_get_chat_id);
- NAPI_EXPORT_FUNCTION(dcn_chatlist_get_cnt);
- NAPI_EXPORT_FUNCTION(dcn_chatlist_get_msg_id);
- NAPI_EXPORT_FUNCTION(dcn_chatlist_get_summary);
- NAPI_EXPORT_FUNCTION(dcn_chatlist_get_summary2);
-
- /**
- * dc_contact_t
- */
-
- NAPI_EXPORT_FUNCTION(dcn_contact_get_addr);
- NAPI_EXPORT_FUNCTION(dcn_contact_get_auth_name);
- NAPI_EXPORT_FUNCTION(dcn_contact_get_color);
- NAPI_EXPORT_FUNCTION(dcn_contact_get_display_name);
- NAPI_EXPORT_FUNCTION(dcn_contact_get_id);
- NAPI_EXPORT_FUNCTION(dcn_contact_get_name);
- NAPI_EXPORT_FUNCTION(dcn_contact_get_name_n_addr);
- NAPI_EXPORT_FUNCTION(dcn_contact_get_profile_image);
- NAPI_EXPORT_FUNCTION(dcn_contact_get_status);
- NAPI_EXPORT_FUNCTION(dcn_contact_get_last_seen);
- NAPI_EXPORT_FUNCTION(dcn_contact_is_blocked);
- NAPI_EXPORT_FUNCTION(dcn_contact_is_verified);
-
- /**
- * dc_lot_t
- */
-
- NAPI_EXPORT_FUNCTION(dcn_lot_get_id);
- NAPI_EXPORT_FUNCTION(dcn_lot_get_state);
- NAPI_EXPORT_FUNCTION(dcn_lot_get_text1);
- NAPI_EXPORT_FUNCTION(dcn_lot_get_text1_meaning);
- NAPI_EXPORT_FUNCTION(dcn_lot_get_text2);
- NAPI_EXPORT_FUNCTION(dcn_lot_get_timestamp);
-
- /**
- * dc_msg_t
- */
-
- NAPI_EXPORT_FUNCTION(dcn_msg_get_parent);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_download_state);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_chat_id);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_duration);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_file);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_filebytes);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_filemime);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_filename);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_from_id);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_height);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_id);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_override_sender_name);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_quoted_text);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_quoted_msg);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_received_timestamp);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_setupcodebegin);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_showpadlock);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_sort_timestamp);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_state);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_summary);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_summarytext);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_subject);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_text);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_timestamp);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_viewtype);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_videochat_type);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_videochat_url);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_width);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_webxdc_info);
- NAPI_EXPORT_FUNCTION(dcn_msg_has_deviating_timestamp);
- NAPI_EXPORT_FUNCTION(dcn_msg_has_location);
- NAPI_EXPORT_FUNCTION(dcn_msg_has_html);
- NAPI_EXPORT_FUNCTION(dcn_msg_is_forwarded);
- NAPI_EXPORT_FUNCTION(dcn_msg_is_info);
- NAPI_EXPORT_FUNCTION(dcn_msg_is_sent);
- NAPI_EXPORT_FUNCTION(dcn_msg_is_setupmessage);
- NAPI_EXPORT_FUNCTION(dcn_msg_latefiling_mediasize);
- NAPI_EXPORT_FUNCTION(dcn_msg_force_plaintext);
- NAPI_EXPORT_FUNCTION(dcn_msg_set_dimension);
- NAPI_EXPORT_FUNCTION(dcn_msg_set_duration);
- NAPI_EXPORT_FUNCTION(dcn_msg_set_override_sender_name);
- NAPI_EXPORT_FUNCTION(dcn_msg_set_file);
- NAPI_EXPORT_FUNCTION(dcn_msg_set_html);
- NAPI_EXPORT_FUNCTION(dcn_msg_set_quote);
- NAPI_EXPORT_FUNCTION(dcn_msg_set_text);
- NAPI_EXPORT_FUNCTION(dcn_msg_set_location);
-
- /**
- * dc_location
- */
- NAPI_EXPORT_FUNCTION(dcn_set_location);
- NAPI_EXPORT_FUNCTION(dcn_get_locations);
-
- /**
- * dc_provider
- */
- NAPI_EXPORT_FUNCTION(dcn_provider_new_from_email);
- NAPI_EXPORT_FUNCTION(dcn_provider_get_overview_page);
- NAPI_EXPORT_FUNCTION(dcn_provider_get_before_login_hint);
- NAPI_EXPORT_FUNCTION(dcn_provider_get_status);
-
- /**
- * dc_array
- */
- NAPI_EXPORT_FUNCTION(dcn_array_get_cnt);
- NAPI_EXPORT_FUNCTION(dcn_array_get_id);
- NAPI_EXPORT_FUNCTION(dcn_array_get_accuracy);
- NAPI_EXPORT_FUNCTION(dcn_array_get_latitude);
- NAPI_EXPORT_FUNCTION(dcn_array_get_longitude);
- NAPI_EXPORT_FUNCTION(dcn_array_get_timestamp);
- NAPI_EXPORT_FUNCTION(dcn_array_get_msg_id);
- NAPI_EXPORT_FUNCTION(dcn_array_is_independent);
- NAPI_EXPORT_FUNCTION(dcn_array_get_contact_id);
- NAPI_EXPORT_FUNCTION(dcn_array_get_chat_id);
- NAPI_EXPORT_FUNCTION(dcn_array_get_marker);
-
- /** webxdc **/
-
- NAPI_EXPORT_FUNCTION(dcn_send_webxdc_status_update);
- NAPI_EXPORT_FUNCTION(dcn_get_webxdc_status_updates);
- NAPI_EXPORT_FUNCTION(dcn_msg_get_webxdc_blob);
-
-
- /** jsonrpc **/
- NAPI_EXPORT_FUNCTION(dcn_accounts_start_jsonrpc);
- NAPI_EXPORT_FUNCTION(dcn_json_rpc_request);
-}
diff --git a/node/src/napi-macros-extensions.h b/node/src/napi-macros-extensions.h
deleted file mode 100644
index 968bc02ca9..0000000000
--- a/node/src/napi-macros-extensions.h
+++ /dev/null
@@ -1,144 +0,0 @@
-#include
-
-#undef NAPI_STATUS_THROWS
-
-#define NAPI_STATUS_THROWS(call) \
- if ((call) != napi_ok) { \
- napi_throw_error(env, NULL, #call " failed!"); \
- }
-
-#define NAPI_DCN_CONTEXT() \
- dcn_context_t* dcn_context; \
- NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_context)); \
- if (!dcn_context) { \
- const char* msg = "Provided dnc_context is null"; \
- NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
- } \
- if (!dcn_context->dc_context) { \
- const char* msg = "Provided dc_context is null, did you close the context or not open it?"; \
- NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
- }
-
-#define NAPI_DCN_ACCOUNTS() \
- dcn_accounts_t* dcn_accounts; \
- NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_accounts)); \
- if (!dcn_accounts) { \
- const char* msg = "Provided dcn_acounts is null"; \
- NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
- } \
- if (!dcn_accounts->dc_accounts) { \
- const char* msg = "Provided dc_accounts is null, did you unref the accounts object?"; \
- NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
- }
-
-
-#define NAPI_DC_CHAT() \
- dc_chat_t* dc_chat; \
- NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_chat));
-
-#define NAPI_DC_CHATLIST() \
- dc_chatlist_t* dc_chatlist; \
- NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_chatlist));
-
-#define NAPI_DC_CONTACT() \
- dc_contact_t* dc_contact; \
- NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_contact));
-
-#define NAPI_DC_LOT() \
- dc_lot_t* dc_lot; \
- NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_lot));
-
-#define NAPI_DC_MSG() \
- dc_msg_t* dc_msg; \
- NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_msg));
-
-#define NAPI_ARGV_DC_MSG(name, position) \
- dc_msg_t* name; \
- NAPI_STATUS_THROWS(napi_get_value_external(env, argv[position], (void**)&name));
-
-#define NAPI_DC_PROVIDER() \
- dc_provider_t* dc_provider; \
- NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_provider));
-
-#define NAPI_DC_ARRAY() \
- dc_array_t* dc_array; \
- NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_array));
-
-#define NAPI_RETURN_UNDEFINED() \
- return 0;
-
-#define NAPI_RETURN_UINT64(name) \
- napi_value return_int64; \
- NAPI_STATUS_THROWS(napi_create_bigint_int64(env, name, &return_int64)); \
- return return_int64;
-
-#define NAPI_RETURN_INT64(name) \
- napi_value return_int64; \
- NAPI_STATUS_THROWS(napi_create_int64(env, name, &return_int64)); \
- return return_int64;
-
-
-#define NAPI_RETURN_AND_UNREF_STRING(name) \
- napi_value return_value; \
- if (name == NULL) { \
- NAPI_STATUS_THROWS(napi_get_null(env, &return_value)); \
- return return_value; \
- } \
- NAPI_STATUS_THROWS(napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &return_value)); \
- dc_str_unref(name); \
- return return_value;
-
-#define NAPI_ASYNC_CARRIER_BEGIN(name) \
- typedef struct name##_carrier_t { \
- napi_ref callback_ref; \
- napi_async_work async_work; \
- dcn_context_t* dcn_context;
-
-#define NAPI_ASYNC_CARRIER_END(name) \
- } name##_carrier_t;
-
-#define NAPI_ASYNC_EXECUTE(name) \
- static void name##_execute(napi_env env, void* data)
-
-#define NAPI_ASYNC_GET_CARRIER(name) \
- name##_carrier_t* carrier = (name##_carrier_t*)data;
-
-#define NAPI_ASYNC_COMPLETE(name) \
- static void name##_complete(napi_env env, napi_status status, void* data)
-
-#define NAPI_ASYNC_CALL_AND_DELETE_CB() \
- napi_value global; \
- NAPI_STATUS_THROWS(napi_get_global(env, &global)); \
- napi_value callback; \
- NAPI_STATUS_THROWS(napi_get_reference_value(env, carrier->callback_ref, &callback)); \
- NAPI_STATUS_THROWS(napi_call_function(env, global, callback, argc, argv, NULL)); \
- NAPI_STATUS_THROWS(napi_delete_reference(env, carrier->callback_ref)); \
- NAPI_STATUS_THROWS(napi_delete_async_work(env, carrier->async_work));
-
-#define NAPI_ASYNC_NEW_CARRIER(name) \
- name##_carrier_t* carrier = calloc(1, sizeof(name##_carrier_t)); \
- carrier->dcn_context = dcn_context;
-
-#define NAPI_ASYNC_QUEUE_WORK(name, cb) \
- napi_value callback = cb; \
- napi_value async_resource_name; \
- NAPI_STATUS_THROWS(napi_create_reference(env, callback, 1, &carrier->callback_ref)); \
- NAPI_STATUS_THROWS(napi_create_string_utf8(env, #name "_callback", \
- NAPI_AUTO_LENGTH, \
- &async_resource_name)); \
- NAPI_STATUS_THROWS(napi_create_async_work(env, callback, async_resource_name, \
- name##_execute, name##_complete, \
- carrier, &carrier->async_work)); \
- NAPI_STATUS_THROWS(napi_queue_async_work(env, carrier->async_work));
-
-/*** this could/should be moved to napi-macros ***/
-
-#define NAPI_DOUBLE(name, val) \
- double name; \
- if (napi_get_value_double(env, val, &name) != napi_ok) { \
- napi_throw_error(env, "EINVAL", "Expected double"); \
- return NULL; \
- }
-
-#define NAPI_ARGV_DOUBLE(name, i) \
- NAPI_DOUBLE(name, argv[i])
diff --git a/node/test/fixtures/avatar.png b/node/test/fixtures/avatar.png
deleted file mode 100644
index 76e69ce492..0000000000
Binary files a/node/test/fixtures/avatar.png and /dev/null differ
diff --git a/node/test/fixtures/image.jpeg b/node/test/fixtures/image.jpeg
deleted file mode 100644
index 7e64c99307..0000000000
Binary files a/node/test/fixtures/image.jpeg and /dev/null differ
diff --git a/node/test/fixtures/logo.png b/node/test/fixtures/logo.png
deleted file mode 100644
index 9408ad90b4..0000000000
Binary files a/node/test/fixtures/logo.png and /dev/null differ
diff --git a/node/test/test.mjs b/node/test/test.mjs
deleted file mode 100644
index 11e4703adb..0000000000
--- a/node/test/test.mjs
+++ /dev/null
@@ -1,943 +0,0 @@
-// @ts-check
-import { DeltaChat } from '../dist/index.js'
-
-import { deepStrictEqual, strictEqual } from 'assert'
-import chai, { expect } from 'chai'
-import chaiAsPromised from 'chai-as-promised'
-import { EventId2EventName, C } from '../dist/constants.js'
-import { join } from 'path'
-import { statSync } from 'fs'
-import { Context } from '../dist/context.js'
-import { fileURLToPath } from 'url';
-
-const __dirname = fileURLToPath(new URL('.', import.meta.url));
-
-chai.use(chaiAsPromised)
-chai.config.truncateThreshold = 0 // Do not truncate assertion errors.
-
-function createTempUser(chatmailDomain) {
- const charset = "2345789acdefghjkmnpqrstuvwxyz";
- let user = "ci-";
- for (let i = 0; i < 6; i++) {
- user += charset[Math.floor(Math.random() * charset.length)];
- }
- const email = user + "@" + chatmailDomain;
- return { email: email, password: user + "$" + user };
-}
-
-describe('static tests', function () {
- this.timeout(60 * 5 * 1000) // increase timeout to 5 min
-
- it('reverse lookup of events', function () {
- const eventKeys = Object.keys(EventId2EventName).map((k) => Number(k))
- const eventValues = Object.values(EventId2EventName)
- const reverse = eventValues.map((v) => C[v])
- expect(reverse).to.be.deep.equal(eventKeys)
- })
-
- it('event constants are consistent', function () {
- const eventKeys = Object.keys(C)
- .filter((k) => k.startsWith('DC_EVENT_'))
- .sort()
- const eventValues = Object.values(EventId2EventName).sort()
- expect(eventKeys).to.be.deep.equal(eventValues)
- })
-
- it('static method maybeValidAddr()', function () {
- expect(DeltaChat.maybeValidAddr(null)).to.equal(false)
- expect(DeltaChat.maybeValidAddr('')).to.equal(false)
- expect(DeltaChat.maybeValidAddr('uuu')).to.equal(false)
- expect(DeltaChat.maybeValidAddr('dd.tt')).to.equal(false)
- expect(DeltaChat.maybeValidAddr('tt.dd@yggmail')).to.equal(true)
- expect(DeltaChat.maybeValidAddr('u@d')).to.equal(true)
- //expect(DeltaChat.maybeValidAddr('u@d.')).to.equal(false)
- //expect(DeltaChat.maybeValidAddr('u@d.t')).to.equal(false)
- //expect(DeltaChat.maybeValidAddr('u@.tt')).to.equal(false)
- expect(DeltaChat.maybeValidAddr('@d.tt')).to.equal(false)
- expect(DeltaChat.maybeValidAddr('user@domain.tld')).to.equal(true)
- expect(DeltaChat.maybeValidAddr('u@d.tt')).to.equal(true)
- })
-
- it('static getSystemInfo()', function () {
- const info = Context.getSystemInfo()
- expect(info).to.contain.keys([
- 'arch',
- 'deltachat_core_version',
- 'sqlite_version',
- ])
- })
-
- it('static context.getProviderFromEmail("example@example.com")', function () {
- const provider = DeltaChat.getProviderFromEmail('example@example.com')
-
- expect(provider).to.deep.equal({
- before_login_hint: "Hush this provider doesn't exist!",
- overview_page: 'https://providers.delta.chat/example-com',
- status: 3,
- })
- })
-})
-
-describe('JSON RPC', function () {
- it('smoketest', async function () {
- const { dc } = DeltaChat.newTemporary()
- let promise_resolve
- const promise = new Promise((res, _rej) => {
- promise_resolve = (response) => {
- // ignore events
- const answer = JSON.parse(response)
- if (answer['method'] !== 'event') res(answer)
- }
- })
- dc.startJsonRpcHandler(promise_resolve)
- dc.jsonRpcRequest(
- JSON.stringify({
- jsonrpc: '2.0',
- method: 'get_all_account_ids',
- params: [],
- id: 2,
- })
- )
- deepStrictEqual(
- {
- jsonrpc: '2.0',
- id: 2,
- result: [1],
- },
- await promise
- )
- dc.close()
- })
-
- it('basic test', async function () {
- const { dc } = DeltaChat.newTemporary()
-
- const promises = {}
- dc.startJsonRpcHandler((msg) => {
- const response = JSON.parse(msg)
- if (response.hasOwnProperty('id')) promises[response.id](response)
- delete promises[response.id]
- })
- const call = (request) => {
- dc.jsonRpcRequest(JSON.stringify(request))
- return new Promise((res, _rej) => {
- promises[request.id] = res
- })
- }
-
- deepStrictEqual(
- {
- jsonrpc: '2.0',
- id: 2,
- result: [1],
- },
- await call({
- jsonrpc: '2.0',
- method: 'get_all_account_ids',
- params: [],
- id: 2,
- })
- )
-
- deepStrictEqual(
- {
- jsonrpc: '2.0',
- id: 3,
- result: 2,
- },
- await call({
- jsonrpc: '2.0',
- method: 'add_account',
- params: [],
- id: 3,
- })
- )
-
- deepStrictEqual(
- {
- jsonrpc: '2.0',
- id: 4,
- result: [1, 2],
- },
- await call({
- jsonrpc: '2.0',
- method: 'get_all_account_ids',
- params: [],
- id: 4,
- })
- )
-
- dc.close()
- })
-})
-
-describe('Basic offline Tests', function () {
- it('opens a context', async function () {
- const { dc, context } = DeltaChat.newTemporary()
-
- strictEqual(context.isConfigured(), false)
- dc.close()
- })
-
- it('set config', async function () {
- const { dc, context } = DeltaChat.newTemporary()
-
- context.setConfig('bot', true)
- strictEqual(context.getConfig('bot'), '1')
- context.setConfig('bot', false)
- strictEqual(context.getConfig('bot'), '0')
- context.setConfig('bot', '1')
- strictEqual(context.getConfig('bot'), '1')
- context.setConfig('bot', '0')
- strictEqual(context.getConfig('bot'), '0')
- context.setConfig('bot', 1)
- strictEqual(context.getConfig('bot'), '1')
- context.setConfig('bot', 0)
- strictEqual(context.getConfig('bot'), '0')
-
- context.setConfig('bot', null)
- strictEqual(context.getConfig('bot'), '')
-
- strictEqual(context.getConfig('selfstatus'), '')
- context.setConfig('selfstatus', 'hello')
- strictEqual(context.getConfig('selfstatus'), 'hello')
- context.setConfig('selfstatus', '')
- strictEqual(context.getConfig('selfstatus'), '')
- context.setConfig('selfstatus', null)
- strictEqual(context.getConfig('selfstatus'), '')
-
- dc.close()
- })
-
- it('configure with either missing addr or missing mail_pw throws', async function () {
- const { dc, context } = DeltaChat.newTemporary()
- dc.startEvents()
-
- await expect(
- context.configure({ addr: 'delta1@delta.localhost' })
- ).to.eventually.be.rejectedWith('Missing (IMAP) password.')
- await expect(context.configure({ mailPw: 'delta1' })).to.eventually.be
- .rejected
-
- context.stopOngoingProcess()
- dc.close()
- })
-
- it('context.getInfo()', async function () {
- const { dc, context } = DeltaChat.newTemporary()
-
- const info = await context.getInfo()
- expect(typeof info).to.be.equal('object')
- expect(info).to.contain.keys([
- 'arch',
- 'bcc_self',
- 'blobdir',
- 'bot',
- 'configured_mvbox_folder',
- 'configured_sentbox_folder',
- 'database_dir',
- 'database_encrypted',
- 'database_version',
- 'delete_device_after',
- 'delete_server_after',
- 'deltachat_core_version',
- 'displayname',
- 'download_limit',
- 'e2ee_enabled',
- 'entered_account_settings',
- 'fetch_existing_msgs',
- 'fingerprint',
- 'folders_configured',
- 'is_configured',
- 'journal_mode',
- 'key_gen_type',
- 'last_housekeeping',
- 'last_cant_decrypt_outgoing_msgs',
- 'level',
- 'mdns_enabled',
- 'media_quality',
- 'messages_in_contact_requests',
- 'mvbox_move',
- 'num_cpus',
- 'number_of_chat_messages',
- 'number_of_chats',
- 'number_of_contacts',
- 'only_fetch_mvbox',
- 'private_key_count',
- 'public_key_count',
- 'quota_exceeding',
- 'scan_all_folders_debounce_secs',
- 'selfavatar',
- 'sync_msgs',
- 'sentbox_watch',
- 'show_emails',
- 'proxy_enabled',
- 'sqlite_version',
- 'uptime',
- 'used_account_settings',
- 'webrtc_instance',
- ])
-
- dc.close()
- })
-})
-
-describe('Offline Tests with unconfigured account', function () {
- let [dc, context, accountId, directory] = [null, null, null, null]
-
- this.beforeEach(async function () {
- let tmp = DeltaChat.newTemporary()
- dc = tmp.dc
- context = tmp.context
- accountId = tmp.accountId
- directory = tmp.directory
- dc.startEvents()
- })
-
- this.afterEach(async function () {
- if (context) {
- context.stopOngoingProcess()
- }
- if (dc) {
- try {
- dc.stopIO()
- dc.close()
- } catch (error) {
- console.error(error)
- }
- }
-
- dc = null
- context = null
- accountId = null
- directory = null
- })
-
- it('invalid context.joinSecurejoin', async function () {
- expect(context.joinSecurejoin('test')).to.be.eq(0)
- })
-
- it('Device Chat', async function () {
- const deviceChatMessageText = 'test234'
-
- expect((await context.getChatList(0, '', null)).getCount()).to.equal(
- 0,
- 'no device chat after setup'
- )
-
- await context.addDeviceMessage('test', deviceChatMessageText)
-
- const chatList = await context.getChatList(0, '', null)
- expect(chatList.getCount()).to.equal(
- 1,
- 'device chat after adding device msg'
- )
-
- const deviceChatId = await chatList.getChatId(0)
- const deviceChat = await context.getChat(deviceChatId)
- expect(deviceChat.isDeviceTalk()).to.be.true
- expect(deviceChat.toJson().isDeviceTalk).to.be.true
-
- const deviceChatMessages = await context.getChatMessages(deviceChatId, 0, 0)
- expect(deviceChatMessages.length).to.be.equal(
- 1,
- 'device chat has added message'
- )
-
- const deviceChatMessage = await context.getMessage(deviceChatMessages[0])
- expect(deviceChatMessage.getText()).to.equal(
- deviceChatMessageText,
- 'device chat message has the inserted text'
- )
- })
-
- it('should have e2ee enabled and right blobdir', function () {
- expect(context.getConfig('e2ee_enabled')).to.equal(
- '1',
- 'e2eeEnabled correct'
- )
- expect(
- String(context.getBlobdir()).startsWith(directory),
- 'blobdir should be inside temp directory'
- )
- expect(
- String(context.getBlobdir()).endsWith('db.sqlite-blobs'),
- 'blobdir end with "db.sqlite-blobs"'
- )
- })
-
- it('should create chat from contact and Chat methods', async function () {
- const contactId = context.createContact('aaa', 'aaa@site.org')
-
- strictEqual(context.lookupContactIdByAddr('aaa@site.org'), contactId)
- strictEqual(context.lookupContactIdByAddr('nope@site.net'), 0)
-
- let chatId = context.createChatByContactId(contactId)
- let chat = context.getChat(chatId)
-
- strictEqual(
- chat.getVisibility(),
- C.DC_CHAT_VISIBILITY_NORMAL,
- 'not archived'
- )
- strictEqual(chat.getId(), chatId, 'chat id matches')
- strictEqual(chat.getName(), 'aaa', 'chat name matches')
- strictEqual(chat.getProfileImage(), null, 'no profile image')
- strictEqual(chat.getType(), C.DC_CHAT_TYPE_SINGLE, 'single chat')
- strictEqual(chat.isSelfTalk(), false, 'no self talk')
- // TODO make sure this is really the case!
- strictEqual(chat.isUnpromoted(), false, 'not unpromoted')
- strictEqual(chat.isProtected(), false, 'not verified')
- strictEqual(typeof chat.color, 'string', 'color is a string')
-
- strictEqual(context.getDraft(chatId), null, 'no draft message')
- context.setDraft(chatId, context.messageNew().setText('w00t!'))
- strictEqual(
- context.getDraft(chatId).toJson().text,
- 'w00t!',
- 'draft text correct'
- )
- context.setDraft(chatId, null)
- strictEqual(context.getDraft(chatId), null, 'draft removed')
-
- strictEqual(context.getChatIdByContactId(contactId), chatId)
- expect(context.getChatContacts(chatId)).to.deep.equal([contactId])
-
- context.setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_ARCHIVED)
- strictEqual(
- context.getChat(chatId).getVisibility(),
- C.DC_CHAT_VISIBILITY_ARCHIVED,
- 'chat archived'
- )
- context.setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_NORMAL)
- strictEqual(
- chat.getVisibility(),
- C.DC_CHAT_VISIBILITY_NORMAL,
- 'chat unarchived'
- )
-
- chatId = context.createGroupChat('unverified group', false)
- chat = context.getChat(chatId)
- strictEqual(chat.isProtected(), false, 'is not verified')
- strictEqual(chat.getType(), C.DC_CHAT_TYPE_GROUP, 'group chat')
- expect(context.getChatContacts(chatId)).to.deep.equal([
- C.DC_CONTACT_ID_SELF,
- ])
-
- const draft2 = context.getDraft(chatId)
- expect(draft2 == null, 'unpromoted group has no draft by default')
-
- context.setChatName(chatId, 'NEW NAME')
- strictEqual(context.getChat(chatId).getName(), 'NEW NAME', 'name updated')
-
- chatId = context.createGroupChat('a verified group', true)
- chat = context.getChat(chatId)
- strictEqual(chat.isProtected(), true, 'is verified')
- })
-
- it('test setting profile image', async function () {
- const chatId = context.createGroupChat('testing profile image group', false)
- const image = 'image.jpeg'
- const imagePath = join(__dirname, 'fixtures', image)
- const blobs = context.getBlobdir()
-
- context.setChatProfileImage(chatId, imagePath)
- const blobPath = context.getChat(chatId).getProfileImage()
- expect(blobPath.startsWith(blobs)).to.be.true
- expect(blobPath.includes('image')).to.be.false
- expect(blobPath.endsWith('.jpeg')).to.be.true
-
- context.setChatProfileImage(chatId, null)
- expect(context.getChat(chatId).getProfileImage()).to.be.equal(
- null,
- 'image is null'
- )
- })
-
- it('test setting ephemeral timer', function () {
- const chatId = context.createGroupChat('testing ephemeral timer')
-
- strictEqual(
- context.getChatEphemeralTimer(chatId),
- 0,
- 'ephemeral timer is not set by default'
- )
-
- context.setChatEphemeralTimer(chatId, 60)
- strictEqual(
- context.getChatEphemeralTimer(chatId),
- 60,
- 'ephemeral timer is set to 1 minute'
- )
-
- context.setChatEphemeralTimer(chatId, 0)
- strictEqual(
- context.getChatEphemeralTimer(chatId),
- 0,
- 'ephemeral timer is reset'
- )
- })
-
- it('should create and delete chat', function () {
- const chatId = context.createGroupChat('GROUPCHAT')
- const chat = context.getChat(chatId)
- strictEqual(chat.getId(), chatId, 'correct chatId')
- context.deleteChat(chat.getId())
- strictEqual(context.getChat(chatId), null, 'chat removed')
- })
-
- it('new message and Message methods', function () {
- const text = 'w00t!'
- const msg = context.messageNew().setText(text)
-
- strictEqual(msg.getChatId(), 0, 'chat id 0 before sent')
- strictEqual(msg.getDuration(), 0, 'duration 0 before sent')
- strictEqual(msg.getFile(), '', 'no file set by default')
- strictEqual(msg.getFilebytes(), 0, 'and file bytes is 0')
- strictEqual(msg.getFilemime(), '', 'no filemime by default')
- strictEqual(msg.getFilename(), '', 'no filename set by default')
- strictEqual(msg.getFromId(), 0, 'no contact id set by default')
- strictEqual(msg.getHeight(), 0, 'plain text message have height 0')
- strictEqual(msg.getId(), 0, 'id 0 before sent')
- strictEqual(msg.getSetupcodebegin(), '', 'no setupcode begin')
- strictEqual(msg.getShowpadlock(), false, 'no padlock by default')
-
- const state = msg.getState()
- strictEqual(state.isUndefined(), true, 'no state by default')
- strictEqual(state.isFresh(), false, 'no state by default')
- strictEqual(state.isNoticed(), false, 'no state by default')
- strictEqual(state.isSeen(), false, 'no state by default')
- strictEqual(state.isPending(), false, 'no state by default')
- strictEqual(state.isFailed(), false, 'no state by default')
- strictEqual(state.isDelivered(), false, 'no state by default')
- strictEqual(state.isReceived(), false, 'no state by default')
-
- const summary = msg.getSummary()
- strictEqual(summary.getId(), 0, 'no summary id')
- strictEqual(summary.getState(), 0, 'no summary state')
- strictEqual(summary.getText1(), null, 'no summary text1')
- strictEqual(summary.getText1Meaning(), 0, 'no summary text1 meaning')
- strictEqual(summary.getText2(), '', 'no summary text2')
- strictEqual(summary.getTimestamp(), 0, 'no summary timestamp')
-
- //strictEqual(msg.getSummarytext(50), text, 'summary text is text')
- strictEqual(msg.getText(), text, 'msg text set correctly')
- strictEqual(msg.getTimestamp(), 0, 'no timestamp')
-
- const viewType = msg.getViewType()
- strictEqual(viewType.isText(), true)
- strictEqual(viewType.isImage(), false)
- strictEqual(viewType.isGif(), false)
- strictEqual(viewType.isAudio(), false)
- strictEqual(viewType.isVoice(), false)
- strictEqual(viewType.isVideo(), false)
- strictEqual(viewType.isFile(), false)
-
- strictEqual(msg.getWidth(), 0, 'no message width')
- strictEqual(msg.isDeadDrop(), false, 'not deaddrop')
- strictEqual(msg.isForwarded(), false, 'not forwarded')
- strictEqual(msg.isInfo(), false, 'not an info message')
- strictEqual(msg.isSent(), false, 'messge is not sent')
- strictEqual(msg.isSetupmessage(), false, 'not an autocrypt setup message')
-
- msg.latefilingMediasize(10, 20, 30)
- strictEqual(msg.getWidth(), 10, 'message width set correctly')
- strictEqual(msg.getHeight(), 20, 'message height set correctly')
- strictEqual(msg.getDuration(), 30, 'message duration set correctly')
-
- msg.setDimension(100, 200)
- strictEqual(msg.getWidth(), 100, 'message width set correctly')
- strictEqual(msg.getHeight(), 200, 'message height set correctly')
-
- msg.setDuration(314)
- strictEqual(msg.getDuration(), 314, 'message duration set correctly')
-
- expect(() => {
- msg.setFile(null)
- }).to.throw('Missing filename')
-
- const logo = join(__dirname, 'fixtures', 'logo.png')
- const stat = statSync(logo)
- msg.setFile(logo)
- strictEqual(msg.getFilebytes(), stat.size, 'correct file size')
- strictEqual(msg.getFile(), logo, 'correct file name')
- strictEqual(msg.getFilemime(), 'image/png', 'mime set implicitly')
- msg.setFile(logo, 'image/gif')
- strictEqual(msg.getFilemime(), 'image/gif', 'mime set (in)correctly')
- msg.setFile(logo, 'image/png')
- strictEqual(msg.getFilemime(), 'image/png', 'mime set correctly')
-
- const json = msg.toJson()
- expect(json).to.not.equal(null, 'not null')
- strictEqual(typeof json, 'object', 'json object')
- })
-
- it('Contact methods', function () {
- const contactId = context.createContact('First Last', 'first.last@site.org')
- const contact = context.getContact(contactId)
-
- strictEqual(contact.getAddress(), 'first.last@site.org', 'correct address')
- strictEqual(typeof contact.color, 'string', 'color is a string')
- strictEqual(contact.getDisplayName(), 'First Last', 'correct display name')
- strictEqual(contact.getId(), contactId, 'contact id matches')
- strictEqual(contact.getName(), 'First Last', 'correct name')
- strictEqual(contact.getNameAndAddress(), 'First Last (first.last@site.org)')
- strictEqual(contact.getProfileImage(), null, 'no contact image')
- strictEqual(contact.isBlocked(), false, 'not blocked')
- strictEqual(contact.isVerified(), false, 'unverified status')
- strictEqual(contact.lastSeen, 0, 'last seen unknown')
- })
-
- it('create contacts from address book', function () {
- const addresses = [
- 'Name One',
- 'name1@site.org',
- 'Name Two',
- 'name2@site.org',
- 'Name Three',
- 'name3@site.org',
- ]
- const count = context.addAddressBook(addresses.join('\n'))
- strictEqual(count, addresses.length / 2)
- context
- .getContacts(0, 'Name ')
- .map((id) => context.getContact(id))
- .forEach((contact) => {
- expect(contact.getName().startsWith('Name ')).to.be.true
- })
- })
-
- it('delete contacts', function () {
- const id = context.createContact('someuser', 'someuser@site.com')
- const contact = context.getContact(id)
- strictEqual(contact.getId(), id, 'contact id matches')
- context.deleteContact(id)
- strictEqual(context.getContact(id), null, 'contact is gone')
- })
-
- it('adding and removing a contact from a chat', function () {
- const chatId = context.createGroupChat('adding_and_removing')
- const contactId = context.createContact('Add Remove', 'add.remove@site.com')
- strictEqual(
- context.addContactToChat(chatId, contactId),
- true,
- 'contact added'
- )
- strictEqual(
- context.isContactInChat(chatId, contactId),
- true,
- 'contact in chat'
- )
- strictEqual(
- context.removeContactFromChat(chatId, contactId),
- true,
- 'contact removed'
- )
- strictEqual(
- context.isContactInChat(chatId, contactId),
- false,
- 'contact not in chat'
- )
- })
-
- it('blocking contacts', function () {
- const id = context.createContact('badcontact', 'bad@site.com')
-
- strictEqual(context.getBlockedCount(), 0)
- strictEqual(context.getContact(id).isBlocked(), false)
- expect(context.getBlockedContacts()).to.be.empty
-
- context.blockContact(id, true)
- strictEqual(context.getBlockedCount(), 1)
- strictEqual(context.getContact(id).isBlocked(), true)
- expect(context.getBlockedContacts()).to.deep.equal([id])
-
- context.blockContact(id, false)
- strictEqual(context.getBlockedCount(), 0)
- strictEqual(context.getContact(id).isBlocked(), false)
- expect(context.getBlockedContacts()).to.be.empty
- })
-
- it('ChatList methods', function () {
- const ids = [
- context.createGroupChat('groupchat1'),
- context.createGroupChat('groupchat11'),
- context.createGroupChat('groupchat111'),
- ]
-
- let chatList = context.getChatList(0, 'groupchat1', null)
- strictEqual(chatList.getCount(), 3, 'should contain above chats')
- expect(ids.indexOf(chatList.getChatId(0))).not.to.equal(-1)
- expect(ids.indexOf(chatList.getChatId(1))).not.to.equal(-1)
- expect(ids.indexOf(chatList.getChatId(2))).not.to.equal(-1)
-
- const lot = chatList.getSummary(0)
- strictEqual(lot.getId(), 0, 'lot has no id')
- strictEqual(lot.getState(), C.DC_STATE_IN_NOTICED, 'correct state')
-
- const text = 'Others will only see this group after you sent a first message.'
- context.createGroupChat('groupchat1111')
- chatList = context.getChatList(0, 'groupchat1111', null)
- strictEqual(
- chatList.getSummary(0).getText2(),
- text,
- 'custom new group message'
- )
-
- context.setChatVisibility(ids[0], C.DC_CHAT_VISIBILITY_ARCHIVED)
- chatList = context.getChatList(C.DC_GCL_ARCHIVED_ONLY, 'groupchat1', null)
- strictEqual(chatList.getCount(), 1, 'only one archived')
- })
-
- it('Remove quote from (draft) message', function () {
- context.addDeviceMessage('test_quote', 'test')
- const msgId = context.getChatMessages(10, 0, 0)[0]
- const msg = context.messageNew()
-
- msg.setQuote(context.getMessage(msgId))
- expect(msg.getQuotedMessage()).to.not.be.null
- msg.setQuote(null)
- expect(msg.getQuotedMessage()).to.be.null
- })
-})
-
-describe('Integration tests', function () {
- this.timeout(60 * 5 * 1000) // increase timeout to 5 min
-
- let [dc, context, accountId, directory, account] = [
- null,
- null,
- null,
- null,
- null,
- ]
-
- let [dc2, context2, accountId2, directory2, account2] = [
- null,
- null,
- null,
- null,
- null,
- ]
-
- this.beforeEach(async function () {
- let tmp = DeltaChat.newTemporary()
- dc = tmp.dc
- context = tmp.context
- accountId = tmp.accountId
- directory = tmp.directory
- dc.startEvents()
- })
-
- this.afterEach(async function () {
- if (context) {
- try {
- context.stopOngoingProcess()
- } catch (error) {
- console.error(error)
- }
- }
- if (context2) {
- try {
- context2.stopOngoingProcess()
- } catch (error) {
- console.error(error)
- }
- }
-
- if (dc) {
- try {
- dc.stopIO()
- dc.close()
- } catch (error) {
- console.error(error)
- }
- }
-
- dc = null
- context = null
- accountId = null
- directory = null
-
- context2 = null
- accountId2 = null
- directory2 = null
- })
-
- this.beforeAll(async function () {
- account = createTempUser(process.env.CHATMAIL_DOMAIN)
- if (!account || !account.email || !account.password) {
- console.log(
- "We didn't got back an account from the api, skip integration tests"
- )
- this.skip()
- }
- })
-
- it('configure', async function () {
- strictEqual(context.isConfigured(), false, 'should not be configured')
-
- // Not sure what's the best way to check the events
- // TODO: check the events
-
- // dc.once('DC_EVENT_CONFIGURE_PROGRESS', (data) => {
- // t.pass('DC_EVENT_CONFIGURE_PROGRESS called at least once')
- // })
- // dc.on('DC_EVENT_ERROR', (error) => {
- // console.error('DC_EVENT_ERROR', error)
- // })
- // dc.on('DC_EVENT_ERROR_NETWORK', (first, error) => {
- // console.error('DC_EVENT_ERROR_NETWORK', error)
- // })
-
- // dc.on('ALL', (event, data1, data2) => console.log('ALL', event, data1, data2))
-
- await expect(
- context.configure({
- addr: account.email,
- mail_pw: account.password,
-
- displayname: 'Delta One',
- selfstatus: 'From Delta One with <3',
- selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
- })
- ).to.be.eventually.fulfilled
-
- strictEqual(context.getConfig('addr'), account.email, 'addr correct')
- strictEqual(
- context.getConfig('displayname'),
- 'Delta One',
- 'displayName correct'
- )
- strictEqual(
- context.getConfig('selfstatus'),
- 'From Delta One with <3',
- 'selfStatus correct'
- )
- expect(
- context.getConfig('selfavatar').endsWith('avatar.png'),
- 'selfavatar correct'
- )
- strictEqual(context.getConfig('e2ee_enabled'), '1', 'e2ee_enabled correct')
- strictEqual(
- context.getConfig('save_mime_headers'),
- '',
- 'save_mime_headers correct'
- )
-
- expect(context.getBlobdir().endsWith('db.sqlite-blobs'), 'correct blobdir')
- strictEqual(context.isConfigured(), true, 'is configured')
-
- // whole re-configure to only change displayname: what the heck? (copied this from the old test)
- await expect(
- context.configure({
- addr: account.email,
- mail_pw: account.password,
- displayname: 'Delta Two',
- selfstatus: 'From Delta One with <3',
- selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
- })
- ).to.be.eventually.fulfilled
- strictEqual(
- context.getConfig('displayname'),
- 'Delta Two',
- 'updated displayName correct'
- )
- })
-
- it('Autocrypt setup - key transfer', async function () {
- // Spawn a second dc instance with same account
- // dc.on('ALL', (event, data1, data2) =>
- // console.log('FIRST ', event, data1, data2)
- // )
- dc.stopIO()
- await expect(
- context.configure({
- addr: account.email,
- mail_pw: account.password,
-
- displayname: 'Delta One',
- selfstatus: 'From Delta One with <3',
- selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
- })
- ).to.be.eventually.fulfilled
-
- const accountId2 = dc.addAccount()
- console.log('accountId2:', accountId2)
- context2 = dc.accountContext(accountId2)
-
- let setupCode = null
- const waitForSetupCode = waitForSomething()
- const waitForEnd = waitForSomething()
-
- dc.on('ALL', (event, accountId, data1, data2) => {
- console.log('[' + accountId + ']', event, data1, data2)
- })
-
- dc.on('DC_EVENT_MSGS_CHANGED', async (aId, chatId, msgId) => {
- console.log('[' + accountId + '] DC_EVENT_MSGS_CHANGED', chatId, msgId)
- if (
- aId != accountId ||
- !context.getChat(chatId).isSelfTalk() ||
- !context.getMessage(msgId).isSetupmessage()
- ) {
- return
- }
- console.log('Setupcode!')
- let setupCode = await waitForSetupCode.promise
- // console.log('incoming msg', { setupCode })
- const messages = context.getChatMessages(chatId, 0, 0)
- expect(messages.indexOf(msgId) !== -1, 'msgId is in chat messages').to.be
- .true
- const result = await context.continueKeyTransfer(msgId, setupCode)
- expect(result === true, 'continueKeyTransfer was successful').to.be.true
-
- waitForEnd.done()
- })
-
- dc.stopIO()
- await expect(
- context2.configure({
- addr: account.email,
- mail_pw: account.password,
-
- displayname: 'Delta One',
- selfstatus: 'From Delta One with <3',
- selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
- })
- ).to.be.eventually.fulfilled
- dc.startIO()
-
- console.log('Sending autocrypt setup code')
- setupCode = await context2.initiateKeyTransfer()
- console.log('Sent autocrypt setup code')
- waitForSetupCode.done(setupCode)
- console.log('setupCode is: ' + setupCode)
- expect(typeof setupCode).to.equal('string', 'setupCode is string')
-
- await waitForEnd.promise
- })
-
- it('configure using invalid password should fail', async function () {
- await expect(
- context.configure({
- addr: 'hpk5@testrun.org',
- mail_pw: 'asd',
- })
- ).to.be.eventually.rejected
- })
-})
-
-/**
- * @returns {{done: (result?)=>void, promise:Promise }}
- */
-function waitForSomething() {
- let resolvePromise
- const promise = new Promise((res, rej) => {
- resolvePromise = res
- })
- return {
- done: resolvePromise,
- promise,
- }
-}
diff --git a/node/tsconfig.json b/node/tsconfig.json
deleted file mode 100644
index 25ce300f26..0000000000
--- a/node/tsconfig.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "compilerOptions": {
- "outDir": "dist",
- "rootDir": "./lib",
- "sourceMap": true,
- "module": "commonjs",
- "target": "es5",
- "esModuleInterop": true,
- "declaration": true,
- "declarationMap": true,
- "strictNullChecks": true,
- "strict": true
- },
- "exclude": ["node_modules", "deltachat-core-rust", "dist", "scripts"],
- "typedocOptions": {
- "out": "docs",
- "excludePrivate": true,
- "defaultCategory": "index",
- "includeVersion": true,
- "entryPoints": ["lib/index.ts"]
- }
-}
\ No newline at end of file
diff --git a/node/windows.md b/node/windows.md
deleted file mode 100644
index df19b03d7b..0000000000
--- a/node/windows.md
+++ /dev/null
@@ -1,37 +0,0 @@
-> Steps on how to get windows set up properly for the node bindings
-
-## install git
-
-E.g via
-
-## install node
-
-Download and install `v18` from
-
-## install rust
-
-Download and run `rust-init.exe` from
-
-## configure node for native addons
-
-```
-$ npm i node-gyp -g
-$ npm i windows-build-tools -g
-```
-
-`windows-build-tools` will install `Visual Studio 2017` by default and should not mess with existing installations of `Visual Studio C++`.
-
-## get the code
-
-```
-$ mkdir -p src/deltachat
-$ cd src/deltachat
-$ git clone https://github.com/deltachat/deltachat-node
-```
-
-## build the code
-
-```
-$ cd src/deltachat/deltachat-node
-$ npm install
-```
diff --git a/package.json b/package.json
deleted file mode 100644
index f345b9a9e8..0000000000
--- a/package.json
+++ /dev/null
@@ -1,59 +0,0 @@
-{
- "dependencies": {
- "debug": "^4.1.1",
- "napi-macros": "^2.0.0",
- "node-gyp-build": "^4.6.1"
- },
- "description": "node.js bindings for deltachat-core",
- "devDependencies": {
- "@types/debug": "^4.1.7",
- "@types/node": "^20.8.10",
- "chai": "~4.3.10",
- "chai-as-promised": "^7.1.1",
- "mocha": "^8.2.1",
- "node-gyp": "~10.1.0",
- "prebuildify": "^5.0.1",
- "prebuildify-ci": "^1.0.5",
- "prettier": "^3.0.3",
- "typedoc": "^0.25.3",
- "typescript": "^5.2.2"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "files": [
- "node/scripts/*",
- "*"
- ],
- "homepage": "https://github.com/deltachat/deltachat-core-rust/tree/master/node",
- "license": "GPL-3.0-or-later",
- "main": "node/dist/index.js",
- "name": "deltachat-node",
- "repository": {
- "type": "git",
- "url": "https://github.com/deltachat/deltachat-core-rust.git"
- },
- "scripts": {
- "build": "npm run build:core && npm run build:bindings",
- "build:bindings": "npm run build:bindings:c && npm run build:bindings:ts",
- "build:bindings:c": "npm run build:bindings:c:c && npm run build:bindings:c:postinstall",
- "build:bindings:c:c": "cd node && node-gyp rebuild",
- "build:bindings:c:postinstall": "node node/scripts/postinstall.js",
- "build:bindings:ts": "cd node && tsc",
- "build:core": "npm run build:core:rust && npm run build:core:constants",
- "build:core:constants": "node node/scripts/generate-constants.js",
- "build:core:rust": "node node/scripts/rebuild-core.js",
- "clean": "rm -rf node/dist node/build node/prebuilds node/node_modules ./target",
- "download-prebuilds": "prebuildify-ci download",
- "install": "node node/scripts/install.js",
- "install:prebuilds": "cd node && node-gyp-build \"npm run build:core\" \"npm run build:bindings:c:postinstall\"",
- "lint": "prettier --check \"node/lib/**/*.{ts,tsx}\"",
- "lint-fix": "prettier --write \"node/lib/**/*.{ts,tsx}\" \"node/test/**/*.js\"",
- "prebuildify": "cd node && prebuildify -t 18.0.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
- "test": "npm run test:lint && npm run test:mocha",
- "test:lint": "npm run lint",
- "test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
- },
- "types": "node/dist/index.d.ts",
- "version": "1.155.2"
-}
diff --git a/python/README.rst b/python/README.rst
index 00440fb35c..dd1c6317f1 100644
--- a/python/README.rst
+++ b/python/README.rst
@@ -2,9 +2,9 @@
CFFI Python Bindings
============================
-This package provides `Python bindings`_ to the `deltachat-core library`_
+This package provides `Python bindings`_ to the `chatmail core library`_
which implements IMAP/SMTP/MIME/OpenPGP e-mail standards and offers
a low-level Chat/Contact/Message API to user interfaces and bots.
-.. _`deltachat-core library`: https://github.com/deltachat/deltachat-core-rust
+.. _`chatmail core library`: https://github.com/chatmail/core
.. _`Python bindings`: https://py.delta.chat/
diff --git a/python/doc/cffi/install.rst b/python/doc/cffi/install.rst
index 5a77557a60..db153020e2 100644
--- a/python/doc/cffi/install.rst
+++ b/python/doc/cffi/install.rst
@@ -43,7 +43,7 @@ Bootstrap Rust and Cargo by using rustup::
Then clone the deltachat-core-rust repo::
- git clone https://github.com/deltachat/deltachat-core-rust
+ git clone https://github.com/chatmail/core
cd deltachat-core-rust
To install the Delta Chat Python bindings make sure you have Python3 installed.
diff --git a/python/doc/index.rst b/python/doc/index.rst
index 1a95370d3e..e3fa519d41 100644
--- a/python/doc/index.rst
+++ b/python/doc/index.rst
@@ -2,7 +2,7 @@ Delta Chat Python bindings, new and old
=======
`Delta Chat `_ provides two kinds of Python bindings
-to the `Rust Core `_:
+to the `Rust Core `_:
JSON-RPC bindings and CFFI bindings.
When starting a new project it is recommended to use JSON-RPC bindings,
which are used in the Delta Chat Desktop app through generated Typescript-bindings.
@@ -41,4 +41,4 @@ as the CFFI bindings are increasingly in maintenance-only mode.
.. _virtualenv: http://pypi.org/project/virtualenv/
.. _merlinux: http://merlinux.eu
.. _pypi: http://pypi.org/
-.. _`issue-tracker`: https://github.com/deltachat/deltachat-core-rust
+.. _`issue-tracker`: https://github.com/chatmail/core
diff --git a/python/doc/jsonrpc/develop.rst b/python/doc/jsonrpc/develop.rst
index 0ac25e606a..b9c044acc7 100644
--- a/python/doc/jsonrpc/develop.rst
+++ b/python/doc/jsonrpc/develop.rst
@@ -3,9 +3,9 @@ Development
===========
To develop JSON-RPC bindings,
-clone the `deltachat-core-rust `_ repository::
+clone the `chatmail core `_ repository::
- git clone https://github.com/deltachat/deltachat-core-rust.git
+ git clone https://github.com/chatmail/core.git
Testing
=======
diff --git a/python/doc/jsonrpc/install.rst b/python/doc/jsonrpc/install.rst
index 9310009198..82daf44f59 100644
--- a/python/doc/jsonrpc/install.rst
+++ b/python/doc/jsonrpc/install.rst
@@ -17,8 +17,8 @@ Install ``deltachat-rpc-server``
To get ``deltachat-rpc-server`` binary you have three options:
1. Install ``deltachat-rpc-server`` from PyPI using ``pip install deltachat-rpc-server``.
-2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server``.
-3. Download prebuilt release from https://github.com/deltachat/deltachat-core-rust/releases and install it into ``PATH``.
+2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server``.
+3. Download prebuilt release from https://github.com/chatmail/core/releases and install it into ``PATH``.
Check that ``deltachat-rpc-server`` is installed and can run::
@@ -33,4 +33,4 @@ Install ``deltachat-rpc-client``
To get ``deltachat-rpc-client`` Python library you can:
1. Install ``deltachat-rpc-client`` from PyPI using ``pip install deltachat-rpc-client``.
-2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/deltachat/deltachat-core-rust.git@main#subdirectory=deltachat-rpc-client``.
+2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/chatmail/core.git@main#subdirectory=deltachat-rpc-client``.
diff --git a/python/examples/test_examples.py b/python/examples/test_examples.py
index 943fae0bb7..7e55d6617c 100644
--- a/python/examples/test_examples.py
+++ b/python/examples/test_examples.py
@@ -25,8 +25,8 @@ def test_echo_quit_plugin(acfactory, lp):
(ac1,) = acfactory.get_online_accounts(1)
lp.sec("sending a message to the bot")
- bot_contact = ac1.create_contact(botproc.addr)
- bot_chat = bot_contact.create_chat()
+ bot_chat = ac1.qr_setup_contact(botproc.qr)
+ ac1._evtracker.wait_securejoin_joiner_progress(1000)
bot_chat.send_text("hello")
lp.sec("waiting for the reply message from the bot to arrive")
@@ -48,7 +48,9 @@ def test_group_tracking_plugin(acfactory, lp):
ac2.add_account_plugin(FFIEventLogger(ac2))
lp.sec("creating bot test group with bot")
- bot_contact = ac1.create_contact(botproc.addr)
+ bot_chat = ac1.qr_setup_contact(botproc.qr)
+ ac1._evtracker.wait_securejoin_joiner_progress(1000)
+ bot_contact = bot_chat.get_contacts()[0]
ch = ac1.create_group_chat("bot test group")
ch.add_contact(bot_contact)
ch.send_text("hello")
@@ -60,7 +62,7 @@ def test_group_tracking_plugin(acfactory, lp):
)
lp.sec("adding third member {}".format(ac2.get_config("addr")))
- contact3 = ac1.create_contact(ac2.get_config("addr"))
+ contact3 = ac1.create_contact(ac2)
ch.add_contact(contact3)
reply = ac1._evtracker.wait_next_incoming_message()
diff --git a/python/pyproject.toml b/python/pyproject.toml
index 0c3a81e1d9..aa5b360aea 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
-version = "1.155.2"
+version = "1.159.5"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"
@@ -29,8 +29,8 @@ dependencies = [
]
[project.urls]
-"Home" = "https://github.com/deltachat/deltachat-core-rust/"
-"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
+"Home" = "https://github.com/chatmail/core/"
+"Bug Tracker" = "https://github.com/chatmail/core/issues"
"Documentation" = "https://py.delta.chat/"
"Mastodon" = "https://chaos.social/@delta"
diff --git a/python/src/deltachat/__init__.py b/python/src/deltachat/__init__.py
index 3d2734cca5..1d9080b36a 100644
--- a/python/src/deltachat/__init__.py
+++ b/python/src/deltachat/__init__.py
@@ -55,6 +55,8 @@ def run_cmdline(argv=None, account_plugins=None):
args = parser.parse_args(argv[1:])
ac = Account(args.db)
+ qr = ac.get_setup_contact_qr()
+ print(qr)
ac.run_account(addr=args.email, password=args.password, account_plugins=account_plugins, show_ffi=args.show_ffi)
diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py
index f780139620..8bc199ce9e 100644
--- a/python/src/deltachat/account.py
+++ b/python/src/deltachat/account.py
@@ -280,6 +280,12 @@ def create_contact(self, obj, name: Optional[str] = None) -> Contact:
:param name: (optional) display name for this contact
:returns: :class:`deltachat.contact.Contact` instance.
"""
+ if isinstance(obj, Account):
+ if not obj.is_configured():
+ raise ValueError("Can only add configured accounts as contacts")
+ assert name is None
+ vcard = obj.get_self_contact().make_vcard()
+ return self.import_vcard(vcard)[0]
(name, addr) = self.get_contact_addr_and_name(obj, name)
name_c = as_dc_charpointer(name)
addr_c = as_dc_charpointer(addr)
@@ -349,25 +355,26 @@ def get_contacts(
self,
query: Optional[str] = None,
with_self: bool = False,
- only_verified: bool = False,
) -> List[Contact]:
"""get a (filtered) list of contacts.
:param query: if a string is specified, only return contacts
whose name or e-mail matches query.
- :param only_verified: if true only return verified contacts.
:param with_self: if true the self-contact is also returned.
:returns: list of :class:`deltachat.contact.Contact` objects.
"""
flags = 0
query_c = as_dc_charpointer(query)
- if only_verified:
- flags |= const.DC_GCL_VERIFIED_ONLY
if with_self:
flags |= const.DC_GCL_ADD_SELF
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query_c), lib.dc_array_unref)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
+ def import_vcard(self, vcard):
+ """Import a vCard and return an array of contacts."""
+ dc_array = ffi.gc(lib.dc_import_vcard(self._dc_context, as_dc_charpointer(vcard)), lib.dc_array_unref)
+ return list(iter_array(dc_array, lambda x: Contact(self, x)))
+
def get_fresh_messages(self) -> Generator[Message, None, None]:
"""yield all fresh messages from all chats."""
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py
index e28a31178d..fc5713d61a 100644
--- a/python/src/deltachat/contact.py
+++ b/python/src/deltachat/contact.py
@@ -90,6 +90,14 @@ def get_profile_image(self) -> Optional[str]:
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
return from_optional_dc_charpointer(dc_res)
+ def make_vcard(self) -> str:
+ """Make a contact vCard.
+
+ :returns: vCard
+ """
+ dc_context = self.account._dc_context
+ return from_dc_charpointer(lib.dc_make_vcard(dc_context, self.id))
+
@property
def status(self):
"""Get contact status.
diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py
index 3508e2dd7d..d5447d76cd 100644
--- a/python/src/deltachat/message.py
+++ b/python/src/deltachat/message.py
@@ -118,7 +118,7 @@ def set_file(self, path, mime_type=None):
mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type)
if not os.path.exists(path):
raise ValueError(f"path does not exist: {path!r}")
- lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
+ lib.dc_msg_set_file_and_deduplicate(self._dc_msg, as_dc_charpointer(path), ffi.NULL, mtype)
@props.with_doc
def basename(self) -> str:
@@ -215,7 +215,7 @@ def continue_key_transfer(self, setup_code):
"""extract key and use it as primary key for this account."""
res = lib.dc_continue_key_transfer(self.account._dc_context, self.id, as_dc_charpointer(setup_code))
if res == 0:
- raise ValueError("could not decrypt")
+ raise ValueError("Importing the key from Autocrypt Setup Message failed")
@props.with_doc
def time_sent(self):
@@ -285,23 +285,6 @@ def force_plaintext(self) -> None:
"""Force the message to be sent in plain text."""
lib.dc_msg_force_plaintext(self._dc_msg)
- def get_mime_headers(self):
- """return mime-header object for an incoming message.
-
- This only returns a non-None object if ``save_mime_headers``
- config option was set and the message is incoming.
-
- :returns: email-mime message object (with headers only, no body).
- """
- import email
-
- mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
- if mime_headers:
- s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
- if isinstance(s, bytes):
- return email.message_from_bytes(s)
- return email.message_from_string(s)
-
@property
def error(self) -> Optional[str]:
"""Error message."""
diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py
index 92da00ea54..aecd333ff6 100644
--- a/python/src/deltachat/testplugin.py
+++ b/python/src/deltachat/testplugin.py
@@ -423,8 +423,6 @@ def get_next_liveconfig(self):
where we can make valid SMTP and IMAP connections with.
"""
configdict = next(self._liveconfig_producer).copy()
- if "e2ee_enabled" not in configdict:
- configdict["e2ee_enabled"] = "1"
if self.pytestconfig.getoption("--strict-tls"):
# Enable strict certificate checks for online accounts
@@ -484,12 +482,8 @@ def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Acc
addr = f"{acname}@offline.org"
ac.update_config(
{
- "addr": addr,
- "displayname": acname,
- "mail_pw": "123",
"configured_addr": addr,
- "configured_mail_pw": "123",
- "configured": "1",
+ "displayname": acname,
},
)
self._preconfigure_key(ac)
@@ -651,6 +645,9 @@ class BotProcess:
def __init__(self, popen, addr) -> None:
self.popen = popen
+
+ # The first thing the bot prints to stdout is an invite link.
+ self.qr = self.popen.stdout.readline()
self.addr = addr
# we read stdout as quickly as we can in a thread and make
diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py
index 2366e90adb..0a347e18eb 100644
--- a/python/tests/test_0_complex_or_slow.py
+++ b/python/tests/test_0_complex_or_slow.py
@@ -1,7 +1,6 @@
import sys
import time
-import pytest
import deltachat as dc
@@ -17,8 +16,6 @@ def test_group_many_members_add_leave_remove(self, acfactory, lp):
lp.sec("ac1: send message to new group chat")
msg1 = chat.send_text("hello")
assert msg1.is_encrypted()
- gossiped_timestamp = chat.get_summary()["gossiped_timestamp"]
- assert gossiped_timestamp > 0
assert chat.num_contacts() == 3 + 1
@@ -47,19 +44,13 @@ def test_group_many_members_add_leave_remove(self, acfactory, lp):
assert to_remove.addr in sysmsg.text
assert sysmsg.chat.num_contacts() == 3
- # Receiving message about removed contact does not reset gossip
- assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
-
lp.sec("ac1: sending another message to the chat")
chat.send_text("hello2")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello2"
- assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: adding fifth member to the chat")
chat.add_contact(ac5)
- # Adding contact to chat resets gossiped_timestamp
- assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp
lp.sec("ac2: receiving system message about contact addition")
sysmsg = ac2._evtracker.wait_next_incoming_message()
@@ -196,118 +187,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_encrypted()
-@pytest.mark.parametrize("mvbox_move", [False, True])
-def test_fetch_existing(acfactory, lp, mvbox_move):
- """Delta Chat reads the recipients from old emails sent by the user and adds them as contacts.
- This way, we can already offer them some email addresses they can write to.
-
- Also, the newest existing emails from each folder are fetched during onboarding.
-
- Additionally tests that bcc_self messages moved to the mvbox/sentbox are marked as read."""
-
- def assert_folders_configured(ac):
- """There was a bug that scan_folders() set the configured folders to None under some circumstances.
- So, check that they are still configured:"""
- assert ac.get_config("configured_sentbox_folder") == "Sent"
- if mvbox_move:
- assert ac.get_config("configured_mvbox_folder")
-
- ac1 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move)
- ac2 = acfactory.new_online_configuring_account()
- acfactory.wait_configured(ac1)
- ac1.direct_imap.create_folder("Sent")
- ac1.set_config("sentbox_watch", "1")
-
- # We need to reconfigure to find the new "Sent" folder.
- # `scan_folders()`, which runs automatically shortly after `start_io()` is invoked,
- # 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)
- acfactory.bring_accounts_online()
- assert_folders_configured(ac1)
-
- lp.sec("send out message with bcc to ourselves")
- ac1.set_config("bcc_self", "1")
- chat = acfactory.get_accepted_chat(ac1, ac2)
- chat.send_text("message text")
-
- lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
- if mvbox_move:
- ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
- else:
- ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
- assert_folders_configured(ac1)
-
- lp.sec("create a cloned ac1 and fetch contact history during configure")
- ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
- ac1_clone.set_config("fetch_existing_msgs", "1")
- acfactory.wait_configured(ac1_clone)
- ac1_clone.start_io()
- assert_folders_configured(ac1_clone)
-
- lp.sec("check that ac2 contact was fetched during configure")
- ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
- ac2_addr = ac2.get_config("addr")
- assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())
- assert_folders_configured(ac1_clone)
-
- lp.sec("check that messages changed events arrive for the correct message")
- msg = ac1_clone._evtracker.wait_next_messages_changed()
- assert msg.text == "message text"
- assert_folders_configured(ac1)
- assert_folders_configured(ac1_clone)
-
-
-def test_fetch_existing_msgs_group_and_single(acfactory, lp):
- """There was a bug concerning fetch-existing-msgs:
-
- A sent a message to you, adding you to a group. This created a contact request.
- You wrote a message to A, creating a chat.
- ...but the group stayed blocked.
- So, after fetch-existing-msgs you have one contact request and one chat with the same person.
-
- See https://github.com/deltachat/deltachat-core-rust/issues/2097"""
- ac1 = acfactory.new_online_configuring_account()
- ac2 = acfactory.new_online_configuring_account()
-
- acfactory.bring_accounts_online()
-
- lp.sec("receive a message")
- ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message")
- ac1._evtracker.wait_next_incoming_message()
-
- lp.sec("send out message with bcc to ourselves")
- ac1.set_config("bcc_self", "1")
- ac1_ac2_chat = ac1.create_chat(ac2)
- ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
-
- # wait until the bcc_self message arrives
- ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
-
- lp.sec("Clone online account and let it fetch the existing messages")
- ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
- ac1_clone.set_config("fetch_existing_msgs", "1")
- acfactory.wait_configured(ac1_clone)
-
- ac1_clone.start_io()
- ac1_clone._evtracker.wait_idle_inbox_ready()
-
- chats = ac1_clone.get_chats()
- assert len(chats) == 4 # two newly created chats + self-chat + device-chat
- group_chat = [c for c in chats if c.get_name() == "group name"][0]
- assert group_chat.is_group()
- (private_chat,) = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()]
- assert not private_chat.is_group()
-
- group_messages = group_chat.get_messages()
- assert len(group_messages) == 1
- assert group_messages[0].text == "incoming, unencrypted group message"
- private_messages = private_chat.get_messages()
- # We can't decrypt the message in this chat, so the chat is empty:
- assert len(private_messages) == 0
-
-
def test_undecipherable_group(acfactory, lp):
"""Test how group messages that cannot be decrypted are
handled.
@@ -444,63 +323,6 @@ def test_ephemeral_timer(acfactory, lp):
assert chat1.get_ephemeral_timer() == 0
-def test_multidevice_sync_seen(acfactory, lp):
- """Test that message marked as seen on one device is marked as seen on another."""
- ac1 = acfactory.new_online_configuring_account()
- ac2 = acfactory.new_online_configuring_account()
- ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
- acfactory.bring_accounts_online()
-
- ac1.set_config("bcc_self", "1")
- ac1_clone.set_config("bcc_self", "1")
-
- ac1_chat = ac1.create_chat(ac2)
- ac1_clone_chat = ac1_clone.create_chat(ac2)
- ac2_chat = ac2.create_chat(ac1)
-
- lp.sec("Send a message from ac2 to ac1 and check that it's 'fresh'")
- ac2_chat.send_text("Hi")
- ac1_message = ac1._evtracker.wait_next_incoming_message()
- ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
- assert ac1_chat.count_fresh_messages() == 1
- assert ac1_clone_chat.count_fresh_messages() == 1
- assert ac1_message.is_in_fresh
- assert ac1_clone_message.is_in_fresh
-
- lp.sec("ac1 marks message as seen on the first device")
- ac1.mark_seen_messages([ac1_message])
- assert ac1_message.is_in_seen
-
- lp.sec("ac1 clone detects that message is marked as seen")
- ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
- assert ev.data1 == ac1_clone_chat.id
- assert ac1_clone_message.is_in_seen
-
- lp.sec("Send an ephemeral message from ac2 to ac1")
- ac2_chat.set_ephemeral_timer(60)
- ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
- ac1._evtracker.wait_next_incoming_message()
- ac1_clone._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
- ac1_clone._evtracker.wait_next_incoming_message()
-
- ac2_chat.send_text("Foobar")
- ac1_message = ac1._evtracker.wait_next_incoming_message()
- ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
- assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
- assert "Expires: " not in ac1_clone_message.get_message_info()
- assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
- assert "Expires: " not in ac1_clone_message.get_message_info()
-
- ac1.mark_seen_messages([ac1_message])
- assert ac1_message.is_in_seen
- assert "Expires: " in ac1_message.get_message_info()
- ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
- assert ev.data1 == ac1_clone_chat.id
- 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, tmp_path, lp):
"""The test for the bug #3836:
- Alice has two devices, the second is offline.
@@ -510,6 +332,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac2_addr = ac2.get_config("addr")
+ acfactory.remove_preconfigured_keys()
ac1_offl = acfactory.new_online_configuring_account(cloned_from=ac1)
for ac in [ac1, ac1_offl]:
ac.set_config("bcc_self", "1")
@@ -560,6 +383,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
missing, cannot encrypt".
"""
ac1, ac2 = acfactory.get_online_accounts(2)
+ acfactory.remove_preconfigured_keys()
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
for ac in [ac2, ac2_offl]:
ac.set_config("bcc_self", "1")
@@ -615,6 +439,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
- Now the seconds device has all members verified.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
+ acfactory.remove_preconfigured_keys()
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
for ac in [ac2, ac2_offl]:
ac.set_config("bcc_self", "1")
diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py
index 53124c4d13..e75f36a869 100644
--- a/python/tests/test_1_online.py
+++ b/python/tests/test_1_online.py
@@ -31,37 +31,6 @@ def test_basic_imap_api(acfactory, tmp_path):
imap2.shutdown()
-@pytest.mark.ignored()
-def test_configure_generate_key(acfactory, lp):
- # A slow test which will generate new keys.
- acfactory.remove_preconfigured_keys()
- ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048))
- ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519))
- acfactory.bring_accounts_online()
- chat = acfactory.get_accepted_chat(ac1, ac2)
-
- lp.sec("ac1: send unencrypted message to ac2")
- chat.send_text("message1")
- lp.sec("ac2: waiting for message from ac1")
- msg_in = ac2._evtracker.wait_next_incoming_message()
- assert msg_in.text == "message1"
- assert not msg_in.is_encrypted()
-
- lp.sec("ac2: send encrypted message to ac1")
- msg_in.chat.send_text("message2")
- lp.sec("ac1: waiting for message from ac2")
- msg2_in = ac1._evtracker.wait_next_incoming_message()
- assert msg2_in.text == "message2"
- assert msg2_in.is_encrypted()
-
- lp.sec("ac1: send encrypted message to ac2")
- msg2_in.chat.send_text("message3")
- lp.sec("ac2: waiting for message from ac1")
- msg3_in = ac2._evtracker.wait_next_incoming_message()
- assert msg3_in.text == "message3"
- assert msg3_in.is_encrypted()
-
-
def test_configure_canceled(acfactory):
ac1 = acfactory.new_online_configuring_account()
ac1.stop_ongoing()
@@ -85,78 +54,6 @@ def test_configure_unref(tmp_path):
lib.dc_context_unref(dc_context)
-def test_export_import_self_keys(acfactory, tmp_path, lp):
- ac1, ac2 = acfactory.get_online_accounts(2)
-
- dir = tmp_path / "exportdir"
- dir.mkdir()
- export_files = ac1.export_self_keys(str(dir))
- assert len(export_files) == 2
- for x in export_files:
- assert x.startswith(str(dir))
- (key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
- ac1._evtracker.consume_events()
-
- lp.sec("exported keys (private and public)")
- for name in dir.iterdir():
- lp.indent(str(dir / name))
- lp.sec("importing into existing account")
- ac2.import_self_keys(str(dir))
- (key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*")
- assert key_id2 == key_id
-
-
-def test_one_account_send_bcc_setting(acfactory, lp):
- ac1 = acfactory.new_online_configuring_account()
- ac2 = acfactory.new_online_configuring_account()
- ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
- acfactory.bring_accounts_online()
-
- # test if sent messages are copied to it via BCC.
-
- chat = acfactory.get_accepted_chat(ac1, ac2)
- self_addr = ac1.get_config("addr")
- other_addr = ac2.get_config("addr")
-
- lp.sec("send out message without bcc to ourselves")
- ac1.set_config("bcc_self", "0")
- msg_out = chat.send_text("message1")
- assert not msg_out.is_forwarded()
-
- # wait for send out (no BCC)
- ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
- assert ac1.get_config("bcc_self") == "0"
-
- # make sure we are not sending message to ourselves
- assert self_addr not in ev.data2
- assert other_addr in ev.data2
-
- lp.sec("ac1: setting bcc_self=1")
- ac1.set_config("bcc_self", "1")
-
- lp.sec("send out message with bcc to ourselves")
- msg_out = chat.send_text("message2")
-
- # wait for send out (BCC)
- ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
- assert ac1.get_config("bcc_self") == "1"
-
- # Second client receives only second message, but not the first.
- ev_msg = ac1_clone._evtracker.wait_next_messages_changed()
- assert ev_msg.text == msg_out.text
-
- # now make sure we are sending message to ourselves too
- assert self_addr in ev.data2
- assert other_addr in ev.data2
-
- # BCC-self messages are marked as seen by the sender device.
- ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
-
- # Check that the message is marked as seen on IMAP.
- ac1.direct_imap.select_folder("Inbox")
- assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
-
-
def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -539,26 +436,6 @@ def test_forward_messages(acfactory, lp):
assert not chat3.get_messages()
-def test_forward_encrypted_to_unencrypted(acfactory, lp):
- ac1, ac2, ac3 = acfactory.get_online_accounts(3)
- chat = acfactory.get_protected_chat(ac1, ac2)
-
- lp.sec("ac1: send encrypted message to ac2")
- txt = "This should be encrypted"
- chat.send_text(txt)
- msg = ac2._evtracker.wait_next_incoming_message()
- assert msg.text == txt
- assert msg.is_encrypted()
-
- lp.sec("ac2: forward message to ac3 unencrypted")
- unencrypted_chat = ac2.create_chat(ac3)
- msg_id = msg.id
- msg2 = unencrypted_chat.send_msg(msg)
- assert msg2 == msg
- assert msg.id != msg_id
- assert not msg.is_encrypted()
-
-
def test_forward_own_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -921,12 +798,6 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
msg3.mark_seen()
assert not list(ac1.get_fresh_messages())
- # Test that we do not gossip peer keys in 1-to-1 chat,
- # as it makes no sense to gossip to peers their own keys.
- # Gossip is only sent in encrypted messages,
- # and we sent encrypted msg_back right above.
- assert chat2b.get_summary()["gossiped_timestamp"] == 0
-
lp.sec("create group chat with two members, one of which has no encrypt state")
chat = ac1.create_group_chat("encryption test")
chat.add_contact(ac2)
@@ -936,102 +807,8 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
-def test_gossip_optimization(acfactory, lp):
- """Test that gossip timestamp is updated when someone else sends gossip,
- so we don't have to send gossip ourselves.
- """
- ac1, ac2, ac3 = acfactory.get_online_accounts(3)
-
- acfactory.introduce_each_other([ac1, ac2])
- acfactory.introduce_each_other([ac2, ac3])
-
- lp.sec("ac1 creates a group chat with ac2")
- group_chat = ac1.create_group_chat("hello")
- group_chat.add_contact(ac2)
- msg = group_chat.send_text("hi")
-
- # No Autocrypt gossip was sent yet.
- gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
- assert gossiped_timestamp == 0
-
- msg = ac2._evtracker.wait_next_incoming_message()
- assert msg.is_encrypted()
- assert msg.text == "hi"
-
- lp.sec("ac2 adds ac3 to the group")
- msg.chat.add_contact(ac3)
-
- lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
- msg = ac1._evtracker.wait_next_incoming_message()
- assert msg.is_encrypted()
-
- # ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
- # ac1 does not need to send gossip because ac2 already did it.
- gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
- assert gossiped_timestamp == int(msg.time_sent.timestamp())
-
-
-def test_gossip_encryption_preference(acfactory, lp):
- """Test that encryption preference of group members is gossiped to new members.
- This is a Delta Chat extension to Autocrypt 1.1.0, which Autocrypt-Gossip headers
- SHOULD NOT contain encryption preference.
- """
- ac1, ac2, ac3 = acfactory.get_online_accounts(3)
-
- lp.sec("ac1 learns that ac2 prefers encryption")
- ac1.create_chat(ac2)
- msg = ac2.create_chat(ac1).send_text("first message")
- msg = ac1._evtracker.wait_next_incoming_message()
- assert msg.text == "first message"
- assert not msg.is_encrypted()
- res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr"))
- assert msg.chat.get_encryption_info() == res
- lp.sec("ac2 learns that ac3 prefers encryption")
- ac2.create_chat(ac3)
- msg = ac3.create_chat(ac2).send_text("I prefer encryption")
- msg = ac2._evtracker.wait_next_incoming_message()
- assert msg.text == "I prefer encryption"
- assert not msg.is_encrypted()
-
- lp.sec("ac3 does not know that ac1 prefers encryption")
- ac1.create_chat(ac3)
- chat = ac3.create_chat(ac1)
- res = "No encryption:\n{}".format(ac1.get_config("addr"))
- assert chat.get_encryption_info() == res
- msg = chat.send_text("not encrypted")
- msg = ac1._evtracker.wait_next_incoming_message()
- assert msg.text == "not encrypted"
- assert not msg.is_encrypted()
-
- lp.sec("ac1 creates a group chat with ac2")
- group_chat = ac1.create_group_chat("hello")
- group_chat.add_contact(ac2)
- encryption_info = group_chat.get_encryption_info()
- res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr"))
- assert encryption_info == res
- msg = group_chat.send_text("hi")
-
- msg = ac2._evtracker.wait_next_incoming_message()
- assert msg.is_encrypted()
- assert msg.text == "hi"
-
- lp.sec("ac2 adds ac3 to the group")
- msg.chat.add_contact(ac3)
- assert msg.is_encrypted()
-
- lp.sec("ac3 learns that ac1 prefers encryption")
- msg = ac3._evtracker.wait_next_incoming_message()
- encryption_info = msg.chat.get_encryption_info().splitlines()
- assert encryption_info[0] == "End-to-end encryption preferred:"
- assert ac1.get_config("addr") in encryption_info[1:]
- assert ac2.get_config("addr") in encryption_info[1:]
- msg = chat.send_text("encrypted")
- assert msg.is_encrypted()
-
-
def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
- ac2.set_config("save_mime_headers", "1")
lp.sec("ac1: create chat with ac2")
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -1042,7 +819,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
" wrapped using format=flowed and unwrapped on the receiver"
)
msg_out = chat.send_text(text1)
- assert not msg_out.is_encrypted()
+ assert msg_out.is_encrypted()
lp.sec("wait for ac2 to receive multi-line non-unicode message")
msg_in = ac2._evtracker.wait_next_incoming_message()
@@ -1051,7 +828,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
lp.sec("sending multi-line unicode text message from ac1 to ac2")
text2 = "äalis\nthis is ßßÄ"
msg_out = chat.send_text(text2)
- assert not msg_out.is_encrypted()
+ assert msg_out.is_encrypted()
lp.sec("wait for ac2 to receive multi-line unicode message")
msg_in = ac2._evtracker.wait_next_incoming_message()
@@ -1220,93 +997,6 @@ def test_dont_show_emails(acfactory, lp):
assert len(msg.chat.get_messages()) == 3
-def test_no_old_msg_is_fresh(acfactory, lp):
- ac1 = acfactory.new_online_configuring_account()
- ac2 = acfactory.new_online_configuring_account()
- ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
- acfactory.bring_accounts_online()
-
- ac1.set_config("e2ee_enabled", "0")
- ac1_clone.set_config("e2ee_enabled", "0")
- ac2.set_config("e2ee_enabled", "0")
-
- ac1_clone.set_config("bcc_self", "1")
-
- ac1.create_chat(ac2)
- ac1_clone.create_chat(ac2)
-
- ac1.get_device_chat().mark_noticed()
-
- lp.sec("Send a first message from ac2 to ac1 and check that it's 'fresh'")
- first_msg_id = ac2.create_chat(ac1).send_text("Hi")
- ac1._evtracker.wait_next_incoming_message()
- assert ac1.create_chat(ac2).count_fresh_messages() == 1
- assert len(list(ac1.get_fresh_messages())) == 1
-
- lp.sec("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
- ac1_clone.create_chat(ac2).send_text("Hi back")
- ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
-
- assert ev.data1 == first_msg_id.chat.id
- assert ac1.create_chat(ac2).count_fresh_messages() == 0
- assert len(list(ac1.get_fresh_messages())) == 0
-
-
-def test_prefer_encrypt(acfactory, lp):
- """Test quorum rule for encryption preference in 1:1 and group chat."""
- ac1 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
- ac2 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
- ac3 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
- acfactory.bring_accounts_online()
- ac1.set_config("e2ee_enabled", "0")
- ac2.set_config("e2ee_enabled", "1")
- ac3.set_config("e2ee_enabled", "0")
-
- # Make sure we do not send a copy to ourselves. This is to
- # test that we count own preference even when we are not in
- # the recipient list.
- ac1.set_config("bcc_self", "0")
- ac2.set_config("bcc_self", "0")
- ac3.set_config("bcc_self", "0")
-
- acfactory.introduce_each_other([ac1, ac2, ac3])
-
- lp.sec("ac1: sending message to ac2")
- chat1 = ac1.create_chat(ac2)
- msg1 = chat1.send_text("message1")
- assert not msg1.is_encrypted()
- ac2._evtracker.wait_next_incoming_message()
-
- lp.sec("ac2: sending message to ac1")
- chat2 = ac2.create_chat(ac1)
- msg2 = chat2.send_text("message2")
- # Own preference is `Mutual` and we have the peer's key.
- assert msg2.is_encrypted()
- ac1._evtracker.wait_next_incoming_message()
-
- lp.sec("ac1: sending message to group chat with ac2 and ac3")
- group = ac1.create_group_chat("hello")
- group.add_contact(ac2)
- group.add_contact(ac3)
- msg3 = group.send_text("message3")
- assert not msg3.is_encrypted()
- ac2._evtracker.wait_next_incoming_message()
- ac3._evtracker.wait_next_incoming_message()
-
- lp.sec("ac3: start preferring encryption and inform ac1")
- ac3.set_config("e2ee_enabled", "1")
- chat3 = ac3.create_chat(ac1)
- msg4 = chat3.send_text("message4")
- # Own preference is `Mutual` and we have the peer's key.
- assert msg4.is_encrypted()
- ac1._evtracker.wait_next_incoming_message()
-
- lp.sec("ac1: sending another message to group chat with ac2 and ac3")
- msg5 = group.send_text("message5")
- # Majority prefers encryption now
- assert msg5.is_encrypted()
-
-
def test_bot(acfactory, lp):
"""Test that bot messages can be identified as such"""
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1335,59 +1025,6 @@ def test_bot(acfactory, lp):
assert msg_in.is_bot()
-def test_quote_encrypted(acfactory, lp):
- """Test that replies to encrypted messages with quotes are encrypted."""
- ac1, ac2 = acfactory.get_online_accounts(2)
-
- lp.sec("ac1: create chat with ac2")
- chat = ac1.create_chat(ac2)
-
- lp.sec("sending text message from ac1 to ac2")
- msg1 = chat.send_text("message1")
- assert not msg1.is_encrypted()
-
- lp.sec("wait for ac2 to receive message")
- msg2 = ac2._evtracker.wait_next_incoming_message()
- assert msg2.text == "message1"
- assert not msg2.is_encrypted()
-
- lp.sec("create new chat with contact and send back (encrypted) message")
- msg2.create_chat().send_text("message-back")
-
- lp.sec("wait for ac1 to receive message")
- msg3 = ac1._evtracker.wait_next_incoming_message()
- assert msg3.text == "message-back"
- assert msg3.is_encrypted()
-
- lp.sec("ac1: e2ee_enabled=0 and see if reply is encrypted")
- print("ac1: e2ee_enabled={}".format(ac1.get_config("e2ee_enabled")))
- print("ac2: e2ee_enabled={}".format(ac2.get_config("e2ee_enabled")))
- ac1.set_config("e2ee_enabled", "0")
-
- for quoted_msg in msg1, msg3:
- # Save the draft with a quote.
- msg_draft = Message.new_empty(ac1, "text")
- msg_draft.set_text("message reply")
- msg_draft.quote = quoted_msg
- chat.set_draft(msg_draft)
-
- # Get the draft and send it.
- msg_draft = chat.get_draft()
- chat.send_msg(msg_draft)
-
- chat.set_draft(None)
- assert chat.get_draft() is None
-
- # Quote should be replaced with "..." if quoted message is encrypted.
- msg_in = ac2._evtracker.wait_next_incoming_message()
- assert msg_in.text == "message reply"
- assert not msg_in.is_encrypted()
- if quoted_msg.is_encrypted():
- assert msg_in.quoted_text == "..."
- else:
- assert msg_in.quoted_text == quoted_msg.text
-
-
def test_quote_attachment(tmp_path, acfactory, lp):
"""Test that replies with an attachment and a quote are received correctly."""
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1421,26 +1058,6 @@ def test_quote_attachment(tmp_path, acfactory, lp):
assert open(received_reply.filename).read() == "data to send"
-def test_saved_mime_on_received_message(acfactory, lp):
- ac1, ac2 = acfactory.get_online_accounts(2)
-
- lp.sec("configure ac2 to save mime headers, create ac1/ac2 chat")
- ac2.set_config("save_mime_headers", "1")
- chat = ac1.create_chat(ac2)
-
- lp.sec("sending text message from ac1 to ac2")
- msg_out = chat.send_text("message1")
- ac1._evtracker.wait_msg_delivered(msg_out)
- assert msg_out.get_mime_headers() is None
-
- lp.sec("wait for ac2 to receive message")
- ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
- in_id = ev.data2
- mime = ac2.get_message_by_id(in_id).get_mime_headers()
- assert mime.get_all("From")
- assert mime.get_all("Received")
-
-
def test_send_mark_seen_clean_incoming_events(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -1597,53 +1214,6 @@ def assert_account_is_proper(ac):
assert ac2.get_latest_backupfile(str(backupdir)) == path2
-def test_ac_setup_message(acfactory, lp):
- # note that the receiving account needs to be configured and running
- # before the setup message is send. DC does not read old messages
- # as of Jul2019
- ac1 = acfactory.new_online_configuring_account()
- ac2 = acfactory.new_online_configuring_account(cloned_from=ac1)
- acfactory.bring_accounts_online()
-
- lp.sec("trigger ac setup message and return setupcode")
- assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
- setup_code = ac1.initiate_key_transfer()
- ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
- msg = ac2.get_message_by_id(ev.data2)
- assert msg.is_setup_message()
- assert msg.get_setupcodebegin() == setup_code[:2]
- lp.sec("try a bad setup code")
- with pytest.raises(ValueError):
- msg.continue_key_transfer(str(reversed(setup_code)))
- lp.sec("try a good setup code")
- print("*************** Incoming ASM File at: ", msg.filename)
- print("*************** Setup Code: ", setup_code)
- msg.continue_key_transfer(setup_code)
- assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
-
-
-def test_ac_setup_message_twice(acfactory, lp):
- ac1 = acfactory.new_online_configuring_account()
- ac2 = acfactory.new_online_configuring_account(cloned_from=ac1)
- acfactory.bring_accounts_online()
-
- lp.sec("trigger ac setup message but ignore")
- assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
- ac1.initiate_key_transfer()
- ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
-
- lp.sec("trigger second ac setup message, wait for receive ")
- setup_code2 = ac1.initiate_key_transfer()
- ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
- msg = ac2.get_message_by_id(ev.data2)
- assert msg.is_setup_message()
- assert msg.get_setupcodebegin() == setup_code2[:2]
-
- lp.sec("process second setup message")
- msg.continue_key_transfer(setup_code2)
- assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
-
-
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group
@@ -1664,7 +1234,7 @@ def test_qr_email_capitalization(acfactory, lp):
lp.sec("ac1 joins a verified group via a QR code")
ac1_chat = ac1.qr_join_chat(qr)
msg = ac1._evtracker.wait_next_incoming_message()
- assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
+ assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr"))
assert len(ac1_chat.get_contacts()) == 2
lp.sec("ac2 joins a verified group via a QR code")
@@ -1763,7 +1333,7 @@ def ac_member_removed(self, chat, contact, message):
lp.sec("ac1: add address2")
# note that if the above create_chat() would not
# happen we would not receive a proper member_added event
- contact2 = chat.add_contact(ac3_addr)
+ contact2 = chat.add_contact(ac3)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
@@ -1932,15 +1502,6 @@ def test_connectivity(acfactory, lp):
assert len(msgs) == 2
assert msgs[1].text == "Hi 2"
- lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
-
- ac1.set_config("configured_mail_pw", "abc")
- ac1.stop_io()
- ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
- ac1.start_io()
- ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
- ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
-
def test_fetch_deleted_msg(acfactory, lp):
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
@@ -2056,7 +1617,7 @@ def test_immediate_autodelete(acfactory, lp):
lp.sec("ac2: wait for close/expunge on autodelete")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
- ac2._evtracker.get_info_contains("close/expunge succeeded")
+ ac2._evtracker.get_info_contains("Close/expunge succeeded.")
lp.sec("ac2: check that message was autodeleted on server")
assert len(ac2.direct_imap.get_all_messages()) == 0
@@ -2092,7 +1653,7 @@ def test_delete_multiple_messages(acfactory, lp):
lp.sec("ac2: test that only one message is left")
while 1:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
- ac2._evtracker.get_info_contains("close/expunge succeeded")
+ ac2._evtracker.get_info_contains("Close/expunge succeeded.")
ac2.direct_imap.select_config_folder("inbox")
nr_msgs = len(ac2.direct_imap.get_all_messages())
assert nr_msgs > 0
@@ -2203,9 +1764,7 @@ def test_name_changes(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("displayname", "Account 1")
- # Similar to acfactory.get_accepted_chat, but without setting the contact name.
- ac2.create_contact(ac1.get_config("addr")).create_chat()
- chat12 = ac1.create_contact(ac2.get_config("addr")).create_chat()
+ chat12 = acfactory.get_accepted_chat(ac1, ac2)
contact = None
def update_name():
@@ -2370,23 +1929,6 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination):
assert len(ac1.direct_imap.get_all_messages()) == 0
-def test_delete_deltachat_folder(acfactory):
- """Test that DeltaChat folder is recreated if user deletes it manually."""
- ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
- ac2 = acfactory.new_online_configuring_account()
- acfactory.wait_configured(ac1)
-
- ac1.direct_imap.conn.folder.delete("DeltaChat")
- assert "DeltaChat" not in ac1.direct_imap.list_folders()
- acfactory.bring_accounts_online()
-
- ac2.create_chat(ac1).send_text("hello")
- msg = ac1._evtracker.wait_next_incoming_message()
- assert msg.text == "hello"
-
- assert "DeltaChat" in ac1.direct_imap.list_folders()
-
-
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.
diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py
index cb5d2ac77f..aae9582163 100644
--- a/python/tests/test_3_offline.py
+++ b/python/tests/test_3_offline.py
@@ -105,10 +105,6 @@ def test_update_config(self, acfactory):
ac1.update_config({"mvbox_move": False})
assert ac1.get_config("mvbox_move") == "0"
- def test_has_savemime(self, acfactory):
- ac1 = acfactory.get_unconfigured_account()
- assert "save_mime_headers" in ac1.get_config("sys.config_keys").split()
-
def test_has_bccself(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
@@ -188,7 +184,6 @@ def test_get_contacts_and_delete(self, acfactory):
assert not ac1.get_contacts(query="some2")
assert ac1.get_contacts(query="some1")
- assert not ac1.get_contacts(only_verified=True)
assert len(ac1.get_contacts(with_self=True)) == 2
assert ac1.delete_contact(contact1)
@@ -436,11 +431,11 @@ def test_message_file(self, chat1, data, lp, fn, typein, typeout):
assert msg.id > 0
assert msg.is_file()
assert os.path.exists(msg.filename)
- assert msg.filename.endswith(msg.basename)
+ assert msg.filename.endswith(".txt") == fn.endswith(".txt")
assert msg.filemime == typeout
msg2 = chat1.send_file(fp, typein)
assert msg2 != msg
- assert msg2.filename != msg.filename
+ assert msg2.filename == msg.filename
def test_create_contact(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
diff --git a/python/tox.ini b/python/tox.ini
index 9866e3b5b9..64a693bbca 100644
--- a/python/tox.ini
+++ b/python/tox.ini
@@ -45,7 +45,7 @@ deps =
pygments
restructuredtext_lint
commands =
- ruff format --quiet --diff setup.py src/deltachat examples/ tests/
+ ruff format --diff setup.py src/deltachat examples/ tests/
ruff check src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst
diff --git a/release-date.in b/release-date.in
index 2da08eae1a..47a3b03014 100644
--- a/release-date.in
+++ b/release-date.in
@@ -1 +1 @@
-2025-01-31
\ No newline at end of file
+2025-05-14
\ No newline at end of file
diff --git a/scripts/concourse/docs_wheels.yml b/scripts/concourse/docs_wheels.yml
index 73e2d7469b..20c099a4a1 100644
--- a/scripts/concourse/docs_wheels.yml
+++ b/scripts/concourse/docs_wheels.yml
@@ -4,14 +4,14 @@ resources:
icon: github
source:
branch: main
- uri: https://github.com/deltachat/deltachat-core-rust.git
+ uri: https://github.com/chatmail/core.git
- name: deltachat-core-rust-release
type: git
icon: github
source:
branch: main
- uri: https://github.com/deltachat/deltachat-core-rust.git
+ uri: https://github.com/chatmail/core.git
tag_filter: "v*"
jobs:
diff --git a/scripts/coredeps/install-rust.sh b/scripts/coredeps/install-rust.sh
index 844b8442a4..1919e3c0c1 100755
--- a/scripts/coredeps/install-rust.sh
+++ b/scripts/coredeps/install-rust.sh
@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
-RUST_VERSION=1.84.0
+RUST_VERSION=1.87.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
diff --git a/scripts/create-provider-data-rs.py b/scripts/create-provider-data-rs.py
index 8e68910de2..62c25b1b91 100755
--- a/scripts/create-provider-data-rs.py
+++ b/scripts/create-provider-data-rs.py
@@ -215,7 +215,7 @@ def process_dir(dir):
" Config, ConfigDefault, Oauth2Authorizer, Provider, ProviderOptions, Server, Status,\n"
"};\n"
"use std::collections::HashMap;\n\n"
- "use once_cell::sync::Lazy;\n\n"
+ "use std::sync::LazyLock;\n\n"
)
process_dir(Path(sys.argv[1]))
@@ -224,7 +224,7 @@ def process_dir(dir):
out_all += out_domains
out_all += "];\n\n"
- out_all += "pub(crate) static PROVIDER_IDS: Lazy> = Lazy::new(|| HashMap::from([\n"
+ out_all += "pub(crate) static PROVIDER_IDS: LazyLock> = LazyLock::new(|| HashMap::from([\n"
out_all += out_ids
out_all += "]));\n\n"
@@ -233,8 +233,8 @@ def process_dir(dir):
else:
now = datetime.datetime.fromisoformat(sys.argv[2])
out_all += (
- "pub static _PROVIDER_UPDATED: Lazy = "
- "Lazy::new(|| chrono::NaiveDate::from_ymd_opt("
+ "pub static _PROVIDER_UPDATED: LazyLock = "
+ "LazyLock::new(|| chrono::NaiveDate::from_ymd_opt("
+ str(now.year)
+ ", "
+ str(now.month)
diff --git a/scripts/set_core_version.py b/scripts/set_core_version.py
index d2abbdb3b1..f140833df6 100755
--- a/scripts/set_core_version.py
+++ b/scripts/set_core_version.py
@@ -67,7 +67,6 @@ def main():
parser.add_argument("newversion")
json_list = [
- "package.json",
"deltachat-jsonrpc/typescript/package.json",
"deltachat-rpc-server/npm-package/package.json",
]
diff --git a/scripts/wheel-rpc-server.py b/scripts/wheel-rpc-server.py
index 6ac1ee6219..4118ec4118 100755
--- a/scripts/wheel-rpc-server.py
+++ b/scripts/wheel-rpc-server.py
@@ -154,6 +154,8 @@ def main():
"armv6l-linux": "linux_armv6l",
"aarch64-linux": "manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
"i686-linux": "manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
+ "arm64-v8a-android": "android_21_arm64_v8a",
+ "armeabi-v7a-android": "android_21_armeabi_v7a",
"win64": "win_amd64",
"win32": "win32",
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
diff --git a/spec.md b/spec.md
index 080cc8d668..2d8a8fc8f6 100644
--- a/spec.md
+++ b/spec.md
@@ -1,10 +1,10 @@
-# chat-mail specification
+# Chatmail Specification
-Version: 0.35.0
+Version: 0.36.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
-This document roughly describes how chat-mail
+This document roughly describes how chatmail
apps use the standard e-mail system
to implement typical messenger functions.
@@ -18,6 +18,8 @@ to implement typical messenger functions.
- [Add and remove members](#add-and-remove-members)
- [Change group name](#change-group-name)
- [Set group image](#set-group-image)
+- [Request editing](#request-editing)
+- [Request deletion](#request-deletion)
- [Set profile image](#set-profile-image)
- [Locations](#locations)
- [User locations](#user-locations)
@@ -304,6 +306,84 @@ To save data, it is RECOMMENDED
to add a `Chat-Group-Avatar` only on image changes.
+# Request editing
+
+To request recipients to edit the text of an already sent message,
+the messenger MUST set the header `Chat-Edit`
+with value set to the message-id of the message to edit
+and the body to the new message text.
+
+The body MAY be prefixed by a quote
+and the emoji "✏️" directly before the new text.
+Both MUST be skipped by the recipient.
+
+Receiving messengers MUST look up the message-id from `Chat-Edit`,
+replace the text and MAY indicate the edit in the UI.
+
+The new message text MUST NOT be empty.
+It is not possible to edit images or other attachments, including HTML messages.
+However, they can be deleted for everyone.
+
+Example:
+
+ From: sender@domain
+ To: rcpt@domain
+ Chat-Version: 1.0
+ Message-ID: 00001@domain
+ Content-Type: text/plain
+
+ Hello wordl!
+
+The typo from the message above can be fixed by the following message:
+
+ From: sender@domain
+ To: rcpt@domain
+ Chat-Version: 1.0
+ Chat-Edit: 00001@domain
+ In-Reply-To: 00001@domain
+ Message-ID: 00002@domain
+ Content-Type: text/plain
+
+ On 2025-03-27, sender@domain wrote:
+ > Hello wordl!
+
+ ✏️Hello world!
+
+
+# Request deletion
+
+To request recipient to delete a message,
+the messenger MUST set the header `Chat-Delete`
+with the value set to the message-id of the message to delete.
+
+Receiving messengers MUST look up the message-id, delete the corresponding message
+and MAY indicating the deletion in the UI.
+
+The sender MUST set the body to any, non-empty text.
+The receiver MUST ignore the body.
+
+Example:
+
+ From: sender@domain
+ To: rcpt@domain
+ Chat-Version: 1.0
+ Message-ID: 00003@domain
+ Content-Type: text/plain
+
+ reminder for my pin: 1234
+
+The message above can be requested for deletion by the following message:
+
+ From: sender@domain
+ To: rcpt@domain
+ Chat-Version: 1.0
+ Chat-Delete: 00003@domain
+ Message-ID: 00004@domain
+ Content-Type: text/plain
+
+ foo
+
+
# Set profile image
A user MAY have a profile-image that MAY be distributed to their contacts.
@@ -375,7 +455,7 @@ eg. forwarded from a normal MUA.
-
+
2020-01-11T20:40:19Z
1.234,5.678
@@ -542,4 +622,4 @@ We define the effective date of a message
as the sending time of the message as indicated by its Date header,
or the time of first receipt if that date is in the future or unavailable.
-Copyright © 2017-2021 Delta Chat contributors.
+Copyright © Chatmail contributors.
diff --git a/src/accounts.rs b/src/accounts.rs
index 4d18316b43..3745cfca1b 100644
--- a/src/accounts.rs
+++ b/src/accounts.rs
@@ -4,13 +4,11 @@ use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};
-use anyhow::{ensure, Context as _, Result};
-use futures::stream::FuturesUnordered;
-use futures::StreamExt;
+use anyhow::{bail, ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
-use tokio::task::JoinHandle;
+use tokio::task::{JoinHandle, JoinSet};
use uuid::Uuid;
#[cfg(not(target_os = "ios"))]
@@ -73,9 +71,7 @@ impl Accounts {
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{:?} does not exist", config_file);
- let config = Config::from_file(config_file, writable)
- .await
- .context("failed to load accounts config")?;
+ let config = Config::from_file(config_file, writable).await?;
let events = Events::new();
let stockstrings = StockStrings::new();
let push_subscriber = PushSubscriber::new();
@@ -306,12 +302,6 @@ impl Accounts {
/// This is an auxiliary function and not part of public API.
/// Use [Accounts::background_fetch] instead.
async fn background_fetch_no_timeout(accounts: Vec, events: Events) {
- async fn background_fetch_and_log_error(account: Context) {
- if let Err(error) = account.background_fetch().await {
- warn!(account, "{error:#}");
- }
- }
-
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
@@ -319,11 +309,15 @@ impl Accounts {
accounts.len()
)),
});
- let mut futures_unordered: FuturesUnordered<_> = accounts
- .into_iter()
- .map(background_fetch_and_log_error)
- .collect();
- while futures_unordered.next().await.is_some() {}
+ let mut set = JoinSet::new();
+ for account in accounts {
+ set.spawn(async move {
+ if let Err(error) = account.background_fetch().await {
+ warn!(account, "{error:#}");
+ }
+ });
+ }
+ set.join_all().await;
}
/// Auxiliary function for [Accounts::background_fetch].
@@ -460,7 +454,9 @@ impl Config {
rx.await?;
Ok(())
});
- locked_rx.await?;
+ if locked_rx.await.is_err() {
+ bail!("Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)");
+ };
Ok(Some(lock_task))
}
diff --git a/src/authres.rs b/src/authres.rs
index 84504d1ee2..21879b0f4b 100644
--- a/src/authres.rs
+++ b/src/authres.rs
@@ -4,12 +4,12 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fmt;
+use std::sync::LazyLock;
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;
use mailparse::MailHeaderMap;
use mailparse::ParsedMail;
-use once_cell::sync::Lazy;
use crate::config::Config;
use crate::context::Context;
@@ -107,7 +107,8 @@ fn remove_comments(header: &str) -> Cow<'_, str> {
// In Pomsky, this is:
// "(" Codepoint* lazy ")"
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
- static RE: Lazy = Lazy::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
+ static RE: LazyLock =
+ LazyLock::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
RE.replace_all(header, " ")
}
diff --git a/src/blob.rs b/src/blob.rs
index 1574956bf8..1589b1d3e9 100644
--- a/src/blob.rs
+++ b/src/blob.rs
@@ -1,7 +1,6 @@
//! # Blob directory management.
use core::cmp::max;
-use std::ffi::OsStr;
use std::io::{Cursor, Seek};
use std::iter::FusedIterator;
use std::mem;
@@ -14,8 +13,7 @@ use image::codecs::jpeg::JpegEncoder;
use image::ImageReader;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use num_traits::FromPrimitive;
-use tokio::io::AsyncWriteExt;
-use tokio::{fs, io, task};
+use tokio::{fs, task};
use tokio_stream::wrappers::ReadDirStream;
use crate::config::Config;
@@ -23,6 +21,7 @@ use crate::constants::{self, MediaQuality};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
+use crate::tools::sanitize_filename;
/// Represents a file in the blob directory.
///
@@ -47,73 +46,6 @@ enum ImageOutputFormat {
}
impl<'a> BlobObject<'a> {
- /// Creates a new file, returning a tuple of the name and the handle.
- async fn create_new_file(
- context: &Context,
- dir: &Path,
- stem: &str,
- ext: &str,
- ) -> Result<(String, fs::File)> {
- const MAX_ATTEMPT: u32 = 16;
- let mut attempt = 0;
- let mut name = format!("{stem}{ext}");
- loop {
- attempt += 1;
- let path = dir.join(&name);
- match fs::OpenOptions::new()
- // Using `create_new(true)` in order to avoid race conditions
- // when creating multiple files with the same name.
- .create_new(true)
- .write(true)
- .open(&path)
- .await
- {
- Ok(file) => return Ok((name, file)),
- Err(err) => {
- if attempt >= MAX_ATTEMPT {
- return Err(err).context("failed to create file");
- } else if attempt == 1 && !dir.exists() {
- fs::create_dir_all(dir).await.log_err(context).ok();
- } else {
- name = format!("{}-{}{}", stem, rand::random::(), ext);
- }
- }
- }
- }
- }
-
- /// Creates a new blob object with unique name by copying an existing file.
- ///
- /// This creates a new blob
- /// and copies an existing file into it. This is done in a
- /// in way which avoids race-conditions when multiple files are
- /// concurrently created.
- pub async fn create_and_copy(context: &'a Context, src: &Path) -> Result> {
- let mut src_file = fs::File::open(src)
- .await
- .with_context(|| format!("failed to open file {}", src.display()))?;
- let (stem, ext) = BlobObject::sanitise_name(&src.to_string_lossy());
- let (name, mut dst_file) =
- BlobObject::create_new_file(context, context.get_blobdir(), &stem, &ext).await?;
- let name_for_err = name.clone();
- if let Err(err) = io::copy(&mut src_file, &mut dst_file).await {
- // Attempt to remove the failed file, swallow errors resulting from that.
- let path = context.get_blobdir().join(&name_for_err);
- fs::remove_file(path).await.ok();
- return Err(err).context("failed to copy file");
- }
-
- // Ensure that all buffered bytes are written
- dst_file.flush().await?;
-
- let blob = BlobObject {
- blobdir: context.get_blobdir(),
- name: format!("$BLOBDIR/{name}"),
- };
- context.emit_event(EventType::NewBlobFile(blob.as_name().to_string()));
- Ok(blob)
- }
-
/// Creates a blob object by copying or renaming an existing file.
/// If the source file is already in the blobdir, it will be renamed,
/// otherwise it will be copied to the blobdir first.
@@ -159,10 +91,9 @@ impl<'a> BlobObject<'a> {
let hash = hash.get(0..31).unwrap_or(hash);
let new_file =
if let Some(extension) = original_name.extension().filter(|e| e.len() <= 32) {
- format!(
- "$BLOBDIR/{hash}.{}",
- extension.to_string_lossy().to_lowercase()
- )
+ let extension = extension.to_string_lossy().to_lowercase();
+ let extension = sanitize_filename(&extension);
+ format!("$BLOBDIR/{hash}.{extension}")
} else {
format!("$BLOBDIR/{hash}")
};
@@ -209,27 +140,6 @@ impl<'a> BlobObject<'a> {
})
}
- /// Creates a blob from a file, possibly copying it to the blobdir.
- ///
- /// If the source file is not a path to into the blob directory
- /// the file will be copied into the blob directory first. If the
- /// source file is already in the blobdir it will not be copied
- /// and only be created if it is a valid blobname, that is no
- /// subdirectory is used and [BlobObject::sanitise_name] does not
- /// modify the filename.
- ///
- /// Paths into the blob directory may be either defined by an absolute path
- /// or by the relative prefix `$BLOBDIR`.
- pub async fn new_from_path(context: &'a Context, src: &Path) -> Result> {
- if src.starts_with(context.get_blobdir()) {
- BlobObject::from_path(context, src)
- } else if src.starts_with("$BLOBDIR/") {
- BlobObject::from_name(context, src.to_str().unwrap_or_default().to_string())
- } else {
- BlobObject::create_and_copy(context, src).await
- }
- }
-
/// Returns a [BlobObject] for an existing blob from a path.
///
/// The path must designate a file directly in the blobdir and
@@ -240,11 +150,11 @@ impl<'a> BlobObject<'a> {
let rel_path = path
.strip_prefix(context.get_blobdir())
.with_context(|| format!("wrong blobdir: {}", path.display()))?;
- if !BlobObject::is_acceptible_blob_name(rel_path) {
+ let name = rel_path.to_str().context("wrong name")?;
+ if !BlobObject::is_acceptible_blob_name(name) {
return Err(format_err!("bad blob name: {}", rel_path.display()));
}
- let name = rel_path.to_str().context("wrong name")?;
- BlobObject::from_name(context, name.to_string())
+ BlobObject::from_name(context, name)
}
/// Returns a [BlobObject] for an existing blob.
@@ -253,13 +163,13 @@ impl<'a> BlobObject<'a> {
/// prefixed, as returned by [BlobObject::as_name]. This is how
/// you want to create a [BlobObject] for a filename read from the
/// database.
- pub fn from_name(context: &'a Context, name: String) -> Result> {
- let name: String = match name.starts_with("$BLOBDIR/") {
- true => name.splitn(2, '/').last().unwrap().to_string(),
+ pub fn from_name(context: &'a Context, name: &str) -> Result> {
+ let name = match name.starts_with("$BLOBDIR/") {
+ true => name.splitn(2, '/').last().unwrap(),
false => name,
};
- if !BlobObject::is_acceptible_blob_name(&name) {
- return Err(format_err!("not an acceptable blob name: {}", &name));
+ if !BlobObject::is_acceptible_blob_name(name) {
+ return Err(format_err!("not an acceptable blob name: {}", name));
}
Ok(BlobObject {
blobdir: context.get_blobdir(),
@@ -283,21 +193,12 @@ impl<'a> BlobObject<'a> {
/// Note that this is NOT the user-visible filename,
/// which is only stored in Param::Filename on the message.
///
+ #[allow(rustdoc::private_intra_doc_links)]
/// [Params]: crate::param::Params
pub fn as_name(&self) -> &str {
&self.name
}
- /// Returns the filename of the blob.
- pub fn as_file_name(&self) -> &str {
- self.name.rsplit('/').next().unwrap_or_default()
- }
-
- /// The path relative in the blob directory.
- pub fn as_rel_path(&self) -> &Path {
- Path::new(self.as_file_name())
- }
-
/// Returns the extension of the blob.
///
/// If a blob's filename has an extension, it is always guaranteed
@@ -311,94 +212,21 @@ impl<'a> BlobObject<'a> {
}
}
- /// Create a safe name based on a messy input string.
- ///
- /// The safe name will be a valid filename on Unix and Windows and
- /// not contain any path separators. The input can contain path
- /// segments separated by either Unix or Windows path separators,
- /// the rightmost non-empty segment will be used as name,
- /// sanitised for special characters.
- ///
- /// The resulting name is returned as a tuple, the first part
- /// being the stem or basename and the second being an extension,
- /// including the dot. E.g. "foo.txt" is returned as `("foo",
- /// ".txt")` while "bar" is returned as `("bar", "")`.
- ///
- /// The extension part will always be lowercased.
- fn sanitise_name(name: &str) -> (String, String) {
- let mut name = name;
- for part in name.rsplit('/') {
- if !part.is_empty() {
- name = part;
- break;
- }
- }
- for part in name.rsplit('\\') {
- if !part.is_empty() {
- name = part;
- break;
- }
- }
- let opts = sanitize_filename::Options {
- truncate: true,
- windows: true,
- replacement: "",
- };
-
- let name = sanitize_filename::sanitize_with_options(name, opts);
- // Let's take a tricky filename,
- // "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example.
- // Assume that the extension is 32 chars maximum.
- let ext: String = name
- .chars()
- .rev()
- .take_while(|c| {
- (!c.is_ascii_punctuation() || *c == '.') && !c.is_whitespace() && !c.is_control()
- })
- .take(33)
- .collect::>()
- .iter()
- .rev()
- .collect();
- // ext == "nd_point_and_double_ending.tar.gz"
-
- // Split it into "nd_point_and_double_ending" and "tar.gz":
- let mut iter = ext.splitn(2, '.');
- iter.next();
-
- let ext = iter.next().unwrap_or_default();
- let ext = if ext.is_empty() {
- String::new()
- } else {
- format!(".{ext}")
- // ".tar.gz"
- };
- let stem = name
- .strip_suffix(&ext)
- .unwrap_or_default()
- .chars()
- .take(64)
- .collect();
- (stem, ext.to_lowercase())
- }
-
/// Checks whether a name is a valid blob name.
///
/// This is slightly less strict than stanitise_name, presumably
/// someone already created a file with such a name so we just
- /// ensure it's not actually a path in disguise is actually utf-8.
- fn is_acceptible_blob_name(name: impl AsRef) -> bool {
- let uname = match name.as_ref().to_str() {
- Some(name) => name,
- None => return false,
- };
- if uname.find('/').is_some() {
+ /// ensure it's not actually a path in disguise.
+ ///
+ /// Acceptible blob name always have to be valid utf-8.
+ fn is_acceptible_blob_name(name: &str) -> bool {
+ if name.find('/').is_some() {
return false;
}
- if uname.find('\\').is_some() {
+ if name.find('\\').is_some() {
return false;
}
- if uname.find('\0').is_some() {
+ if name.find('\0').is_some() {
return false;
}
true
@@ -424,26 +252,30 @@ impl<'a> BlobObject<'a> {
Ok(blob.as_name().to_string())
}
+ /// Recode image to avatar size.
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
- let img_wh =
+ let (img_wh, max_bytes) =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
- MediaQuality::Balanced => constants::BALANCED_AVATAR_SIZE,
- MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
+ MediaQuality::Balanced => (
+ constants::BALANCED_AVATAR_SIZE,
+ constants::BALANCED_AVATAR_BYTES,
+ ),
+ MediaQuality::Worse => {
+ (constants::WORSE_AVATAR_SIZE, constants::WORSE_AVATAR_BYTES)
+ }
};
let maybe_sticker = &mut false;
- let strict_limits = true;
- // max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
- // 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
+ let is_avatar = true;
self.recode_to_size(
context,
None, // The name of an avatar doesn't matter
maybe_sticker,
img_wh,
- 20_000,
- strict_limits,
+ max_bytes,
+ is_avatar,
)?;
Ok(())
@@ -472,21 +304,17 @@ impl<'a> BlobObject<'a> {
),
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
};
- let strict_limits = false;
- let new_name = self.recode_to_size(
- context,
- name,
- maybe_sticker,
- img_wh,
- max_bytes,
- strict_limits,
- )?;
+ let is_avatar = false;
+ let new_name =
+ self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?;
Ok(new_name)
}
- /// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
- /// proceed with the result.
+ /// Recodes the image so that it fits into limits on width/height and byte size.
+ ///
+ /// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds
+ /// with the result without rechecking.
///
/// This modifies the blob object in-place.
///
@@ -501,10 +329,10 @@ impl<'a> BlobObject<'a> {
maybe_sticker: &mut bool,
mut img_wh: u32,
max_bytes: usize,
- strict_limits: bool,
+ is_avatar: bool,
) -> Result {
// Add white background only to avatars to spare the CPU.
- let mut add_white_bg = img_wh <= constants::BALANCED_AVATAR_SIZE;
+ let mut add_white_bg = is_avatar;
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let mut name = name.unwrap_or_else(|| self.name.clone());
@@ -575,7 +403,7 @@ impl<'a> BlobObject<'a> {
// also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated
// images.
let do_scale = exceeds_max_bytes
- || strict_limits
+ || is_avatar
&& (exceeds_wh
|| exif.is_some() && {
if mem::take(&mut add_white_bg) {
@@ -604,7 +432,20 @@ impl<'a> BlobObject<'a> {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
- let new_img = img.thumbnail(img_wh, img_wh);
+
+ // resize() results in often slightly better quality,
+ // however, comes at high price of being 4+ times slower than thumbnail().
+ // for a typical camera image that is sent, this may be a change from "instant" (500ms) to "long time waiting" (3s).
+ // as we do not have recoding in background while chat has already a preview,
+ // we vote for speed.
+ // exception is the avatar image: this is far more often sent than recoded,
+ // usually has less pixels by cropping, UI that needs to wait anyways,
+ // and also benefits from slightly better (5%) encoding of Triangle-filtered images.
+ let new_img = if is_avatar {
+ img.resize(img_wh, img_wh, image::imageops::FilterType::Triangle)
+ } else {
+ img.thumbnail(img_wh, img_wh)
+ };
if encoded_img_exceeds_bytes(
context,
@@ -612,7 +453,7 @@ impl<'a> BlobObject<'a> {
ofmt.clone(),
max_bytes,
&mut encoded,
- )? && strict_limits
+ )? && is_avatar
{
if img_wh < 20 {
return Err(format_err!(
@@ -662,7 +503,7 @@ impl<'a> BlobObject<'a> {
match res {
Ok(_) => res,
Err(err) => {
- if !strict_limits && no_exif {
+ if !is_avatar && no_exif {
warn!(
context,
"Cannot recode image, using original data: {err:#}.",
@@ -844,821 +685,4 @@ fn add_white_bg(img: &mut DynamicImage) {
}
#[cfg(test)]
-mod tests {
- use std::time::Duration;
-
- use super::*;
- use crate::message::{Message, Viewtype};
- use crate::sql;
- use crate::test_utils::{self, TestContext};
- use crate::tools::SystemTime;
-
- fn check_image_size(path: impl AsRef, width: u32, height: u32) -> image::DynamicImage {
- tokio::task::block_in_place(move || {
- let img = ImageReader::open(path)
- .expect("failed to open image")
- .with_guessed_format()
- .expect("failed to guess format")
- .decode()
- .expect("failed to decode image");
- assert_eq!(img.width(), width, "invalid width");
- assert_eq!(img.height(), height, "invalid height");
- img
- })
- }
-
- const FILE_BYTES: &[u8] = b"hello";
- const FILE_DEDUPLICATED: &str = "ea8f163db38682925e4491c5e58d4bb.txt";
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_create() {
- let t = TestContext::new().await;
- let blob =
- BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
- let fname = t.get_blobdir().join(FILE_DEDUPLICATED);
- let data = fs::read(fname).await.unwrap();
- assert_eq!(data, FILE_BYTES);
- assert_eq!(blob.as_name(), format!("$BLOBDIR/{FILE_DEDUPLICATED}"));
- assert_eq!(blob.to_abs_path(), t.get_blobdir().join(FILE_DEDUPLICATED));
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_lowercase_ext() {
- let t = TestContext::new().await;
- let blob =
- BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.TXT").unwrap();
- assert!(
- blob.as_name().ends_with(".txt"),
- "Blob {blob:?} should end with .txt"
- );
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_as_file_name() {
- let t = TestContext::new().await;
- let blob =
- BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
- assert_eq!(blob.as_file_name(), FILE_DEDUPLICATED);
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_as_rel_path() {
- let t = TestContext::new().await;
- let blob =
- BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
- assert_eq!(blob.as_rel_path(), Path::new(FILE_DEDUPLICATED));
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_suffix() {
- let t = TestContext::new().await;
- let blob =
- BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
- assert_eq!(blob.suffix(), Some("txt"));
- let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "bar").unwrap();
- assert_eq!(blob.suffix(), None);
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_create_dup() {
- let t = TestContext::new().await;
- BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
- let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED);
- assert!(foo_path.exists());
- BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.txt").unwrap();
- let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
- while let Ok(Some(dirent)) = dir.next_entry().await {
- let fname = dirent.file_name();
- if fname == foo_path.file_name().unwrap() {
- assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
- } else {
- let name = fname.to_str().unwrap();
- assert!(name.ends_with(".txt"));
- }
- }
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_double_ext() {
- let t = TestContext::new().await;
- BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.tar.gz").unwrap();
- let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED).with_extension("gz");
- assert!(foo_path.exists());
- BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.tar.gz").unwrap();
- let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
- while let Ok(Some(dirent)) = dir.next_entry().await {
- let fname = dirent.file_name();
- if fname == foo_path.file_name().unwrap() {
- assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
- } else {
- let name = fname.to_str().unwrap();
- println!("{name}");
- assert_eq!(name.starts_with("foo"), false);
- assert_eq!(name.ends_with(".tar.gz"), false);
- assert!(name.ends_with(".gz"));
- }
- }
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_create_long_names() {
- let t = TestContext::new().await;
- let s = format!("file.{}", "a".repeat(100));
- let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap();
- let blobname = blob.as_name().split('/').last().unwrap();
- assert!(blobname.len() < 70);
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_create_and_copy() {
- let t = TestContext::new().await;
- let src = t.dir.path().join("src");
- fs::write(&src, b"boo").await.unwrap();
- let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
- assert_eq!(blob.as_name(), "$BLOBDIR/src");
- let data = fs::read(blob.to_abs_path()).await.unwrap();
- assert_eq!(data, b"boo");
-
- let whoops = t.dir.path().join("whoops");
- assert!(BlobObject::create_and_copy(&t, whoops.as_ref())
- .await
- .is_err());
- let whoops = t.get_blobdir().join("whoops");
- assert!(!whoops.exists());
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_create_from_path() {
- let t = TestContext::new().await;
-
- let src_ext = t.dir.path().join("external");
- fs::write(&src_ext, b"boo").await.unwrap();
- let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
- .await
- .unwrap();
- assert_eq!(blob.as_name(), "$BLOBDIR/external");
- let data = fs::read(blob.to_abs_path()).await.unwrap();
- assert_eq!(data, b"boo");
-
- let src_int = t.get_blobdir().join("internal");
- fs::write(&src_int, b"boo").await.unwrap();
- let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap();
- assert_eq!(blob.as_name(), "$BLOBDIR/internal");
- let data = fs::read(blob.to_abs_path()).await.unwrap();
- assert_eq!(data, b"boo");
- }
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_create_from_name_long() {
- let t = TestContext::new().await;
- let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
- fs::write(&src_ext, b"boo").await.unwrap();
- let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
- .await
- .unwrap();
- assert_eq!(
- blob.as_name(),
- "$BLOBDIR/autocrypt-setup-message-4137848473.html"
- );
- }
-
- #[test]
- fn test_is_blob_name() {
- assert!(BlobObject::is_acceptible_blob_name("foo"));
- assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
- assert!(BlobObject::is_acceptible_blob_name("f".repeat(128)));
- assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
- assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
- assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
- }
-
- #[test]
- fn test_sanitise_name() {
- let (stem, ext) =
- BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
- assert_eq!(ext, ".txt");
- assert!(!stem.is_empty());
-
- // the extensions are kept together as between stem and extension a number may be added -
- // and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz`
- let (stem, ext) = BlobObject::sanitise_name("wot.tar.gz");
- assert_eq!(stem, "wot");
- assert_eq!(ext, ".tar.gz");
-
- let (stem, ext) = BlobObject::sanitise_name(".foo.bar");
- assert_eq!(stem, "");
- assert_eq!(ext, ".foo.bar");
-
- let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
- assert!(stem.contains("foo"));
- assert!(!stem.contains('?'));
- assert_eq!(ext, ".bar");
-
- let (stem, ext) = BlobObject::sanitise_name("no-extension");
- assert_eq!(stem, "no-extension");
- assert_eq!(ext, "");
-
- let (stem, ext) = BlobObject::sanitise_name("path/ignored\\this: is* forbidden?.c");
- assert_eq!(ext, ".c");
- assert!(!stem.contains("path"));
- assert!(!stem.contains("ignored"));
- assert!(stem.contains("this"));
- assert!(stem.contains("forbidden"));
- assert!(!stem.contains('/'));
- assert!(!stem.contains('\\'));
- assert!(!stem.contains(':'));
- assert!(!stem.contains('*'));
- assert!(!stem.contains('?'));
-
- let (stem, ext) = BlobObject::sanitise_name(
- "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz",
- );
- assert_eq!(
- stem,
- "file.with_lots_of_characters_behind_point_and_double_ending"
- );
- assert_eq!(ext, ".tar.gz");
-
- let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz");
- assert_eq!(stem, "a. tar");
- assert_eq!(ext, ".tar.gz");
-
- let (stem, ext) = BlobObject::sanitise_name("Guia_uso_GNB (v0.8).pdf");
- assert_eq!(stem, "Guia_uso_GNB (v0.8)");
- assert_eq!(ext, ".pdf");
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_add_white_bg() {
- let t = TestContext::new().await;
- let bytes0 = include_bytes!("../test-data/image/logo.png").as_slice();
- let bytes1 = include_bytes!("../test-data/image/avatar900x900.png").as_slice();
- for (bytes, color) in [
- (bytes0, [255u8, 255, 255, 255]),
- (bytes1, [253u8, 198, 0, 255]),
- ] {
- let avatar_src = t.dir.path().join("avatar.png");
- fs::write(&avatar_src, bytes).await.unwrap();
-
- let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
- let img_wh = 128;
- let maybe_sticker = &mut false;
- let strict_limits = true;
- blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
- .unwrap();
- tokio::task::block_in_place(move || {
- let img = ImageReader::open(blob.to_abs_path())
- .unwrap()
- .with_guessed_format()
- .unwrap()
- .decode()
- .unwrap();
- assert!(img.width() == img_wh);
- assert!(img.height() == img_wh);
- assert_eq!(img.get_pixel(0, 0), Rgba(color));
- });
- }
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_selfavatar_outside_blobdir() {
- async fn file_size(path_buf: &Path) -> u64 {
- fs::metadata(path_buf).await.unwrap().len()
- }
-
- let t = TestContext::new().await;
- let avatar_src = t.dir.path().join("avatar.jpg");
- let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
- fs::write(&avatar_src, avatar_bytes).await.unwrap();
- t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
- .await
- .unwrap();
- let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
- let avatar_path = Path::new(&avatar_blob);
- assert!(
- avatar_blob.ends_with("d98cd30ed8f2129bf3968420208849d.jpg"),
- "The avatar filename should be its hash, put instead it's {avatar_blob}"
- );
- let scaled_avatar_size = file_size(avatar_path).await;
- assert!(scaled_avatar_size < avatar_bytes.len() as u64);
-
- check_image_size(avatar_src, 1000, 1000);
- check_image_size(
- &avatar_blob,
- constants::BALANCED_AVATAR_SIZE,
- constants::BALANCED_AVATAR_SIZE,
- );
-
- let mut blob = BlobObject::new_from_path(&t, avatar_path).await.unwrap();
- let maybe_sticker = &mut false;
- let strict_limits = true;
- blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
- .unwrap();
- let new_file_size = file_size(&blob.to_abs_path()).await;
- assert!(new_file_size <= 3000);
- assert!(new_file_size > 2000);
- // The new file should be smaller:
- assert!(new_file_size < scaled_avatar_size);
- // And the original file should not be touched:
- assert_eq!(file_size(avatar_path).await, scaled_avatar_size);
- tokio::task::block_in_place(move || {
- let img = ImageReader::open(blob.to_abs_path())
- .unwrap()
- .with_guessed_format()
- .unwrap()
- .decode()
- .unwrap();
- assert!(img.width() > 130);
- assert_eq!(img.width(), img.height());
- });
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_selfavatar_in_blobdir() {
- let t = TestContext::new().await;
- let avatar_src = t.get_blobdir().join("avatar.png");
- fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
- .await
- .unwrap();
-
- check_image_size(&avatar_src, 900, 900);
-
- t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
- .await
- .unwrap();
- let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
- assert!(
- avatar_cfg.ends_with("9e7f409ac5c92b942cc4f31cee2770a.png"),
- "Avatar file name {avatar_cfg} should end with its hash"
- );
-
- check_image_size(
- avatar_cfg,
- constants::BALANCED_AVATAR_SIZE,
- constants::BALANCED_AVATAR_SIZE,
- );
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_selfavatar_copy_without_recode() {
- let t = TestContext::new().await;
- let avatar_src = t.dir.path().join("avatar.png");
- let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
- fs::write(&avatar_src, avatar_bytes).await.unwrap();
- let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png");
- assert!(!avatar_blob.exists());
- t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
- .await
- .unwrap();
- assert!(avatar_blob.exists());
- assert_eq!(
- fs::metadata(&avatar_blob).await.unwrap().len(),
- avatar_bytes.len() as u64
- );
- let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
- assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_recode_image_1() {
- let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
- SendImageCheckMediaquality {
- viewtype: Viewtype::Image,
- media_quality_config: "0",
- bytes,
- extension: "jpg",
- has_exif: true,
- original_width: 1000,
- original_height: 1000,
- compressed_width: 1000,
- compressed_height: 1000,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
- SendImageCheckMediaquality {
- viewtype: Viewtype::Image,
- media_quality_config: "1",
- bytes,
- extension: "jpg",
- has_exif: true,
- original_width: 1000,
- original_height: 1000,
- compressed_width: 1000,
- compressed_height: 1000,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_recode_image_2() {
- // The "-rotated" files are rotated by 270 degrees using the Exif metadata
- let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
- let img_rotated = SendImageCheckMediaquality {
- viewtype: Viewtype::Image,
- media_quality_config: "0",
- bytes,
- extension: "jpg",
- has_exif: true,
- original_width: 2000,
- original_height: 1800,
- orientation: 270,
- compressed_width: 1800,
- compressed_height: 2000,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
- assert_correct_rotation(&img_rotated);
-
- let mut buf = Cursor::new(vec![]);
- img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
- let bytes = buf.into_inner();
-
- let img_rotated = SendImageCheckMediaquality {
- viewtype: Viewtype::Image,
- media_quality_config: "1",
- bytes: &bytes,
- extension: "jpg",
- original_width: 1800,
- original_height: 2000,
- compressed_width: 1800,
- compressed_height: 2000,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
- assert_correct_rotation(&img_rotated);
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_recode_image_balanced_png() {
- let bytes = include_bytes!("../test-data/image/screenshot.png");
-
- SendImageCheckMediaquality {
- viewtype: Viewtype::Image,
- media_quality_config: "0",
- bytes,
- extension: "png",
- original_width: 1920,
- original_height: 1080,
- compressed_width: 1920,
- compressed_height: 1080,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
-
- SendImageCheckMediaquality {
- viewtype: Viewtype::Image,
- media_quality_config: "1",
- bytes,
- extension: "png",
- original_width: 1920,
- original_height: 1080,
- compressed_width: constants::WORSE_IMAGE_SIZE,
- compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
-
- SendImageCheckMediaquality {
- viewtype: Viewtype::File,
- media_quality_config: "1",
- bytes,
- extension: "png",
- original_width: 1920,
- original_height: 1080,
- compressed_width: 1920,
- compressed_height: 1080,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
-
- SendImageCheckMediaquality {
- viewtype: Viewtype::File,
- media_quality_config: "1",
- bytes,
- extension: "png",
- original_width: 1920,
- original_height: 1080,
- compressed_width: 1920,
- compressed_height: 1080,
- set_draft: true,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
-
- // This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
- SendImageCheckMediaquality {
- viewtype: Viewtype::Sticker,
- media_quality_config: "0",
- bytes,
- extension: "png",
- original_width: 1920,
- original_height: 1080,
- compressed_width: 1920,
- compressed_height: 1080,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
- }
-
- /// Tests that RGBA PNG can be recoded into JPEG
- /// by dropping alpha channel.
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_recode_image_rgba_png_to_jpeg() {
- let bytes = include_bytes!("../test-data/image/screenshot-rgba.png");
-
- SendImageCheckMediaquality {
- viewtype: Viewtype::Image,
- media_quality_config: "1",
- bytes,
- extension: "png",
- original_width: 1920,
- original_height: 1080,
- compressed_width: constants::WORSE_IMAGE_SIZE,
- compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_recode_image_huge_jpg() {
- let bytes = include_bytes!("../test-data/image/screenshot.jpg");
- SendImageCheckMediaquality {
- viewtype: Viewtype::Image,
- media_quality_config: "0",
- bytes,
- extension: "jpg",
- has_exif: true,
- original_width: 1920,
- original_height: 1080,
- compressed_width: constants::BALANCED_IMAGE_SIZE,
- compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
- ..Default::default()
- }
- .test()
- .await
- .unwrap();
- }
-
- fn assert_correct_rotation(img: &DynamicImage) {
- // The test images are black in the bottom left corner after correctly applying
- // the EXIF orientation
-
- let [luma] = img.get_pixel(10, 10).to_luma().0;
- assert_eq!(luma, 255);
- let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
- assert_eq!(luma, 255);
- let [luma] = img
- .get_pixel(img.width() - 10, img.height() - 10)
- .to_luma()
- .0;
- assert_eq!(luma, 255);
- let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
- assert_eq!(luma, 0);
- }
-
- #[derive(Default)]
- struct SendImageCheckMediaquality<'a> {
- pub(crate) viewtype: Viewtype,
- pub(crate) media_quality_config: &'a str,
- pub(crate) bytes: &'a [u8],
- pub(crate) extension: &'a str,
- pub(crate) has_exif: bool,
- pub(crate) original_width: u32,
- pub(crate) original_height: u32,
- pub(crate) orientation: i32,
- pub(crate) compressed_width: u32,
- pub(crate) compressed_height: u32,
- pub(crate) set_draft: bool,
- }
-
- impl SendImageCheckMediaquality<'_> {
- pub(crate) async fn test(self) -> anyhow::Result {
- let viewtype = self.viewtype;
- let media_quality_config = self.media_quality_config;
- let bytes = self.bytes;
- let extension = self.extension;
- let has_exif = self.has_exif;
- let original_width = self.original_width;
- let original_height = self.original_height;
- let orientation = self.orientation;
- let compressed_width = self.compressed_width;
- let compressed_height = self.compressed_height;
- let set_draft = self.set_draft;
-
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
- alice
- .set_config(Config::MediaQuality, Some(media_quality_config))
- .await?;
- let file = alice.get_blobdir().join("file").with_extension(extension);
- let file_name = format!("file.{extension}");
-
- fs::write(&file, &bytes)
- .await
- .context("failed to write file")?;
- check_image_size(&file, original_width, original_height);
-
- let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
- if has_exif {
- let exif = exif.unwrap();
- assert_eq!(exif_orientation(&exif, &alice), orientation);
- } else {
- assert!(exif.is_none());
- }
-
- let mut msg = Message::new(viewtype);
- msg.set_file_and_deduplicate(&alice, &file, Some(&file_name), None)?;
- let chat = alice.create_chat(&bob).await;
- if set_draft {
- chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
- msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
- assert_eq!(msg.get_viewtype(), Viewtype::File);
- }
- let sent = alice.send_msg(chat.id, &mut msg).await;
- let alice_msg = alice.get_last_msg().await;
- assert_eq!(alice_msg.get_width() as u32, compressed_width);
- assert_eq!(alice_msg.get_height() as u32, compressed_height);
- let file_saved = alice
- .get_blobdir()
- .join("saved-".to_string() + &alice_msg.get_filename().unwrap());
- alice_msg.save_file(&alice, &file_saved).await?;
- check_image_size(file_saved, compressed_width, compressed_height);
-
- let bob_msg = bob.recv_msg(&sent).await;
- assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
- assert_eq!(bob_msg.get_width() as u32, compressed_width);
- assert_eq!(bob_msg.get_height() as u32, compressed_height);
- let file_saved = bob
- .get_blobdir()
- .join("saved-".to_string() + &bob_msg.get_filename().unwrap());
- bob_msg.save_file(&bob, &file_saved).await?;
- if viewtype == Viewtype::File {
- assert_eq!(file_saved.extension().unwrap(), extension);
- let bytes1 = fs::read(&file_saved).await?;
- assert_eq!(&bytes1, bytes);
- }
-
- let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
- assert!(exif.is_none());
-
- let img = check_image_size(file_saved, compressed_width, compressed_height);
- Ok(img)
- }
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_send_big_gif_as_image() -> Result<()> {
- let bytes = include_bytes!("../test-data/image/screenshot.gif");
- let (width, height) = (1920u32, 1080u32);
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
- alice
- .set_config(
- Config::MediaQuality,
- Some(&(MediaQuality::Worse as i32).to_string()),
- )
- .await?;
- let file = alice.get_blobdir().join("file").with_extension("gif");
- fs::write(&file, &bytes)
- .await
- .context("failed to write file")?;
- let mut msg = Message::new(Viewtype::Image);
- msg.set_file_and_deduplicate(&alice, &file, Some("file.gif"), None)?;
- let chat = alice.create_chat(&bob).await;
- let sent = alice.send_msg(chat.id, &mut msg).await;
- let bob_msg = bob.recv_msg(&sent).await;
- // DC must detect the image as GIF and send it w/o reencoding.
- assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif);
- assert_eq!(bob_msg.get_width() as u32, width);
- assert_eq!(bob_msg.get_height() as u32, height);
- let file_saved = bob
- .get_blobdir()
- .join("saved-".to_string() + &bob_msg.get_filename().unwrap());
- bob_msg.save_file(&bob, &file_saved).await?;
- let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
- assert_eq!(file_size, bytes.len() as u64);
- check_image_size(file_saved, width, height);
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_send_gif_as_sticker() -> Result<()> {
- let bytes = include_bytes!("../test-data/image/image100x50.gif");
- let alice = &TestContext::new_alice().await;
- let file = alice.get_blobdir().join("file").with_extension("gif");
- fs::write(&file, &bytes)
- .await
- .context("failed to write file")?;
- let mut msg = Message::new(Viewtype::Sticker);
- msg.set_file_and_deduplicate(alice, &file, None, None)?;
- let chat = alice.get_self_chat().await;
- let sent = alice.send_msg(chat.id, &mut msg).await;
- let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
- // Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
- // extension.
- assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_create_and_deduplicate() -> Result<()> {
- let t = TestContext::new().await;
-
- let path = t.get_blobdir().join("anyfile.dat");
- fs::write(&path, b"bla").await?;
- let blob = BlobObject::create_and_deduplicate(&t, &path, &path)?;
- assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f.dat");
- assert_eq!(path.exists(), false);
-
- assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
-
- fs::write(&path, b"bla").await?;
- let blob2 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
- assert_eq!(blob2.name, blob.name);
-
- let path_outside_blobdir = t.dir.path().join("anyfile.dat");
- fs::write(&path_outside_blobdir, b"bla").await?;
- let blob3 =
- BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
- assert!(path_outside_blobdir.exists());
- assert_eq!(blob3.name, blob.name);
-
- fs::write(&path, b"blabla").await?;
- let blob4 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
- assert_ne!(blob4.name, blob.name);
-
- fs::remove_dir_all(t.get_blobdir()).await?;
- let blob5 =
- BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
- assert_eq!(blob5.name, blob.name);
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
- let t = TestContext::new().await;
-
- fs::remove_dir(t.get_blobdir()).await?;
- let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
- assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f");
-
- assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
- let modified1 = blob.to_abs_path().metadata()?.modified()?;
-
- // Test that the modification time of the file is updated when a new file is created
- // so that it's not deleted during housekeeping.
- // We can't use SystemTime::shift() here because file creation uses the actual OS time,
- // which we can't mock from our code.
- tokio::time::sleep(Duration::from_millis(1100)).await;
-
- let blob2 = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
- assert_eq!(blob2.name, blob.name);
-
- let modified2 = blob.to_abs_path().metadata()?.modified()?;
- assert_ne!(modified1, modified2);
- sql::housekeeping(&t).await?;
- assert!(blob2.to_abs_path().exists());
-
- // If we do shift the time by more than 1h, the blob file will be deleted during housekeeping:
- SystemTime::shift(Duration::from_secs(65 * 60));
- sql::housekeeping(&t).await?;
- assert_eq!(blob2.to_abs_path().exists(), false);
-
- let blob3 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
- assert_ne!(blob3.name, blob.name);
-
- {
- // If something goes wrong and the blob file is overwritten,
- // the correct content should be restored:
- fs::write(blob3.to_abs_path(), b"bloblo").await?;
-
- let blob4 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
- let blob4_content = fs::read(blob4.to_abs_path()).await?;
- assert_eq!(blob4_content, b"blabla");
- }
-
- Ok(())
- }
-}
+mod blob_tests;
diff --git a/src/blob/blob_tests.rs b/src/blob/blob_tests.rs
new file mode 100644
index 0000000000..47132baf26
--- /dev/null
+++ b/src/blob/blob_tests.rs
@@ -0,0 +1,750 @@
+use std::time::Duration;
+
+use super::*;
+use crate::message::{Message, Viewtype};
+use crate::param::Param;
+use crate::sql;
+use crate::test_utils::{self, TestContext};
+use crate::tools::SystemTime;
+
+fn check_image_size(path: impl AsRef, width: u32, height: u32) -> image::DynamicImage {
+ tokio::task::block_in_place(move || {
+ let img = ImageReader::open(path)
+ .expect("failed to open image")
+ .with_guessed_format()
+ .expect("failed to guess format")
+ .decode()
+ .expect("failed to decode image");
+ assert_eq!(img.width(), width, "invalid width");
+ assert_eq!(img.height(), height, "invalid height");
+ img
+ })
+}
+
+const FILE_BYTES: &[u8] = b"hello";
+const FILE_DEDUPLICATED: &str = "ea8f163db38682925e4491c5e58d4bb.txt";
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_create() {
+ let t = TestContext::new().await;
+ let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
+ let fname = t.get_blobdir().join(FILE_DEDUPLICATED);
+ let data = fs::read(fname).await.unwrap();
+ assert_eq!(data, FILE_BYTES);
+ assert_eq!(blob.as_name(), format!("$BLOBDIR/{FILE_DEDUPLICATED}"));
+ assert_eq!(blob.to_abs_path(), t.get_blobdir().join(FILE_DEDUPLICATED));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_lowercase_ext() {
+ let t = TestContext::new().await;
+ let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.TXT").unwrap();
+ assert!(
+ blob.as_name().ends_with(".txt"),
+ "Blob {blob:?} should end with .txt"
+ );
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_suffix() {
+ let t = TestContext::new().await;
+ let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
+ assert_eq!(blob.suffix(), Some("txt"));
+ let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "bar").unwrap();
+ assert_eq!(blob.suffix(), None);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_create_dup() {
+ let t = TestContext::new().await;
+ BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
+ let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED);
+ assert!(foo_path.exists());
+ BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.txt").unwrap();
+ let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
+ while let Ok(Some(dirent)) = dir.next_entry().await {
+ let fname = dirent.file_name();
+ if fname == foo_path.file_name().unwrap() {
+ assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
+ } else {
+ let name = fname.to_str().unwrap();
+ assert!(name.ends_with(".txt"));
+ }
+ }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_double_ext() {
+ let t = TestContext::new().await;
+ BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.tar.gz").unwrap();
+ let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED).with_extension("gz");
+ assert!(foo_path.exists());
+ BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.tar.gz").unwrap();
+ let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
+ while let Ok(Some(dirent)) = dir.next_entry().await {
+ let fname = dirent.file_name();
+ if fname == foo_path.file_name().unwrap() {
+ assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
+ } else {
+ let name = fname.to_str().unwrap();
+ println!("{name}");
+ assert_eq!(name.starts_with("foo"), false);
+ assert_eq!(name.ends_with(".tar.gz"), false);
+ assert!(name.ends_with(".gz"));
+ }
+ }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_create_long_names() {
+ let t = TestContext::new().await;
+ let s = format!("file.{}", "a".repeat(100));
+ let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap();
+ let blobname = blob.as_name().split('/').next_back().unwrap();
+ assert!(blobname.len() < 70);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_create_from_name_long() {
+ let t = TestContext::new().await;
+ let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
+ fs::write(&src_ext, b"boo").await.unwrap();
+ let blob = BlobObject::create_and_deduplicate(&t, &src_ext, &src_ext).unwrap();
+ assert_eq!(
+ blob.as_name(),
+ "$BLOBDIR/06f010b24d1efe57ffab44a8ad20c54.html"
+ );
+}
+
+#[test]
+fn test_is_blob_name() {
+ assert!(BlobObject::is_acceptible_blob_name("foo"));
+ assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
+ assert!(BlobObject::is_acceptible_blob_name(&"f".repeat(128)));
+ assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
+ assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
+ assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_add_white_bg() {
+ let t = TestContext::new().await;
+ let bytes0 = include_bytes!("../../test-data/image/logo.png").as_slice();
+ let bytes1 = include_bytes!("../../test-data/image/avatar900x900.png").as_slice();
+ for (bytes, color) in [
+ (bytes0, [255u8, 255, 255, 255]),
+ (bytes1, [253u8, 198, 0, 255]),
+ ] {
+ let avatar_src = t.dir.path().join("avatar.png");
+ fs::write(&avatar_src, bytes).await.unwrap();
+
+ let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap();
+ let img_wh = 128;
+ let maybe_sticker = &mut false;
+ let strict_limits = true;
+ blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
+ .unwrap();
+ tokio::task::block_in_place(move || {
+ let img = ImageReader::open(blob.to_abs_path())
+ .unwrap()
+ .with_guessed_format()
+ .unwrap()
+ .decode()
+ .unwrap();
+ assert!(img.width() == img_wh);
+ assert!(img.height() == img_wh);
+ assert_eq!(img.get_pixel(0, 0), Rgba(color));
+ });
+ }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_selfavatar_outside_blobdir() {
+ async fn file_size(path_buf: &Path) -> u64 {
+ fs::metadata(path_buf).await.unwrap().len()
+ }
+
+ let t = TestContext::new().await;
+ let avatar_src = t.dir.path().join("avatar.jpg");
+ let avatar_bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
+ fs::write(&avatar_src, avatar_bytes).await.unwrap();
+ t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
+ .await
+ .unwrap();
+ let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
+ let avatar_path = Path::new(&avatar_blob);
+ assert!(
+ avatar_blob.ends_with("7dde69e06b5ae6c27520a436bbfd65b.jpg"),
+ "The avatar filename should be its hash, put instead it's {avatar_blob}"
+ );
+ let scaled_avatar_size = file_size(avatar_path).await;
+ assert!(scaled_avatar_size < avatar_bytes.len() as u64);
+
+ check_image_size(avatar_src, 1000, 1000);
+ check_image_size(
+ &avatar_blob,
+ constants::BALANCED_AVATAR_SIZE,
+ constants::BALANCED_AVATAR_SIZE,
+ );
+
+ let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
+ let maybe_sticker = &mut false;
+ let strict_limits = true;
+ blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
+ .unwrap();
+ let new_file_size = file_size(&blob.to_abs_path()).await;
+ assert!(new_file_size <= 3000);
+ assert!(new_file_size > 2000);
+ // The new file should be smaller:
+ assert!(new_file_size < scaled_avatar_size);
+ // And the original file should not be touched:
+ assert_eq!(file_size(avatar_path).await, scaled_avatar_size);
+ tokio::task::block_in_place(move || {
+ let img = ImageReader::open(blob.to_abs_path())
+ .unwrap()
+ .with_guessed_format()
+ .unwrap()
+ .decode()
+ .unwrap();
+ assert!(img.width() > 130);
+ assert_eq!(img.width(), img.height());
+ });
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_selfavatar_in_blobdir() {
+ let t = TestContext::new().await;
+ let avatar_src = t.get_blobdir().join("avatar.png");
+ fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
+ .await
+ .unwrap();
+
+ check_image_size(&avatar_src, 900, 900);
+
+ t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
+ .await
+ .unwrap();
+ let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
+ assert!(
+ avatar_cfg.ends_with("d57cb5ce5f371531b6e1fb17b6dd1af.png"),
+ "Avatar file name {avatar_cfg} should end with its hash"
+ );
+
+ check_image_size(
+ avatar_cfg,
+ constants::BALANCED_AVATAR_SIZE,
+ constants::BALANCED_AVATAR_SIZE,
+ );
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_selfavatar_copy_without_recode() {
+ let t = TestContext::new().await;
+ let avatar_src = t.dir.path().join("avatar.png");
+ let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
+ fs::write(&avatar_src, avatar_bytes).await.unwrap();
+ let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png");
+ assert!(!avatar_blob.exists());
+ t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
+ .await
+ .unwrap();
+ assert!(avatar_blob.exists());
+ assert_eq!(
+ fs::metadata(&avatar_blob).await.unwrap().len(),
+ avatar_bytes.len() as u64
+ );
+ let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
+ assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_recode_image_1() {
+ let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
+ SendImageCheckMediaquality {
+ viewtype: Viewtype::Image,
+ media_quality_config: "0",
+ bytes,
+ extension: "jpg",
+ has_exif: true,
+ original_width: 1000,
+ original_height: 1000,
+ compressed_width: 1000,
+ compressed_height: 1000,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+ SendImageCheckMediaquality {
+ viewtype: Viewtype::Image,
+ media_quality_config: "1",
+ bytes,
+ extension: "jpg",
+ has_exif: true,
+ original_width: 1000,
+ original_height: 1000,
+ compressed_width: 1000,
+ compressed_height: 1000,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_recode_image_2() {
+ // The "-rotated" files are rotated by 270 degrees using the Exif metadata
+ let bytes = include_bytes!("../../test-data/image/rectangle2000x1800-rotated.jpg");
+ let img_rotated = SendImageCheckMediaquality {
+ viewtype: Viewtype::Image,
+ media_quality_config: "0",
+ bytes,
+ extension: "jpg",
+ has_exif: true,
+ original_width: 2000,
+ original_height: 1800,
+ orientation: 270,
+ compressed_width: 1800,
+ compressed_height: 2000,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+ assert_correct_rotation(&img_rotated);
+
+ let mut buf = Cursor::new(vec![]);
+ img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
+ let bytes = buf.into_inner();
+
+ let img_rotated = SendImageCheckMediaquality {
+ viewtype: Viewtype::Image,
+ media_quality_config: "1",
+ bytes: &bytes,
+ extension: "jpg",
+ original_width: 1800,
+ original_height: 2000,
+ compressed_width: 1800,
+ compressed_height: 2000,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+ assert_correct_rotation(&img_rotated);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_recode_image_balanced_png() {
+ let bytes = include_bytes!("../../test-data/image/screenshot.png");
+
+ SendImageCheckMediaquality {
+ viewtype: Viewtype::Image,
+ media_quality_config: "0",
+ bytes,
+ extension: "png",
+ original_width: 1920,
+ original_height: 1080,
+ compressed_width: 1920,
+ compressed_height: 1080,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+
+ SendImageCheckMediaquality {
+ viewtype: Viewtype::Image,
+ media_quality_config: "1",
+ bytes,
+ extension: "png",
+ original_width: 1920,
+ original_height: 1080,
+ compressed_width: constants::WORSE_IMAGE_SIZE,
+ compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+
+ SendImageCheckMediaquality {
+ viewtype: Viewtype::File,
+ media_quality_config: "1",
+ bytes,
+ extension: "png",
+ original_width: 1920,
+ original_height: 1080,
+ compressed_width: 1920,
+ compressed_height: 1080,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+
+ SendImageCheckMediaquality {
+ viewtype: Viewtype::File,
+ media_quality_config: "1",
+ bytes,
+ extension: "png",
+ original_width: 1920,
+ original_height: 1080,
+ compressed_width: 1920,
+ compressed_height: 1080,
+ set_draft: true,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+
+ // This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
+ SendImageCheckMediaquality {
+ viewtype: Viewtype::Sticker,
+ media_quality_config: "0",
+ bytes,
+ extension: "png",
+ original_width: 1920,
+ original_height: 1080,
+ compressed_width: 1920,
+ compressed_height: 1080,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+}
+
+/// Tests that RGBA PNG can be recoded into JPEG
+/// by dropping alpha channel.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_recode_image_rgba_png_to_jpeg() {
+ let bytes = include_bytes!("../../test-data/image/screenshot-rgba.png");
+
+ SendImageCheckMediaquality {
+ viewtype: Viewtype::Image,
+ media_quality_config: "1",
+ bytes,
+ extension: "png",
+ original_width: 1920,
+ original_height: 1080,
+ compressed_width: constants::WORSE_IMAGE_SIZE,
+ compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_recode_image_huge_jpg() {
+ let bytes = include_bytes!("../../test-data/image/screenshot.jpg");
+ SendImageCheckMediaquality {
+ viewtype: Viewtype::Image,
+ media_quality_config: "0",
+ bytes,
+ extension: "jpg",
+ has_exif: true,
+ original_width: 1920,
+ original_height: 1080,
+ compressed_width: constants::BALANCED_IMAGE_SIZE,
+ compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
+ ..Default::default()
+ }
+ .test()
+ .await
+ .unwrap();
+}
+
+fn assert_correct_rotation(img: &DynamicImage) {
+ // The test images are black in the bottom left corner after correctly applying
+ // the EXIF orientation
+
+ let [luma] = img.get_pixel(10, 10).to_luma().0;
+ assert_eq!(luma, 255);
+ let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
+ assert_eq!(luma, 255);
+ let [luma] = img
+ .get_pixel(img.width() - 10, img.height() - 10)
+ .to_luma()
+ .0;
+ assert_eq!(luma, 255);
+ let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
+ assert_eq!(luma, 0);
+}
+
+#[derive(Default)]
+struct SendImageCheckMediaquality<'a> {
+ pub(crate) viewtype: Viewtype,
+ pub(crate) media_quality_config: &'a str,
+ pub(crate) bytes: &'a [u8],
+ pub(crate) extension: &'a str,
+ pub(crate) has_exif: bool,
+ pub(crate) original_width: u32,
+ pub(crate) original_height: u32,
+ pub(crate) orientation: i32,
+ pub(crate) compressed_width: u32,
+ pub(crate) compressed_height: u32,
+ pub(crate) set_draft: bool,
+}
+
+impl SendImageCheckMediaquality<'_> {
+ pub(crate) async fn test(self) -> anyhow::Result {
+ let viewtype = self.viewtype;
+ let media_quality_config = self.media_quality_config;
+ let bytes = self.bytes;
+ let extension = self.extension;
+ let has_exif = self.has_exif;
+ let original_width = self.original_width;
+ let original_height = self.original_height;
+ let orientation = self.orientation;
+ let compressed_width = self.compressed_width;
+ let compressed_height = self.compressed_height;
+ let set_draft = self.set_draft;
+
+ let alice = TestContext::new_alice().await;
+ let bob = TestContext::new_bob().await;
+ alice
+ .set_config(Config::MediaQuality, Some(media_quality_config))
+ .await?;
+ let file = alice.get_blobdir().join("file").with_extension(extension);
+ let file_name = format!("file.{extension}");
+
+ fs::write(&file, &bytes)
+ .await
+ .context("failed to write file")?;
+ check_image_size(&file, original_width, original_height);
+
+ let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
+ if has_exif {
+ let exif = exif.unwrap();
+ assert_eq!(exif_orientation(&exif, &alice), orientation);
+ } else {
+ assert!(exif.is_none());
+ }
+
+ let mut msg = Message::new(viewtype);
+ msg.set_file_and_deduplicate(&alice, &file, Some(&file_name), None)?;
+ let chat = alice.create_chat(&bob).await;
+ if set_draft {
+ chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
+ msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
+ assert_eq!(msg.get_viewtype(), Viewtype::File);
+ }
+ let sent = alice.send_msg(chat.id, &mut msg).await;
+ let alice_msg = alice.get_last_msg().await;
+ assert_eq!(alice_msg.get_width() as u32, compressed_width);
+ assert_eq!(alice_msg.get_height() as u32, compressed_height);
+ let file_saved = alice
+ .get_blobdir()
+ .join("saved-".to_string() + &alice_msg.get_filename().unwrap());
+ alice_msg.save_file(&alice, &file_saved).await?;
+ check_image_size(file_saved, compressed_width, compressed_height);
+
+ if original_width == compressed_width {
+ assert_extension(&alice, alice_msg, extension);
+ } else {
+ assert_extension(&alice, alice_msg, "jpg");
+ }
+
+ let bob_msg = bob.recv_msg(&sent).await;
+ assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
+ assert_eq!(bob_msg.get_width() as u32, compressed_width);
+ assert_eq!(bob_msg.get_height() as u32, compressed_height);
+ let file_saved = bob
+ .get_blobdir()
+ .join("saved-".to_string() + &bob_msg.get_filename().unwrap());
+ bob_msg.save_file(&bob, &file_saved).await?;
+ if viewtype == Viewtype::File {
+ assert_eq!(file_saved.extension().unwrap(), extension);
+ let bytes1 = fs::read(&file_saved).await?;
+ assert_eq!(&bytes1, bytes);
+ }
+
+ let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
+ assert!(exif.is_none());
+
+ let img = check_image_size(file_saved, compressed_width, compressed_height);
+
+ if original_width == compressed_width {
+ assert_extension(&bob, bob_msg, extension);
+ } else {
+ assert_extension(&bob, bob_msg, "jpg");
+ }
+
+ Ok(img)
+ }
+}
+
+fn assert_extension(context: &TestContext, msg: Message, extension: &str) {
+ assert!(msg
+ .param
+ .get(Param::File)
+ .unwrap()
+ .ends_with(&format!(".{extension}")));
+ assert!(msg
+ .param
+ .get(Param::Filename)
+ .unwrap()
+ .ends_with(&format!(".{extension}")));
+ assert!(msg
+ .get_filename()
+ .unwrap()
+ .ends_with(&format!(".{extension}")));
+ assert_eq!(
+ msg.get_file(context)
+ .unwrap()
+ .extension()
+ .unwrap()
+ .to_str()
+ .unwrap(),
+ extension
+ );
+ assert_eq!(
+ msg.param
+ .get_file_blob(context)
+ .unwrap()
+ .unwrap()
+ .suffix()
+ .unwrap(),
+ extension
+ );
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_send_big_gif_as_image() -> Result<()> {
+ let bytes = include_bytes!("../../test-data/image/screenshot.gif");
+ let (width, height) = (1920u32, 1080u32);
+ let alice = TestContext::new_alice().await;
+ let bob = TestContext::new_bob().await;
+ alice
+ .set_config(
+ Config::MediaQuality,
+ Some(&(MediaQuality::Worse as i32).to_string()),
+ )
+ .await?;
+ let file = alice.get_blobdir().join("file").with_extension("gif");
+ fs::write(&file, &bytes)
+ .await
+ .context("failed to write file")?;
+ let mut msg = Message::new(Viewtype::Image);
+ msg.set_file_and_deduplicate(&alice, &file, Some("file.gif"), None)?;
+ let chat = alice.create_chat(&bob).await;
+ let sent = alice.send_msg(chat.id, &mut msg).await;
+ let bob_msg = bob.recv_msg(&sent).await;
+ // DC must detect the image as GIF and send it w/o reencoding.
+ assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif);
+ assert_eq!(bob_msg.get_width() as u32, width);
+ assert_eq!(bob_msg.get_height() as u32, height);
+ let file_saved = bob
+ .get_blobdir()
+ .join("saved-".to_string() + &bob_msg.get_filename().unwrap());
+ bob_msg.save_file(&bob, &file_saved).await?;
+ let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
+ assert_eq!(file_size, bytes.len() as u64);
+ check_image_size(file_saved, width, height);
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_send_gif_as_sticker() -> Result<()> {
+ let bytes = include_bytes!("../../test-data/image/image100x50.gif");
+ let alice = &TestContext::new_alice().await;
+ let file = alice.get_blobdir().join("file").with_extension("gif");
+ fs::write(&file, &bytes)
+ .await
+ .context("failed to write file")?;
+ let mut msg = Message::new(Viewtype::Sticker);
+ msg.set_file_and_deduplicate(alice, &file, None, None)?;
+ let chat = alice.get_self_chat().await;
+ let sent = alice.send_msg(chat.id, &mut msg).await;
+ let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
+ // Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
+ // extension.
+ assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_create_and_deduplicate() -> Result<()> {
+ let t = TestContext::new().await;
+
+ let path = t.get_blobdir().join("anyfile.dat");
+ fs::write(&path, b"bla").await?;
+ let blob = BlobObject::create_and_deduplicate(&t, &path, &path)?;
+ assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f.dat");
+ assert_eq!(path.exists(), false);
+
+ assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
+
+ fs::write(&path, b"bla").await?;
+ let blob2 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
+ assert_eq!(blob2.name, blob.name);
+
+ let path_outside_blobdir = t.dir.path().join("anyfile.dat");
+ fs::write(&path_outside_blobdir, b"bla").await?;
+ let blob3 =
+ BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
+ assert!(path_outside_blobdir.exists());
+ assert_eq!(blob3.name, blob.name);
+
+ fs::write(&path, b"blabla").await?;
+ let blob4 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
+ assert_ne!(blob4.name, blob.name);
+
+ fs::remove_dir_all(t.get_blobdir()).await?;
+ let blob5 =
+ BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
+ assert_eq!(blob5.name, blob.name);
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
+ let t = TestContext::new().await;
+
+ fs::remove_dir(t.get_blobdir()).await?;
+ let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
+ assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f");
+
+ assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
+ let modified1 = blob.to_abs_path().metadata()?.modified()?;
+
+ // Test that the modification time of the file is updated when a new file is created
+ // so that it's not deleted during housekeeping.
+ // We can't use SystemTime::shift() here because file creation uses the actual OS time,
+ // which we can't mock from our code.
+ tokio::time::sleep(Duration::from_millis(1100)).await;
+
+ let blob2 = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
+ assert_eq!(blob2.name, blob.name);
+
+ let modified2 = blob.to_abs_path().metadata()?.modified()?;
+ assert_ne!(modified1, modified2);
+ sql::housekeeping(&t).await?;
+ assert!(blob2.to_abs_path().exists());
+
+ // If we do shift the time by more than 1h, the blob file will be deleted during housekeeping:
+ SystemTime::shift(Duration::from_secs(65 * 60));
+ sql::housekeeping(&t).await?;
+ assert_eq!(blob2.to_abs_path().exists(), false);
+
+ let blob3 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
+ assert_ne!(blob3.name, blob.name);
+
+ {
+ // If something goes wrong and the blob file is overwritten,
+ // the correct content should be restored:
+ fs::write(blob3.to_abs_path(), b"bloblo").await?;
+
+ let blob4 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
+ let blob4_content = fs::read(blob4.to_abs_path()).await?;
+ assert_eq!(blob4_content, b"blabla");
+ }
+
+ Ok(())
+}
diff --git a/src/chat.rs b/src/chat.rs
index be256e5984..0c5fa97922 100644
--- a/src/chat.rs
+++ b/src/chat.rs
@@ -3,6 +3,7 @@
use std::cmp;
use std::collections::{HashMap, HashSet};
use std::fmt;
+use std::io::Cursor;
use std::marker::Sync;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@@ -11,6 +12,7 @@ use std::time::Duration;
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use deltachat_contact_tools::{sanitize_bidi_characters, sanitize_single_line, ContactAddress};
use deltachat_derive::{FromSql, ToSql};
+use mail_builder::mime::MimePart;
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use tokio::task;
@@ -18,12 +20,11 @@ use tokio::task;
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
use crate::chatlist::Chatlist;
-use crate::chatlist_events;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
- DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
+ DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX,
TIMESTAMP_SENT_TOLERANCE,
};
use crate::contact::{self, Contact, ContactId, Origin};
@@ -32,7 +33,6 @@ use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
use crate::ephemeral::{start_chat_ephemeral_timers, Timer as EphemeralTimer};
use crate::events::EventType;
-use crate::html::new_html_mimepart;
use crate::location;
use crate::log::LogExt;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
@@ -41,7 +41,6 @@ use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::receive_imf::ReceivedMsg;
-use crate::securejoin::BobState;
use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
use crate::sync::{self, Sync::*, SyncData};
@@ -51,6 +50,7 @@ use crate::tools::{
truncate_msg_text, IsNoneOrEmpty, SystemTime,
};
use crate::webxdc::StatusUpdateSerial;
+use crate::{chatlist_events, imap};
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -130,8 +130,7 @@ pub(crate) enum CantSendReason {
/// Not a member of the chat.
NotAMember,
- /// Temporary state for 1:1 chats while SecureJoin is in progress, after a timeout sending
- /// messages (incl. unencrypted if we don't yet know the contact's pubkey) is allowed.
+ /// Temporary state for 1:1 chats while SecureJoin is in progress.
SecurejoinWait,
}
@@ -434,7 +433,7 @@ impl ChatId {
.ok();
}
if delete {
- self.delete(context).await?;
+ self.delete_ex(context, Nosync).await?;
}
Ok(())
}
@@ -582,7 +581,18 @@ impl ChatId {
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
};
- add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?;
+ add_info_msg_with_cmd(
+ context,
+ self,
+ &text,
+ cmd,
+ timestamp_sort,
+ None,
+ None,
+ None,
+ None,
+ )
+ .await?;
Ok(())
}
@@ -643,7 +653,7 @@ impl ChatId {
) -> Result<()> {
let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes)
.await
- .with_context(|| format!("can't create chat for {}", contact_id))?;
+ .with_context(|| format!("can't create chat for {contact_id}"))?;
chat_id
.set_protection(
context,
@@ -773,6 +783,10 @@ impl ChatId {
/// Deletes a chat.
pub async fn delete(self, context: &Context) -> Result<()> {
+ self.delete_ex(context, Sync).await
+ }
+
+ pub(crate) async fn delete_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
ensure!(
!self.is_special(),
"bad chat_id, can not be a special chat: {}",
@@ -780,10 +794,23 @@ impl ChatId {
);
let chat = Chat::load_from_db(context, self).await?;
+ let delete_msgs_target = context.get_delete_msgs_target().await?;
+ let sync_id = match sync {
+ Nosync => None,
+ Sync => chat.get_sync_id(context).await?,
+ };
context
.sql
.transaction(|transaction| {
+ transaction.execute(
+ "UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=?)",
+ (delete_msgs_target, self,),
+ )?;
+ transaction.execute(
+ "DELETE FROM smtp WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
+ (self,),
+ )?;
transaction.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
(self,),
@@ -795,13 +822,15 @@ impl ChatId {
})
.await?;
+ context.emit_event(EventType::ChatDeleted { chat_id: self });
context.emit_msgs_changed_without_ids();
- chatlist_events::emit_chatlist_changed(context);
- context
- .set_config_internal(Config::LastHousekeeping, None)
- .await?;
- context.scheduler.interrupt_inbox().await;
+ if let Some(id) = sync_id {
+ self::sync(context, id, SyncAction::Delete)
+ .await
+ .log_err(context)
+ .ok();
+ }
if chat.is_self_talk() {
let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
@@ -809,6 +838,11 @@ impl ChatId {
}
chatlist_events::emit_chatlist_changed(context);
+ context
+ .set_config_internal(Config::LastHousekeeping, None)
+ .await?;
+ context.scheduler.interrupt_inbox().await;
+
Ok(())
}
@@ -890,12 +924,6 @@ impl ChatId {
}
}
_ => {
- let blob = msg
- .param
- .get_blob(Param::File, context)
- .await?
- .context("no file stored in params")?;
- msg.param.set(Param::File, blob.as_name());
if msg.viewtype == Viewtype::File {
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
// We do not do an automatic conversion to other viewtypes here so that
@@ -908,6 +936,10 @@ impl ChatId {
}
}
if msg.viewtype == Viewtype::Vcard {
+ let blob = msg
+ .param
+ .get_file_blob(context)?
+ .context("no file stored in params")?;
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
}
}
@@ -963,6 +995,7 @@ impl ChatId {
transaction.execute(
"INSERT INTO msgs (
chat_id,
+ rfc724_mid,
from_id,
timestamp,
type,
@@ -972,9 +1005,10 @@ impl ChatId {
param,
hidden,
mime_in_reply_to)
- VALUES (?,?,?,?,?,?,?,?,?,?);",
+ VALUES (?,?,?,?,?,?,?,?,?,?,?);",
(
self,
+ &msg.rfc724_mid,
ContactId::SELF,
time(),
msg.viewtype,
@@ -1050,7 +1084,16 @@ impl ChatId {
Ok(count)
}
- /// Returns timestamp of the latest message in the chat.
+ pub(crate) async fn created_timestamp(self, context: &Context) -> Result {
+ Ok(context
+ .sql
+ .query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self,))
+ .await?
+ .unwrap_or(0))
+ }
+
+ /// Returns timestamp of the latest message in the chat,
+ /// including hidden messages or a draft if there is one.
pub(crate) async fn get_timestamp(self, context: &Context) -> Result> {
let timestamp = context
.sql
@@ -1283,8 +1326,7 @@ impl ChatId {
///
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
pub async fn get_encryption_info(self, context: &Context) -> Result {
- let mut ret_mutual = String::new();
- let mut ret_nopreference = String::new();
+ let mut ret_available = String::new();
let mut ret_reset = String::new();
for contact_id in get_chat_contacts(context, self)
@@ -1300,8 +1342,9 @@ impl ChatId {
.filter(|peerstate| peerstate.peek_key(false).is_some())
.map(|peerstate| peerstate.prefer_encrypt)
{
- Some(EncryptPreference::Mutual) => ret_mutual += &format!("{addr}\n"),
- Some(EncryptPreference::NoPreference) => ret_nopreference += &format!("{addr}\n"),
+ Some(EncryptPreference::Mutual) | Some(EncryptPreference::NoPreference) => {
+ ret_available += &format!("{addr}\n")
+ }
Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"),
};
}
@@ -1313,23 +1356,14 @@ impl ChatId {
ret.push('\n');
ret += &ret_reset;
}
- if !ret_nopreference.is_empty() {
+ if !ret_available.is_empty() {
if !ret.is_empty() {
ret.push('\n');
}
ret += &stock_str::e2e_available(context).await;
ret.push(':');
ret.push('\n');
- ret += &ret_nopreference;
- }
- if !ret_mutual.is_empty() {
- if !ret.is_empty() {
- ret.push('\n');
- }
- ret += &stock_str::e2e_preferred(context).await;
- ret.push(':');
- ret.push('\n');
- ret += &ret_mutual;
+ ret += &ret_available;
}
Ok(ret.trim().to_string())
@@ -1344,41 +1378,10 @@ impl ChatId {
}
pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
- self.set_gossiped_timestamp(context, 0).await
- }
-
- /// Get timestamp of the last gossip sent in the chat.
- /// Zero return value means that gossip was never sent.
- pub async fn get_gossiped_timestamp(self, context: &Context) -> Result {
- let timestamp: Option = context
- .sql
- .query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,))
- .await?;
- Ok(timestamp.unwrap_or_default())
- }
-
- pub(crate) async fn set_gossiped_timestamp(
- self,
- context: &Context,
- timestamp: i64,
- ) -> Result<()> {
- ensure!(
- !self.is_special(),
- "can not set gossiped timestamp for special chats"
- );
- info!(
- context,
- "Set gossiped_timestamp for chat {} to {}.", self, timestamp,
- );
-
context
.sql
- .execute(
- "UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
- (timestamp, self),
- )
+ .execute("DELETE FROM gossip_timestamp WHERE chat_id=?", (self,))
.await?;
-
Ok(())
}
@@ -1714,8 +1717,8 @@ impl Chat {
/// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin.
///
- /// If the timeout has expired, notifies the user that sending messages is possible. See also
- /// [`CantSendReason::SecurejoinWait`].
+ /// If the timeout has expired, adds an info message with additional information.
+ /// See also [`CantSendReason::SecurejoinWait`].
pub(crate) async fn check_securejoin_wait(
&self,
context: &Context,
@@ -1724,16 +1727,19 @@ impl Chat {
if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
return Ok(0);
}
- let (mut param0, mut param1) = (Params::new(), Params::new());
- param0.set_cmd(SystemMessage::SecurejoinWait);
- param1.set_cmd(SystemMessage::SecurejoinWaitTimeout);
- let (param0, param1) = (param0.to_string(), param1.to_string());
+
+ // chat is single and unprotected:
+ // get last info message of type SecurejoinWait or SecurejoinWaitTimeout
+ let (mut param_wait, mut param_timeout) = (Params::new(), Params::new());
+ param_wait.set_cmd(SystemMessage::SecurejoinWait);
+ param_timeout.set_cmd(SystemMessage::SecurejoinWaitTimeout);
+ let (param_wait, param_timeout) = (param_wait.to_string(), param_timeout.to_string());
let Some((param, ts_sort, ts_start)) = context
.sql
.query_row_optional(
"SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\
(SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))",
- (self.id, ¶m0, ¶m1),
+ (self.id, ¶m_wait, ¶m_timeout),
|row| {
let param: String = row.get(0)?;
let ts_sort: i64 = row.get(1)?;
@@ -1745,9 +1751,10 @@ impl Chat {
else {
return Ok(0);
};
- if param == param1 {
+ if param == param_timeout {
return Ok(0);
}
+
let now = time();
// Don't await SecureJoin if the clock was set back.
if ts_start <= now {
@@ -1761,7 +1768,7 @@ impl Chat {
add_info_msg_with_cmd(
context,
self.id,
- &stock_str::securejoin_wait_timeout(context).await,
+ &stock_str::securejoin_takes_longer(context).await,
SystemMessage::SecurejoinWaitTimeout,
// Use the sort timestamp of the "please wait" message, this way the added message is
// never sorted below the protection message if the SecureJoin finishes in parallel.
@@ -1769,6 +1776,7 @@ impl Chat {
Some(now),
None,
None,
+ None,
)
.await?;
context.emit_event(EventType::ChatModified(self.id));
@@ -1877,7 +1885,6 @@ impl Chat {
name: self.name.clone(),
archived: self.visibility == ChatVisibility::Archived,
param: self.param.to_string(),
- gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
is_sending_locations: self.is_sending_locations,
color: self.get_color(context).await?,
profile_image: self
@@ -1968,13 +1975,7 @@ impl Chat {
if let Some(member_list_timestamp) = self.param.get_i64(Param::MemberListTimestamp) {
Ok(member_list_timestamp)
} else {
- let creation_timestamp: i64 = context
- .sql
- .query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self.id,))
- .await
- .context("SQL error querying created_timestamp")?
- .context("Chat not found")?;
- Ok(creation_timestamp)
+ Ok(self.id.created_timestamp(context).await?)
}
}
@@ -2006,7 +2007,9 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
- let new_rfc724_mid = create_outgoing_rfc724_mid();
+ if msg.rfc724_mid.is_empty() {
+ msg.rfc724_mid = create_outgoing_rfc724_mid();
+ }
if self.typ == Chattype::Single {
if let Some(id) = context
@@ -2029,7 +2032,9 @@ impl Chat {
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
{
msg.param.set_int(Param::AttachGroupImage, 1);
- self.param.remove(Param::Unpromoted);
+ self.param
+ .remove(Param::Unpromoted)
+ .set_i64(Param::GroupNameTimestamp, timestamp);
self.update_param(context).await?;
// TODO: Remove this compat code needed because Core <= v1.143:
// - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also
@@ -2102,7 +2107,7 @@ impl Chat {
if references_vec.is_empty() {
// As a fallback, use our Message-ID,
// same as in the case of top-level message.
- new_references = new_rfc724_mid.clone();
+ new_references = msg.rfc724_mid.clone();
} else {
new_references = references_vec.join(" ");
}
@@ -2112,7 +2117,7 @@ impl Chat {
// This allows us to identify replies to our message even if
// email server such as Outlook changes `Message-ID:` header.
// MUAs usually keep the first Message-ID in `References:` header unchanged.
- new_references = new_rfc724_mid.clone();
+ new_references = msg.rfc724_mid.clone();
}
// add independent location to database
@@ -2157,14 +2162,18 @@ impl Chat {
} else {
None
};
- let new_mime_headers = new_mime_headers.map(|s| new_html_mimepart(s).build().as_string());
+ let new_mime_headers: Option = new_mime_headers.map(|s| {
+ let html_part = MimePart::new("text/html", s);
+ let mut buffer = Vec::new();
+ let cursor = Cursor::new(&mut buffer);
+ html_part.write_part(cursor).ok();
+ String::from_utf8_lossy(&buffer).to_string()
+ });
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
// We need to add some headers so that they are stripped before formatting HTML by
// `MsgId::get_html()`, not a part of the actual text. Let's add "Content-Type", it's
// anyway a useful metadata about the stored text.
- true => Some(
- "Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text + "\r\n",
- ),
+ true => Some("Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text),
false => None,
});
let new_mime_headers = match new_mime_headers {
@@ -2176,7 +2185,6 @@ impl Chat {
msg.chat_id = self.id;
msg.from_id = ContactId::SELF;
- msg.rfc724_mid = new_rfc724_mid;
msg.timestamp_sort = timestamp;
// add message to the database
@@ -2298,6 +2306,10 @@ impl Chat {
async fn get_sync_id(&self, context: &Context) -> Result> {
match self.typ {
Chattype::Single => {
+ if self.is_device_talk() {
+ return Ok(Some(SyncId::Device));
+ }
+
let mut r = None;
for contact_id in get_chat_contacts(context, self.id).await? {
if contact_id == ContactId::SELF && !self.is_self_talk() {
@@ -2416,9 +2428,6 @@ pub struct ChatInfo {
/// This is the string-serialised version of `Params` currently.
pub param: String,
- /// Last time this client sent autocrypt gossip headers to this chat.
- pub gossiped_timestamp: i64,
-
/// Whether this chat is currently sending location-stream messages.
pub is_sending_locations: bool,
@@ -2560,19 +2569,27 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
/// Checks if there is a 1:1 chat in-progress SecureJoin for Bob and, if necessary, schedules a task
/// unblocking the chat and notifying the user accordingly.
pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
- let Some(bobstate) = BobState::from_db(&context.sql).await? else {
- return Ok(());
- };
- if !bobstate.in_progress() {
- return Ok(());
- }
- let chat_id = bobstate.alice_chat();
- let chat = Chat::load_from_db(context, chat_id).await?;
- let timeout = chat
- .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
+ let chat_ids: Vec = context
+ .sql
+ .query_map(
+ "SELECT chat_id FROM bobstate",
+ (),
+ |row| {
+ let chat_id: ChatId = row.get(0)?;
+ Ok(chat_id)
+ },
+ |rows| rows.collect::, _>>().map_err(Into::into),
+ )
.await?;
- if timeout > 0 {
- chat_id.spawn_securejoin_wait(context, timeout);
+
+ for chat_id in chat_ids {
+ let chat = Chat::load_from_db(context, chat_id).await?;
+ let timeout = chat
+ .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
+ .await?;
+ if timeout > 0 {
+ chat_id.spawn_securejoin_wait(context, timeout);
+ }
}
Ok(())
}
@@ -2736,8 +2753,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
} else if msg.viewtype.has_file() {
let mut blob = msg
.param
- .get_blob(Param::File, context)
- .await?
+ .get_file_blob(context)?
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
let send_as_is = msg.viewtype == Viewtype::File;
@@ -2785,20 +2801,12 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
.recode_to_image_size(context, msg.get_filename(), &mut maybe_sticker)
.await?;
msg.param.set(Param::Filename, new_name);
+ msg.param.set(Param::File, blob.as_name());
if !maybe_sticker {
msg.viewtype = Viewtype::Image;
}
}
- msg.param.set(Param::File, blob.as_name());
- if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) {
- let stem = match filename.rsplit_once('.') {
- Some((stem, _)) => stem,
- None => filename,
- };
- msg.param
- .set(Param::Filename, stem.to_string() + "." + blob_ext);
- }
if !msg.param.exists(Param::MimeType) {
if let Some((_, mime)) = message::guess_msgtype_from_suffix(msg) {
@@ -2981,6 +2989,12 @@ async fn prepare_send_msg(
///
/// The caller has to interrupt SMTP loop or otherwise process new rows.
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result> {
+ if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
+ msg.chat_id
+ .update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
+ .await?;
+ }
+
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
let mimefactory = MimeFactory::from_msg(context, msg.clone()).await?;
let attach_selfavatar = mimefactory.attach_selfavatar;
@@ -3001,10 +3015,10 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
// disabled by default is fine.
//
// `from` must be the last addr, see `receive_imf_inner()` why.
- if context.get_config_bool(Config::BccSelf).await?
- && !recipients
- .iter()
- .any(|x| x.to_lowercase() == lowercase_from)
+ recipients.retain(|x| x.to_lowercase() != lowercase_from);
+ if (context.get_config_bool(Config::BccSelf).await?
+ || msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage)
+ && (context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty())
{
recipients.push(from);
}
@@ -3020,6 +3034,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
context,
"Message {} has no recipient, skipping smtp-send.", msg.id
);
+ msg.param.set_int(Param::GuaranteeE2ee, 1);
+ msg.update_param(context).await?;
msg.id.set_delivered(context).await?;
msg.state = MessageState::OutDelivered;
return Ok(Vec::new());
@@ -3050,10 +3066,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
let now = smeared_time(context);
- if rendered_msg.is_gossiped {
- msg.chat_id.set_gossiped_timestamp(context, now).await?;
- }
-
if rendered_msg.last_added_location_id.is_some() {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
@@ -3124,6 +3136,66 @@ pub async fn send_text_msg(
send_msg(context, chat_id, &mut msg).await
}
+/// Sends chat members a request to edit the given message's text.
+pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: String) -> Result<()> {
+ let mut original_msg = Message::load_from_db(context, msg_id).await?;
+ ensure!(
+ original_msg.from_id == ContactId::SELF,
+ "Can edit only own messages"
+ );
+ ensure!(!original_msg.is_info(), "Cannot edit info messages");
+ ensure!(!original_msg.has_html(), "Cannot edit HTML messages");
+ ensure!(
+ original_msg.viewtype != Viewtype::VideochatInvitation,
+ "Cannot edit videochat invitations"
+ );
+ ensure!(
+ !original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings
+ "Cannot add text"
+ );
+ ensure!(!new_text.trim().is_empty(), "Edited text cannot be empty");
+ if original_msg.text == new_text {
+ info!(context, "Text unchanged.");
+ return Ok(());
+ }
+
+ save_text_edit_to_db(context, &mut original_msg, &new_text).await?;
+
+ let mut edit_msg = Message::new_text(EDITED_PREFIX.to_owned() + &new_text); // prefix only set for nicer display in Non-Delta-MUAs
+ edit_msg.set_quote(context, Some(&original_msg)).await?; // quote only set for nicer display in Non-Delta-MUAs
+ if original_msg.get_showpadlock() {
+ edit_msg.param.set_int(Param::GuaranteeE2ee, 1);
+ }
+ edit_msg
+ .param
+ .set(Param::TextEditFor, original_msg.rfc724_mid);
+ edit_msg.hidden = true;
+ send_msg(context, original_msg.chat_id, &mut edit_msg).await?;
+ Ok(())
+}
+
+pub(crate) async fn save_text_edit_to_db(
+ context: &Context,
+ original_msg: &mut Message,
+ new_text: &str,
+) -> Result<()> {
+ original_msg.param.set_int(Param::IsEdited, 1);
+ context
+ .sql
+ .execute(
+ "UPDATE msgs SET txt=?, txt_normalized=?, param=? WHERE id=?",
+ (
+ new_text,
+ message::normalize_text(new_text),
+ original_msg.param.to_string(),
+ original_msg.id,
+ ),
+ )
+ .await?;
+ context.emit_msgs_changed(original_msg.chat_id, original_msg.id);
+ Ok(())
+}
+
/// Sends invitation to a videochat.
pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Result {
ensure!(
@@ -3328,7 +3400,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
} else {
start_chat_ephemeral_timers(context, chat_id).await?;
- if context
+ let noticed_msgs_count = context
.sql
.execute(
"UPDATE msgs
@@ -3338,9 +3410,36 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
AND chat_id=?;",
(MessageState::InNoticed, MessageState::InFresh, chat_id),
)
- .await?
- == 0
- {
+ .await?;
+
+ // This is to trigger emitting `MsgsNoticed` on other devices when reactions are noticed
+ // locally (i.e. when the chat was opened locally).
+ let hidden_messages = context
+ .sql
+ .query_map(
+ "SELECT id, rfc724_mid FROM msgs
+ WHERE state=?
+ AND hidden=1
+ AND chat_id=?
+ ORDER BY id LIMIT 100", // LIMIT to 100 in order to avoid blocking the UI too long, usually there will be less than 100 messages anyway
+ (MessageState::InFresh, chat_id), // No need to check for InNoticed messages, because reactions are never InNoticed
+ |row| {
+ let msg_id: MsgId = row.get(0)?;
+ let rfc724_mid: String = row.get(1)?;
+ Ok((msg_id, rfc724_mid))
+ },
+ |rows| {
+ rows.collect::, _>>()
+ .map_err(Into::into)
+ },
+ )
+ .await?;
+ for (msg_id, rfc724_mid) in &hidden_messages {
+ message::update_msg_state(context, *msg_id, MessageState::InSeen).await?;
+ imap::markseen_on_imap_table(context, rfc724_mid).await?;
+ }
+
+ if noticed_msgs_count == 0 {
return Ok(());
}
}
@@ -3435,37 +3534,62 @@ pub async fn get_chat_media(
msg_type2: Viewtype,
msg_type3: Viewtype,
) -> Result> {
- // TODO This query could/should be converted to `AND type IN (?, ?, ?)`.
- let list = context
- .sql
- .query_map(
- "SELECT id
+ let list = if msg_type == Viewtype::Webxdc
+ && msg_type2 == Viewtype::Unknown
+ && msg_type3 == Viewtype::Unknown
+ {
+ context
+ .sql
+ .query_map(
+ "SELECT id
FROM msgs
WHERE (1=? OR chat_id=?)
AND chat_id != ?
- AND (type=? OR type=? OR type=?)
+ AND type = ?
+ AND hidden=0
+ ORDER BY max(timestamp, timestamp_rcvd), id;",
+ (
+ chat_id.is_none(),
+ chat_id.unwrap_or_else(|| ChatId::new(0)),
+ DC_CHAT_ID_TRASH,
+ Viewtype::Webxdc,
+ ),
+ |row| row.get::<_, MsgId>(0),
+ |ids| Ok(ids.flatten().collect()),
+ )
+ .await?
+ } else {
+ context
+ .sql
+ .query_map(
+ "SELECT id
+ FROM msgs
+ WHERE (1=? OR chat_id=?)
+ AND chat_id != ?
+ AND type IN (?, ?, ?)
AND hidden=0
ORDER BY timestamp, id;",
- (
- chat_id.is_none(),
- chat_id.unwrap_or_else(|| ChatId::new(0)),
- DC_CHAT_ID_TRASH,
- msg_type,
- if msg_type2 != Viewtype::Unknown {
- msg_type2
- } else {
- msg_type
- },
- if msg_type3 != Viewtype::Unknown {
- msg_type3
- } else {
- msg_type
- },
- ),
- |row| row.get::<_, MsgId>(0),
- |ids| Ok(ids.flatten().collect()),
- )
- .await?;
+ (
+ chat_id.is_none(),
+ chat_id.unwrap_or_else(|| ChatId::new(0)),
+ DC_CHAT_ID_TRASH,
+ msg_type,
+ if msg_type2 != Viewtype::Unknown {
+ msg_type2
+ } else {
+ msg_type
+ },
+ if msg_type3 != Viewtype::Unknown {
+ msg_type3
+ } else {
+ msg_type
+ },
+ ),
+ |row| row.get::<_, MsgId>(0),
+ |ids| Ok(ids.flatten().collect()),
+ )
+ .await?
+ };
Ok(list)
}
@@ -3493,6 +3617,8 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result Result> {
let now = time();
let list = context
@@ -3505,7 +3631,7 @@ pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Resul
WHERE cc.chat_id=?
AND cc.add_timestamp < cc.remove_timestamp
AND ? < cc.remove_timestamp
- ORDER BY c.id=1, c.last_seen DESC, c.id DESC",
+ ORDER BY c.id=1, cc.remove_timestamp DESC, c.id DESC",
(chat_id, now.saturating_sub(60 * 24 * 3600)),
|row| row.get::<_, ContactId>(0),
|ids| ids.collect::, _>>().map_err(Into::into),
@@ -3748,7 +3874,7 @@ pub(crate) async fn add_contact_to_chat_ex(
) -> Result {
ensure!(!chat_id.is_special(), "can not add member to special chats");
let contact = Contact::get_by_id(context, contact_id).await?;
- let mut msg = Message::default();
+ let mut msg = Message::new(Viewtype::default());
chat_id.reset_gossiped_timestamp(context).await?;
@@ -3779,7 +3905,9 @@ pub(crate) async fn add_contact_to_chat_ex(
let sync_qr_code_tokens;
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
- chat.param.remove(Param::Unpromoted);
+ chat.param
+ .remove(Param::Unpromoted)
+ .set_i64(Param::GroupNameTimestamp, smeared_time(context));
chat.update_param(context).await?;
sync_qr_code_tokens = true;
} else {
@@ -3822,6 +3950,8 @@ pub(crate) async fn add_contact_to_chat_ex(
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
+ msg.param
+ .set_int(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
send_msg(context, chat_id, &mut msg).await?;
sync = Nosync;
@@ -3975,7 +4105,7 @@ pub async fn remove_contact_from_chat(
"Cannot remove special contact"
);
- let mut msg = Message::default();
+ let mut msg = Message::new(Viewtype::default());
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
@@ -4019,6 +4149,8 @@ pub async fn remove_contact_from_chat(
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
+ msg.param
+ .set(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
let res = send_msg(context, chat_id, &mut msg).await;
if contact_id == ContactId::SELF {
res?;
@@ -4080,7 +4212,7 @@ async fn rename_ex(
ensure!(!chat_id.is_special(), "Invalid chat ID");
let chat = Chat::load_from_db(context, chat_id).await?;
- let mut msg = Message::default();
+ let mut msg = Message::new(Viewtype::default());
if chat.typ == Chattype::Group
|| chat.typ == Chattype::Mailinglist
@@ -4236,15 +4368,18 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.param.remove(Param::WebxdcDocumentTimestamp);
msg.param.remove(Param::WebxdcSummary);
msg.param.remove(Param::WebxdcSummaryTimestamp);
+ msg.param.remove(Param::IsEdited);
msg.in_reply_to = None;
// do not leak data as group names; a default subject is generated by mimefactory
msg.subject = "".to_string();
msg.state = MessageState::OutPending;
+ msg.rfc724_mid = create_outgoing_rfc724_mid();
let new_msg_id = chat
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
.await?;
+
curr_timestamp += 1;
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
@@ -4270,7 +4405,7 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
})
.await?;
}
- context.send_sync_msg().await?;
+ context.scheduler.interrupt_inbox().await;
Ok(())
}
@@ -4296,7 +4431,7 @@ pub(crate) async fn save_copy_in_self_talk(
bail!("message already saved.");
}
- let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, txt_raw, \
+ let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, \
mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
let row_id = context
.sql
@@ -4490,17 +4625,7 @@ pub async fn add_device_msg_with_importance(
// makes sure, the added message is the last one,
// even if the date is wrong (useful esp. when warning about bad dates)
let mut timestamp_sort = timestamp_sent;
- if let Some(last_msg_time) = context
- .sql
- .query_get_value(
- "SELECT MAX(timestamp)
- FROM msgs
- WHERE chat_id=?
- HAVING COUNT(*) > 0",
- (chat_id,),
- )
- .await?
- {
+ if let Some(last_msg_time) = chat_id.get_timestamp(context).await? {
if timestamp_sort <= last_msg_time {
timestamp_sort = last_msg_time + 1;
}
@@ -4627,13 +4752,17 @@ pub(crate) async fn add_info_msg_with_cmd(
timestamp_sent_rcvd: Option,
parent: Option<&Message>,
from_id: Option,
+ added_removed_id: Option,
) -> Result {
let rfc724_mid = create_outgoing_rfc724_mid();
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
let mut param = Params::new();
if cmd != SystemMessage::Unknown {
- param.set_cmd(cmd)
+ param.set_cmd(cmd);
+ }
+ if let Some(contact_id) = added_removed_id {
+ param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
}
let row_id =
@@ -4681,6 +4810,7 @@ pub(crate) async fn add_info_msg(
None,
None,
None,
+ None,
)
.await
}
@@ -4748,6 +4878,9 @@ pub(crate) enum SyncId {
Grpid(String),
/// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups.
Msgids(Vec),
+
+ // Special id for device chat.
+ Device,
}
/// An action synchronised to other devices.
@@ -4763,6 +4896,7 @@ pub(crate) enum SyncAction {
Rename(String),
/// Set chat contacts by their addresses.
SetContacts(Vec),
+ Delete,
}
impl Context {
@@ -4809,6 +4943,7 @@ impl Context {
ChatId::lookup_by_message(&msg)
.with_context(|| format!("No chat found for Message-IDs {msgids:?}"))?
}
+ SyncId::Device => ChatId::get_for_contact(self, ContactId::DEVICE).await?,
};
match action {
SyncAction::Block => chat_id.block_ex(self, Nosync).await,
@@ -4821,6 +4956,7 @@ impl Context {
}
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
+ SyncAction::Delete => chat_id.delete_ex(self, Nosync).await,
}
}
diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs
index 2b1a2ea95f..2f75d7c00d 100644
--- a/src/chat/chat_tests.rs
+++ b/src/chat/chat_tests.rs
@@ -1,6 +1,7 @@
use super::*;
use crate::chatlist::get_archived_cnt;
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
+use crate::ephemeral::Timer;
use crate::headerdef::HeaderDef;
use crate::imex::{has_backup, imex, ImexMode};
use crate::message::{delete_msgs, MessengerMessage};
@@ -298,13 +299,11 @@ async fn test_member_add_remove() -> Result<()> {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
-
- // Disable encryption so we can inspect raw message contents.
- alice.set_config(Config::E2eeEnabled, Some("0")).await?;
- bob.set_config(Config::E2eeEnabled, Some("0")).await?;
+ let fiona = tcm.fiona().await;
// Create contact for Bob on the Alice side with name "robert".
- let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?;
+ let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
+ alice_bob_contact_id.set_name(&alice, "robert").await?;
// Set Bob authname to "Bob" and send it to Alice.
bob.set_config(Config::Displayname, Some("Bob")).await?;
@@ -324,42 +323,35 @@ async fn test_member_add_remove() -> Result<()> {
// Create and promote a group.
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
- let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
+ let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
- let sent = alice
+ alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
- assert!(sent.payload.contains("Hi! I created a group."));
// Alice adds Bob to the chat.
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
- assert!(sent
- .payload
- .contains("I added member Bob (bob@example.net)."));
+
// Locally set name "robert" should not leak.
assert!(!sent.payload.contains("robert"));
assert_eq!(
sent.load_from_db().await.get_text(),
- "You added member robert (bob@example.net)."
+ "You added member robert."
);
// Alice removes Bob from the chat.
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
- assert!(sent
- .payload
- .contains("I removed member Bob (bob@example.net)."));
assert!(!sent.payload.contains("robert"));
assert_eq!(
sent.load_from_db().await.get_text(),
- "You removed member robert (bob@example.net)."
+ "You removed member robert."
);
// Alice leaves the chat.
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
let sent = alice.pop_sent_msg().await;
- assert!(sent.payload.contains("I left the group."));
assert_eq!(sent.load_from_db().await.get_text(), "You left the group.");
Ok(())
@@ -372,13 +364,12 @@ async fn test_parallel_member_remove() -> Result<()> {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
+ let charlie = tcm.charlie().await;
+ let fiona = tcm.fiona().await;
- alice.set_config(Config::E2eeEnabled, Some("0")).await?;
- bob.set_config(Config::E2eeEnabled, Some("0")).await?;
-
- let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
- let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
- let alice_claire_contact_id = Contact::create(&alice, "Claire", "claire@example.net").await?;
+ let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
+ let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
+ let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await;
// Create and promote a group.
let alice_chat_id =
@@ -393,8 +384,8 @@ async fn test_parallel_member_remove() -> Result<()> {
let bob_chat_id = bob_received_msg.get_chat_id();
bob_chat_id.accept(&bob).await?;
- // Alice adds Claire to the chat.
- add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
+ // Alice adds Charlie to the chat.
+ add_contact_to_chat(&alice, alice_chat_id, alice_charlie_contact_id).await?;
let alice_sent_add_msg = alice.pop_sent_msg().await;
// Bob leaves the chat.
@@ -423,7 +414,7 @@ async fn test_parallel_member_remove() -> Result<()> {
// Test that remove message is rewritten.
assert_eq!(
bob_received_remove_msg.get_text(),
- "Member Me (bob@example.net) removed by alice@example.org."
+ "Member Me removed by alice@example.org."
);
Ok(())
@@ -435,11 +426,10 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
- let alice_bob_contact_id =
- Contact::create(&alice, "Bob", &bob.get_config(Config::Addr).await?.unwrap()).await?;
- let fiona_addr = "fiona@example.net";
- let alice_fiona_contact_id = Contact::create(&alice, "Fiona", fiona_addr).await?;
- let bob_fiona_contact_id = Contact::create(&bob, "Fiona", fiona_addr).await?;
+ let fiona = tcm.fiona().await;
+ let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
+ let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
+ let bob_fiona_contact_id = bob.add_or_lookup_contact_id(&fiona).await;
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
@@ -481,8 +471,9 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_modify_chat_multi_device() -> Result<()> {
- let a1 = TestContext::new_alice().await;
- let a2 = TestContext::new_alice().await;
+ let mut tcm = TestContextManager::new();
+ let a1 = tcm.alice().await;
+ let a2 = tcm.alice().await;
a1.set_config_bool(Config::BccSelf, true).await?;
// create group and sync it to the second device
@@ -506,8 +497,9 @@ async fn test_modify_chat_multi_device() -> Result<()> {
assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1);
// add a member to the group
- let bob = Contact::create(&a1, "", "bob@example.org").await?;
- add_contact_to_chat(&a1, a1_chat_id, bob).await?;
+ let bob = tcm.bob().await;
+ let bob_id = a1.add_or_lookup_contact_id(&bob).await;
+ add_contact_to_chat(&a1, a1_chat_id, bob_id).await?;
let a1_msg = a1.get_last_msg().await;
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
@@ -531,11 +523,19 @@ async fn test_modify_chat_multi_device() -> Result<()> {
assert!(a2_msg.is_system_message());
assert_eq!(a1_msg.get_info_type(), SystemMessage::GroupNameChanged);
assert_eq!(a2_msg.get_info_type(), SystemMessage::GroupNameChanged);
+ assert_eq!(
+ a1_msg.get_info_contact_id(&a1).await?,
+ Some(ContactId::SELF)
+ );
+ assert_eq!(
+ a2_msg.get_info_contact_id(&a2).await?,
+ Some(ContactId::SELF)
+ );
assert_eq!(Chat::load_from_db(&a1, a1_chat_id).await?.name, "bar");
assert_eq!(Chat::load_from_db(&a2, a2_chat_id).await?.name, "bar");
// remove member from group
- remove_contact_from_chat(&a1, a1_chat_id, bob).await?;
+ remove_contact_from_chat(&a1, a1_chat_id, bob_id).await?;
let a1_msg = a1.get_last_msg().await;
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
@@ -562,13 +562,18 @@ async fn test_modify_chat_multi_device() -> Result<()> {
async fn test_modify_chat_disordered() -> Result<()> {
let _n = TimeShiftFalsePositiveNote;
- // Alice creates a group with Bob, Claire and Daisy and then removes Claire and Daisy
+ let mut tcm = TestContextManager::new();
+
+ // Alice creates a group with Bob, Charlie and Fiona and then removes Charlie and Fiona
// (time shift is needed as otherwise smeared time from Alice looks to Bob like messages from the future which are all set to "now" then)
- let alice = TestContext::new_alice().await;
+ let alice = tcm.alice().await;
- let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
- let claire_id = Contact::create(&alice, "", "claire@foo.de").await?;
- let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?;
+ let bob = tcm.bob().await;
+ let bob_id = alice.add_or_lookup_contact_id(&bob).await;
+ let charlie = tcm.charlie().await;
+ let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
+ let fiona = tcm.fiona().await;
+ let fiona_id = alice.add_or_lookup_contact_id(&fiona).await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
@@ -576,21 +581,21 @@ async fn test_modify_chat_disordered() -> Result<()> {
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
let add1 = alice.pop_sent_msg().await;
- add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
+ add_contact_to_chat(&alice, alice_chat_id, charlie_id).await?;
let add2 = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_millis(1100));
- add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?;
+ add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?;
let add3 = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_millis(1100));
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
- remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
+ remove_contact_from_chat(&alice, alice_chat_id, charlie_id).await?;
let remove1 = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_millis(1100));
- remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
+ remove_contact_from_chat(&alice, alice_chat_id, fiona_id).await?;
let remove2 = alice.pop_sent_msg().await;
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
@@ -641,24 +646,27 @@ async fn test_modify_chat_lost() -> 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@foo.de").await?;
- let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?;
+ let bob = tcm.bob().await;
+ let bob_id = alice.add_or_lookup_contact_id(&bob).await;
+ let charlie = tcm.charlie().await;
+ let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
+ let fiona = tcm.fiona().await;
+ let fiona_id = alice.add_or_lookup_contact_id(&fiona).await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
- add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
- add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?;
+ add_contact_to_chat(&alice, alice_chat_id, charlie_id).await?;
+ add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let add = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_millis(1100));
- remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
+ remove_contact_from_chat(&alice, alice_chat_id, charlie_id).await?;
let remove1 = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_millis(1100));
- remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
+ remove_contact_from_chat(&alice, alice_chat_id, fiona_id).await?;
let remove2 = alice.pop_sent_msg().await;
let bob = tcm.bob().await;
@@ -681,12 +689,13 @@ async fn test_modify_chat_lost() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_leave_group() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
+ let mut tcm = TestContextManager::new();
+ let alice = tcm.alice().await;
+ let bob = tcm.bob().await;
// Create group chat with Bob.
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
- let bob_contact = Contact::create(&alice, "", "bob@example.net").await?;
+ let bob_contact = alice.add_or_lookup_contact(&bob).await.id;
add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?;
// Alice sends first message to group.
@@ -695,16 +704,45 @@ async fn test_leave_group() -> Result<()> {
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
+ // Clear events so that we can later check
+ // that the 'Group left' message didn't trigger IncomingMsg:
+ alice.evtracker.clear_events();
+
+ // Shift the time so that we can later check the 'Group left' message's timestamp:
+ SystemTime::shift(Duration::from_secs(60));
+
// Bob leaves the group.
let bob_chat_id = bob_msg.chat_id;
bob_chat_id.accept(&bob).await?;
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
let leave_msg = bob.pop_sent_msg().await;
- alice.recv_msg(&leave_msg).await;
+ let rcvd_leave_msg = alice.recv_msg(&leave_msg).await;
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 1);
+ assert_eq!(rcvd_leave_msg.state, MessageState::InSeen);
+
+ alice.emit_event(EventType::Test);
+ alice
+ .evtracker
+ .get_matching(|ev| match ev {
+ EventType::Test => true,
+ EventType::IncomingMsg { .. } => panic!("'Group left' message should be silent"),
+ EventType::MsgsNoticed(..) => {
+ panic!("'Group left' message shouldn't clear notifications")
+ }
+ _ => false,
+ })
+ .await;
+
+ // The 'Group left' message timestamp should be the same as the previous message in the chat
+ // so that the chat is not popped up in the chatlist:
+ assert_eq!(
+ sent_msg.load_from_db().await.timestamp_sort,
+ rcvd_leave_msg.timestamp_sort
+ );
+
Ok(())
}
@@ -774,6 +812,21 @@ async fn test_self_talk() -> Result<()> {
Ok(())
}
+/// Tests that when BCC-self is disabled
+/// and no messages are actually sent
+/// in a self-chat, they have a padlock.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_self_talk_no_bcc_padlock() -> Result<()> {
+ let t = &TestContext::new_alice().await;
+ t.set_config_bool(Config::BccSelf, false).await?;
+ let chat = &t.get_self_chat().await;
+
+ let msg_id = send_text_msg(t, chat.id, "Foobar".to_string()).await?;
+ let msg = Message::load_from_db(t, msg_id).await?;
+ assert!(msg.get_showpadlock());
+ Ok(())
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_device_msg_unlabelled() {
let t = TestContext::new().await;
@@ -1355,20 +1408,52 @@ async fn test_pinned_after_new_msgs() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_chat_name() {
- let t = TestContext::new().await;
- let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+
+ let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
assert_eq!(
- Chat::load_from_db(&t, chat_id).await.unwrap().get_name(),
+ Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
"foo"
);
- set_chat_name(&t, chat_id, "bar").await.unwrap();
+ set_chat_name(alice, chat_id, "bar").await.unwrap();
assert_eq!(
- Chat::load_from_db(&t, chat_id).await.unwrap().get_name(),
+ Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
"bar"
);
+
+ let bob = &tcm.bob().await;
+ let bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
+ add_contact_to_chat(alice, chat_id, bob_contact_id)
+ .await
+ .unwrap();
+
+ let sent_msg = alice.send_text(chat_id, "Hi").await;
+ let received_msg = bob.recv_msg(&sent_msg).await;
+ let bob_chat_id = received_msg.chat_id;
+
+ for new_name in [
+ "Baz",
+ "xyzzy",
+ "Quux",
+ "another name",
+ "something different",
+ ] {
+ set_chat_name(alice, chat_id, new_name).await.unwrap();
+ let sent_msg = alice.pop_sent_msg().await;
+ let received_msg = bob.recv_msg(&sent_msg).await;
+ assert_eq!(received_msg.chat_id, bob_chat_id);
+ assert_eq!(
+ Chat::load_from_db(bob, bob_chat_id)
+ .await
+ .unwrap()
+ .get_name(),
+ new_name
+ );
+ }
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1418,6 +1503,39 @@ async fn test_shall_attach_selfavatar() -> Result<()> {
Ok(())
}
+/// Tests that profile data is attached to group leave messages. There are some pros and cons of
+/// doing this, but at least we don't want to complicate the code with this special case.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_profile_data_on_group_leave() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let t = &tcm.alice().await;
+ let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "foo").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?;
+
+ send_text_msg(t, chat_id, "populate".to_string()).await?;
+ t.pop_sent_msg().await;
+
+ let file = t.dir.path().join("avatar.png");
+ let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
+ tokio::fs::write(&file, bytes).await?;
+ t.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
+ .await?;
+ assert!(shall_attach_selfavatar(t, chat_id).await?);
+
+ remove_contact_from_chat(t, chat_id, ContactId::SELF).await?;
+ let sent_msg = t.pop_sent_msg().await;
+ assert!(sent_msg.payload().contains("Chat-User-Avatar"));
+ Ok(())
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_mute_duration() {
let t = TestContext::new().await;
@@ -1499,6 +1617,7 @@ async fn test_add_info_msg_with_cmd() -> Result<()> {
None,
None,
None,
+ None,
)
.await?;
@@ -1578,58 +1697,6 @@ async fn test_lookup_self_by_contact_id() {
assert_eq!(chat.blocked, Blocked::Not);
}
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_group_with_removed_message_id() -> Result<()> {
- // Alice creates a group with Bob, sends a message to bob
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().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?;
-
- add_contact_to_chat(&alice, alice_chat_id, contact_id).await?;
- assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
- send_text_msg(&alice, alice_chat_id, "hi!".to_string()).await?;
- assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 1);
-
- // Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
- let sent_msg = alice.pop_sent_msg().await;
- let msg = sent_msg.payload();
- assert_eq!(msg.match_indices("Message-ID: <").count(), 2);
- assert_eq!(msg.match_indices("References: <").count(), 1);
- let msg = msg.replace("Message-ID: <", "Message-ID: Result<()> {
let t = TestContext::new_alice().await;
@@ -1839,10 +1906,6 @@ async fn test_sticker(
msg.set_file_and_deduplicate(&alice, &file, Some(filename), None)?;
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
- let mime = sent_msg.payload();
- if res_viewtype == Viewtype::Sticker {
- assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
- }
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, bob_chat.id);
@@ -1977,20 +2040,28 @@ async fn test_sticker_forward() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_forward() -> Result<()> {
+async fn test_forward_basic() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
- let mut msg = Message::new_text("Hi Bob".to_owned());
- let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
+ let mut alice_msg = Message::new_text("Hi Bob".to_owned());
+ let sent_msg = alice.send_msg(alice_chat.get_id(), &mut alice_msg).await;
let msg = bob.recv_msg(&sent_msg).await;
+ assert_eq!(alice_msg.rfc724_mid, msg.rfc724_mid);
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
let forwarded_msg = bob.pop_sent_msg().await;
+ assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, 2);
+ assert_ne!(
+ forwarded_msg.load_from_db().await.rfc724_mid,
+ msg.rfc724_mid,
+ );
+ let msg_bob = Message::load_from_db(&bob, forwarded_msg.sender_msg_id).await?;
let msg = alice.recv_msg(&forwarded_msg).await;
+ assert_eq!(msg.rfc724_mid(), msg_bob.rfc724_mid());
assert_eq!(msg.get_text(), "Hi Bob");
assert!(msg.is_forwarded());
Ok(())
@@ -1998,20 +2069,22 @@ async fn test_forward() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_info_msg() -> Result<()> {
- let t = TestContext::new_alice().await;
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
- let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a").await?;
- send_text_msg(&t, chat_id1, "msg one".to_string()).await?;
- let bob_id = Contact::create(&t, "", "bob@example.net").await?;
- add_contact_to_chat(&t, chat_id1, bob_id).await?;
- let msg1 = t.get_last_msg_in(chat_id1).await;
+ let chat_id1 = create_group_chat(alice, ProtectionStatus::Unprotected, "a").await?;
+ send_text_msg(alice, chat_id1, "msg one".to_string()).await?;
+ let bob_id = alice.add_or_lookup_contact_id(bob).await;
+ add_contact_to_chat(alice, chat_id1, bob_id).await?;
+ let msg1 = alice.get_last_msg_in(chat_id1).await;
assert!(msg1.is_info());
assert!(msg1.get_text().contains("bob@example.net"));
- let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?;
- assert_eq!(get_chat_msgs(&t, chat_id2).await?.len(), 0);
- forward_msgs(&t, &[msg1.id], chat_id2).await?;
- let msg2 = t.get_last_msg_in(chat_id2).await;
+ let chat_id2 = ChatId::create_for_contact(alice, bob_id).await?;
+ assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), 0);
+ forward_msgs(alice, &[msg1.id], chat_id2).await?;
+ let msg2 = alice.get_last_msg_in(chat_id2).await;
assert!(!msg2.is_info()); // forwarded info-messages lose their info-state
assert_eq!(msg2.get_info_type(), SystemMessage::Unknown);
assert_ne!(msg2.from_id, ContactId::INFO);
@@ -2058,8 +2131,10 @@ async fn test_forward_quote() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_group() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
+ let mut tcm = TestContextManager::new();
+ let alice = tcm.alice().await;
+ let bob = tcm.bob().await;
+ let charlie = tcm.charlie().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
@@ -2067,12 +2142,12 @@ async fn test_forward_group() -> Result<()> {
// Alice creates a group with Bob.
let alice_group_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
- let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
- let claire_id = Contact::create(&alice, "Claire", "claire@example.net").await?;
+ let bob_id = alice.add_or_lookup_contact_id(&bob).await;
+ let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?;
- add_contact_to_chat(&alice, alice_group_chat_id, claire_id).await?;
+ add_contact_to_chat(&alice, alice_group_chat_id, charlie_id).await?;
let sent_group_msg = alice
- .send_text(alice_group_chat_id, "Hi Bob and Claire")
+ .send_text(alice_group_chat_id, "Hi Bob and Charlie")
.await;
let bob_group_chat_id = bob.recv_msg(&sent_group_msg).await.chat_id;
@@ -2260,6 +2335,29 @@ async fn test_forward_from_saved_to_saved() -> Result<()> {
Ok(())
}
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_forward_encrypted_to_unencrypted() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let charlie = &tcm.charlie().await;
+
+ let txt = "This should be encrypted";
+ let sent = alice.send_text(alice.create_chat(bob).await.id, txt).await;
+ let msg = bob.recv_msg(&sent).await;
+ assert_eq!(msg.text, txt);
+ assert!(msg.get_showpadlock());
+
+ let unencrypted_chat = bob.create_email_chat(charlie).await;
+ forward_msgs(bob, &[msg.id], unencrypted_chat.id).await?;
+ let msg2 = bob.get_last_msg().await;
+ assert_eq!(msg2.text, txt);
+ assert_ne!(msg.id, msg2.id);
+ assert!(!msg2.get_showpadlock());
+
+ Ok(())
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_save_from_saved_to_saved_failing() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -2281,11 +2379,13 @@ async fn test_save_from_saved_to_saved_failing() -> Result<()> {
async fn test_resend_own_message() -> Result<()> {
// Alice creates group with Bob and sends an initial message
let alice = TestContext::new_alice().await;
+ let bob = TestContext::new_bob().await;
+ let fiona = TestContext::new_fiona().await;
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
add_contact_to_chat(
&alice,
alice_grp,
- Contact::create(&alice, "", "bob@example.net").await?,
+ alice.add_or_lookup_contact_id(&bob).await,
)
.await?;
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
@@ -2294,7 +2394,7 @@ async fn test_resend_own_message() -> Result<()> {
add_contact_to_chat(
&alice,
alice_grp,
- Contact::create(&alice, "", "claire@example.org").await?,
+ alice.add_or_lookup_contact_id(&fiona).await,
)
.await?;
let sent2 = alice.pop_sent_msg().await;
@@ -2338,15 +2438,13 @@ async fn test_resend_own_message() -> Result<()> {
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
- // Claire does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
- let claire = TestContext::new().await;
- claire.configure_addr("claire@example.org").await;
- claire.recv_msg(&sent2).await;
- let msg = claire.recv_msg(&sent3).await;
+ // Fiona does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
+ fiona.recv_msg(&sent2).await;
+ let msg = fiona.recv_msg(&sent3).await;
assert_eq!(msg.get_text(), "alice->bob");
- assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
- assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
- let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
+ assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3);
+ assert_eq!(get_chat_msgs(&fiona, msg.chat_id).await?.len(), 2);
+ let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?;
assert_eq!(msg_from.get_addr(), "alice@example.org");
assert!(sent1_ts_sent < msg.timestamp_sent);
@@ -2355,19 +2453,15 @@ async fn test_resend_own_message() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_foreign_message_fails() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
- add_contact_to_chat(
- &alice,
- alice_grp,
- Contact::create(&alice, "", "bob@example.net").await?,
- )
- .await?;
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
+ add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
- let bob = TestContext::new_bob().await;
let msg = bob.recv_msg(&sent1).await;
- assert!(resend_msgs(&bob, &[msg.id]).await.is_err());
+ assert!(resend_msgs(bob, &[msg.id]).await.is_err());
Ok(())
}
@@ -2411,24 +2505,23 @@ async fn test_resend_opportunistically_encryption() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_info_message_fails() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
- add_contact_to_chat(
- &alice,
- alice_grp,
- Contact::create(&alice, "", "bob@example.net").await?,
- )
- .await?;
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let charlie = &tcm.charlie().await;
+
+ let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
+ add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
alice.send_text(alice_grp, "alice->bob").await;
add_contact_to_chat(
- &alice,
+ alice,
alice_grp,
- Contact::create(&alice, "", "claire@example.org").await?,
+ alice.add_or_lookup_contact_id(charlie).await,
)
.await?;
let sent2 = alice.pop_sent_msg().await;
- assert!(resend_msgs(&alice, &[sent2.sender_msg_id]).await.is_err());
+ assert!(resend_msgs(alice, &[sent2.sender_msg_id]).await.is_err());
Ok(())
}
@@ -2464,6 +2557,7 @@ async fn test_broadcast() -> Result<()> {
// create two context, send two messages so both know the other
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
+ let fiona = TestContext::new_fiona().await;
let chat_alice = alice.create_chat(&bob).await;
send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
@@ -2482,6 +2576,8 @@ async fn test_broadcast() -> Result<()> {
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
)
.await?;
+ let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
+ add_contact_to_chat(&alice, broadcast_id, fiona_contact_id).await?;
set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
{
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
@@ -2496,11 +2592,14 @@ async fn test_broadcast() -> Result<()> {
{
let sent_msg = alice.pop_sent_msg().await;
- assert!(!sent_msg.payload.contains("Chat-Group-Member-Timestamps:"));
+ let msg = bob.parse_msg(&sent_msg).await;
+ assert!(msg.was_encrypted());
+ assert!(!msg.header_exists(HeaderDef::ChatGroupMemberTimestamps));
+ assert!(!msg.header_exists(HeaderDef::AutocryptGossip));
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_text(), "ola!");
assert_eq!(msg.subject, "Broadcast list");
- assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
+ assert!(msg.get_showpadlock());
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Mailinglist);
assert_ne!(chat.id, chat_bob.id);
@@ -2637,11 +2736,10 @@ async fn test_chat_get_encryption_info() -> Result<()> {
"No encryption:\n\
fiona@example.net\n\
\n\
- End-to-end encryption preferred:\n\
+ End-to-end encryption available:\n\
bob@example.net"
);
- bob.set_config(Config::E2eeEnabled, Some("0")).await?;
send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?;
alice.recv_msg(&bob.pop_sent_msg().await).await;
@@ -2826,17 +2924,67 @@ async fn test_get_chat_media() -> Result<()> {
Ok(())
}
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_get_chat_media_webxdc_order() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = tcm.alice().await;
+ let bob = tcm.bob().await;
+ let chat = alice.create_chat(&bob).await;
+
+ let mut instance1 = Message::new(Viewtype::Webxdc);
+ instance1.set_file_from_bytes(
+ &alice,
+ "test1.xdc",
+ include_bytes!("../../test-data/webxdc/minimal.xdc"),
+ None,
+ )?;
+ let instance1_id = send_msg(&alice, chat.id, &mut instance1).await?;
+
+ let mut instance2 = Message::new(Viewtype::Webxdc);
+ instance2.set_file_from_bytes(
+ &alice,
+ "test2.xdc",
+ include_bytes!("../../test-data/webxdc/minimal.xdc"),
+ None,
+ )?;
+ let instance2_id = send_msg(&alice, chat.id, &mut instance2).await?;
+
+ // list is ordered oldest to newest, check that
+ let media = get_chat_media(
+ &alice,
+ Some(chat.id),
+ Viewtype::Webxdc,
+ Viewtype::Unknown,
+ Viewtype::Unknown,
+ )
+ .await?;
+ assert_eq!(media.first().unwrap(), &instance1_id);
+ assert_eq!(media.get(1).unwrap(), &instance2_id);
+
+ // add a status update for the oder instance; that resorts the list
+ alice
+ .send_webxdc_status_update(instance1_id, r#"{"payload": {"foo": "bar"}}"#)
+ .await?;
+ let media = get_chat_media(
+ &alice,
+ Some(chat.id),
+ Viewtype::Webxdc,
+ Viewtype::Unknown,
+ Viewtype::Unknown,
+ )
+ .await?;
+ assert_eq!(media.first().unwrap(), &instance2_id);
+ assert_eq!(media.get(1).unwrap(), &instance1_id);
+
+ Ok(())
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_blob_renaming() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
- add_contact_to_chat(
- &alice,
- chat_id,
- Contact::create(&alice, "bob", "bob@example.net").await?,
- )
- .await?;
+ add_contact_to_chat(&alice, chat_id, alice.add_or_lookup_contact_id(&bob).await).await?;
let file = alice.get_blobdir().join("harmless_file.\u{202e}txt.exe");
fs::write(&file, "aaa").await?;
let mut msg = Message::new(Viewtype::File);
@@ -2868,7 +3016,7 @@ async fn test_sync_blocked() -> Result<()> {
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
alice1.recv_msg(&sent_msg).await;
- let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id;
+ let a0b_contact_id = alice0.add_or_lookup_contact_id(&bob).await;
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(alice0).await?;
@@ -2923,26 +3071,36 @@ async fn test_sync_blocked() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_accept_before_first_msg() -> Result<()> {
- let alice0 = &TestContext::new_alice().await;
- let alice1 = &TestContext::new_alice().await;
+ let mut tcm = TestContextManager::new();
+ let alice0 = &tcm.alice().await;
+ let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
- let bob = TestContext::new_bob().await;
+ let bob = &tcm.bob().await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
- let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
- assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
+ let rcvd_msg = alice0.recv_msg(&sent_msg).await;
+ let a0b_chat_id = rcvd_msg.chat_id;
+ let a0b_contact_id = rcvd_msg.from_id;
+ assert_eq!(
+ Chat::load_from_db(alice0, a0b_chat_id).await?.blocked,
+ Blocked::Request
+ );
a0b_chat_id.accept(alice0).await?;
- let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
+ let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?;
assert_eq!(a0b_contact.origin, Origin::CreateChat);
- assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not);
+ assert_eq!(alice0.get_chat(bob).await.blocked, Blocked::Not);
sync(alice0, alice1).await;
- let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
+ let alice1_contacts = Contact::get_all(alice1, 0, None).await?;
+ assert_eq!(alice1_contacts.len(), 1);
+ let a1b_contact_id = alice1_contacts[0];
+ let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
+ assert_eq!(a1b_contact.get_addr(), "bob@example.net");
assert_eq!(a1b_contact.origin, Origin::CreateChat);
- let a1b_chat = alice1.get_chat(&bob).await;
+ let a1b_chat = alice1.get_chat(bob).await;
assert_eq!(a1b_chat.blocked, Blocked::Not);
let chats = Chatlist::try_load(alice1, 0, None, None).await?;
assert_eq!(chats.len(), 1);
@@ -2963,22 +3121,22 @@ async fn test_sync_block_before_first_msg() -> Result<()> {
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
- let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
+ let rcvd_msg = alice0.recv_msg(&sent_msg).await;
+ let a0b_chat_id = rcvd_msg.chat_id;
+ let a0b_contact_id = rcvd_msg.from_id;
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.block(alice0).await?;
- let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
+ let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?;
assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom);
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes);
sync(alice0, alice1).await;
- let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
- assert_eq!(a1b_contact.origin, Origin::Hidden);
- assert!(ChatIdBlocked::lookup_by_contact(alice1, a1b_contact.id)
- .await?
- .is_none());
+ let alice1_contacts = Contact::get_all(alice1, 0, None).await?;
+ assert_eq!(alice1_contacts.len(), 0);
let rcvd_msg = alice1.recv_msg(&sent_msg).await;
- let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
+ let a1b_contact_id = rcvd_msg.from_id;
+ let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom);
let a1b_chat = alice1.get_chat(&bob).await;
assert_eq!(a1b_chat.blocked, Blocked::Yes);
@@ -2986,6 +3144,48 @@ async fn test_sync_block_before_first_msg() -> Result<()> {
Ok(())
}
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_sync_delete_chat() -> Result<()> {
+ let alice0 = &TestContext::new_alice().await;
+ let alice1 = &TestContext::new_alice().await;
+ for a in [alice0, alice1] {
+ a.set_config_bool(Config::SyncMsgs, true).await?;
+ }
+ let bob = TestContext::new_bob().await;
+
+ let ba_chat = bob.create_chat(alice0).await;
+ let sent_msg = bob.send_text(ba_chat.id, "hi").await;
+ let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
+ let a1b_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
+ a0b_chat_id.accept(alice0).await?;
+ sync(alice0, alice1).await;
+ a0b_chat_id.delete(alice0).await?;
+ sync(alice0, alice1).await;
+ alice1.assert_no_chat(a1b_chat_id).await;
+ alice1
+ .evtracker
+ .get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. }))
+ .await;
+
+ let bob_grp_chat_id = bob
+ .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0])
+ .await;
+ let sent_msg = bob.send_text(bob_grp_chat_id, "hi").await;
+ let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
+ let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
+ a0_grp_chat_id.accept(alice0).await?;
+ sync(alice0, alice1).await;
+ a0_grp_chat_id.delete(alice0).await?;
+ sync(alice0, alice1).await;
+ alice1.assert_no_chat(a1_grp_chat_id).await;
+ alice0
+ .evtracker
+ .get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. }))
+ .await;
+
+ Ok(())
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_adhoc_grp() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
@@ -3036,8 +3236,9 @@ async fn test_sync_adhoc_grp() -> Result<()> {
/// - That sync messages don't unarchive the self-chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_visibility() -> Result<()> {
- let alice0 = &TestContext::new_alice().await;
- let alice1 = &TestContext::new_alice().await;
+ let mut tcm = TestContextManager::new();
+ let alice0 = &tcm.alice().await;
+ let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
@@ -3059,10 +3260,39 @@ async fn test_sync_visibility() -> Result<()> {
Ok(())
}
+/// Tests syncing of chat visibility on device message chat.
+///
+/// Previously due to a bug pinning "Device Messages"
+/// chat resulted in creation of `device@localhost` chat
+/// on another device.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_sync_device_messages_visibility() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice0 = &tcm.alice().await;
+ let alice1 = &tcm.alice().await;
+ for a in [alice0, alice1] {
+ a.set_config_bool(Config::SyncMsgs, true).await?;
+ }
+
+ let device_chat_id0 = ChatId::get_for_contact(alice0, ContactId::DEVICE).await?;
+ device_chat_id0
+ .set_visibility(alice0, ChatVisibility::Pinned)
+ .await?;
+
+ sync(alice0, alice1).await;
+
+ let device_chat_id1 = ChatId::get_for_contact(alice1, ContactId::DEVICE).await?;
+ let device_chat1 = Chat::load_from_db(alice1, device_chat_id1).await?;
+ assert_eq!(device_chat1.get_visibility(), ChatVisibility::Pinned);
+
+ Ok(())
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_muted() -> Result<()> {
- let alice0 = &TestContext::new_alice().await;
- let alice1 = &TestContext::new_alice().await;
+ let mut tcm = TestContextManager::new();
+ let alice0 = &tcm.alice().await;
+ let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
@@ -3096,8 +3326,9 @@ async fn test_sync_muted() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_broadcast() -> Result<()> {
- let alice0 = &TestContext::new_alice().await;
- let alice1 = &TestContext::new_alice().await;
+ let mut tcm = TestContextManager::new();
+ let alice0 = &tcm.alice().await;
+ let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
@@ -3140,6 +3371,10 @@ async fn test_sync_broadcast() -> Result<()> {
assert!(get_past_chat_contacts(alice1, a1_broadcast_id)
.await?
.is_empty());
+
+ a0_broadcast_id.delete(alice0).await?;
+ sync(alice0, alice1).await;
+ alice1.assert_no_chat(a1_broadcast_id).await;
Ok(())
}
@@ -3229,6 +3464,128 @@ async fn test_do_not_overwrite_draft() -> Result<()> {
Ok(())
}
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_info_contact_id() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let alice2 = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+
+ async fn pop_recv_and_check(
+ alice: &TestContext,
+ alice2: &TestContext,
+ bob: &TestContext,
+ expected_type: SystemMessage,
+ expected_alice_id: ContactId,
+ expected_bob_id: ContactId,
+ ) -> Result<()> {
+ let sent_msg = alice.pop_sent_msg().await;
+ let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?;
+ assert_eq!(msg.get_info_type(), expected_type);
+ assert_eq!(
+ msg.get_info_contact_id(alice).await?,
+ Some(expected_alice_id)
+ );
+
+ let msg = alice2.recv_msg(&sent_msg).await;
+ assert_eq!(msg.get_info_type(), expected_type);
+ assert_eq!(
+ msg.get_info_contact_id(alice2).await?,
+ Some(expected_alice_id)
+ );
+
+ let msg = bob.recv_msg(&sent_msg).await;
+ assert_eq!(msg.get_info_type(), expected_type);
+ assert_eq!(msg.get_info_contact_id(bob).await?, Some(expected_bob_id));
+
+ Ok(())
+ }
+
+ // Alice creates group, Bob receives group
+ let alice_chat_id = alice
+ .create_group_with_members(ProtectionStatus::Unprotected, "play", &[bob])
+ .await;
+ let sent_msg1 = alice.send_text(alice_chat_id, "moin").await;
+
+ let msg = bob.recv_msg(&sent_msg1).await;
+ let bob_alice_id = msg.from_id;
+ assert!(!bob_alice_id.is_special());
+
+ // Alice does group changes, Bob receives them
+ set_chat_name(alice, alice_chat_id, "games").await?;
+ pop_recv_and_check(
+ alice,
+ alice2,
+ bob,
+ SystemMessage::GroupNameChanged,
+ ContactId::SELF,
+ bob_alice_id,
+ )
+ .await?;
+
+ let file = alice.get_blobdir().join("avatar.png");
+ let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
+ tokio::fs::write(&file, bytes).await?;
+ set_chat_profile_image(alice, alice_chat_id, file.to_str().unwrap()).await?;
+ pop_recv_and_check(
+ alice,
+ alice2,
+ bob,
+ SystemMessage::GroupImageChanged,
+ ContactId::SELF,
+ bob_alice_id,
+ )
+ .await?;
+
+ alice_chat_id
+ .set_ephemeral_timer(alice, Timer::Enabled { duration: 60 })
+ .await?;
+ pop_recv_and_check(
+ alice,
+ alice2,
+ bob,
+ SystemMessage::EphemeralTimerChanged,
+ ContactId::SELF,
+ bob_alice_id,
+ )
+ .await?;
+
+ let fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await; // contexts are in sync, fiona_id is same everywhere
+ add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
+ pop_recv_and_check(
+ alice,
+ alice2,
+ bob,
+ SystemMessage::MemberAddedToGroup,
+ fiona_id,
+ fiona_id,
+ )
+ .await?;
+
+ remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?;
+ pop_recv_and_check(
+ alice,
+ alice2,
+ bob,
+ SystemMessage::MemberRemovedFromGroup,
+ fiona_id,
+ fiona_id,
+ )
+ .await?;
+
+ // When fiona_id is deleted, get_info_contact_id() returns None.
+ // We raw delete in db as Contact::delete() leaves a tombstone (which is great as the tap works longer then)
+ alice
+ .sql
+ .execute("DELETE FROM contacts WHERE id=?", (fiona_id,))
+ .await?;
+ let msg = alice.get_last_msg().await;
+ assert_eq!(msg.get_info_type(), SystemMessage::MemberRemovedFromGroup);
+ assert!(msg.get_info_contact_id(alice).await?.is_none());
+
+ Ok(())
+}
+
/// Test group consistency.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_member_bug() -> Result<()> {
@@ -3236,9 +3593,10 @@ async fn test_add_member_bug() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
+ let fiona = &tcm.fiona().await;
- let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
- let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
+ let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
+ let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
// Create a group.
let alice_chat_id =
@@ -3282,7 +3640,8 @@ async fn test_past_members() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
- let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
+ let fiona = &tcm.fiona().await;
+ let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3294,8 +3653,7 @@ async fn test_past_members() -> Result<()> {
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1);
let bob = &tcm.bob().await;
- let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
- let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
+ let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let add_message = alice.pop_sent_msg().await;
@@ -3314,8 +3672,7 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
- let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
- let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
+ let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3327,7 +3684,8 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
let bob_chat_id = bob_received_msg.get_chat_id();
bob_chat_id.accept(bob).await?;
- let bob_fiona_contact_id = Contact::create(bob, "Fiona", "fiona@example.net").await?;
+ let fiona = &tcm.fiona().await;
+ let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
// Alice removes Bob and Bob adds Fiona at the same time.
remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
@@ -3349,11 +3707,10 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
+ let fiona = &tcm.fiona().await;
- let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
- let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
- let fiona_addr = "fiona@example.net";
- let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?;
+ let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
+ let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3369,10 +3726,6 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
let sent = alice.send_text(alice_chat_id, "Hello group!").await;
- let payload = sent.payload();
- assert_eq!(payload.contains("Hello group!"), true);
- assert_eq!(payload.contains(&bob_addr), true);
- assert_eq!(payload.contains(fiona_addr), false);
let bob_msg = bob.recv_msg(&sent).await;
let bob_chat_id = bob_msg.chat_id;
@@ -3388,8 +3741,8 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
- let fiona_addr = "fiona@example.net";
- let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?;
+ let fiona = &tcm.fiona().await;
+ let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3404,12 +3757,10 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
let bob = &tcm.bob().await;
- let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
- let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
+ let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let add_message = alice.pop_sent_msg().await;
- assert_eq!(add_message.payload.contains(fiona_addr), false);
let bob_add_message = bob.recv_msg(&add_message).await;
let bob_chat_id = bob_add_message.chat_id;
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
@@ -3418,6 +3769,65 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
Ok(())
}
+/// Test that past members are ordered by the timestamp of their removal.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_past_members_order() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let t = &tcm.alice().await;
+
+ let bob = tcm.bob().await;
+ let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
+ let charlie = tcm.charlie().await;
+ let charlie_contact_id = t.add_or_lookup_contact_id(&charlie).await;
+ let fiona = tcm.fiona().await;
+ let fiona_contact_id = t.add_or_lookup_contact_id(&fiona).await;
+
+ let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "Group chat").await?;
+ add_contact_to_chat(t, chat_id, bob_contact_id).await?;
+ add_contact_to_chat(t, chat_id, charlie_contact_id).await?;
+ add_contact_to_chat(t, chat_id, fiona_contact_id).await?;
+ t.send_text(chat_id, "Hi! I created a group.").await;
+
+ assert_eq!(get_past_chat_contacts(t, chat_id).await?.len(), 0);
+
+ remove_contact_from_chat(t, chat_id, charlie_contact_id).await?;
+
+ let past_contacts = get_past_chat_contacts(t, chat_id).await?;
+ assert_eq!(past_contacts.len(), 1);
+ assert_eq!(past_contacts[0], charlie_contact_id);
+
+ SystemTime::shift(Duration::from_secs(5));
+ remove_contact_from_chat(t, chat_id, bob_contact_id).await?;
+
+ let past_contacts = get_past_chat_contacts(t, chat_id).await?;
+ assert_eq!(past_contacts.len(), 2);
+ assert_eq!(past_contacts[0], bob_contact_id);
+ assert_eq!(past_contacts[1], charlie_contact_id);
+
+ SystemTime::shift(Duration::from_secs(5));
+ remove_contact_from_chat(t, chat_id, fiona_contact_id).await?;
+
+ let past_contacts = get_past_chat_contacts(t, chat_id).await?;
+ assert_eq!(past_contacts.len(), 3);
+ assert_eq!(past_contacts[0], fiona_contact_id);
+ assert_eq!(past_contacts[1], bob_contact_id);
+ assert_eq!(past_contacts[2], charlie_contact_id);
+
+ // Adding and removing Bob
+ // moves him to the top of past member list.
+ SystemTime::shift(Duration::from_secs(5));
+ add_contact_to_chat(t, chat_id, bob_contact_id).await?;
+ remove_contact_from_chat(t, chat_id, bob_contact_id).await?;
+
+ let past_contacts = get_past_chat_contacts(t, chat_id).await?;
+ assert_eq!(past_contacts.len(), 3);
+ assert_eq!(past_contacts[0], bob_contact_id);
+ assert_eq!(past_contacts[1], fiona_contact_id);
+ assert_eq!(past_contacts[2], charlie_contact_id);
+
+ Ok(())
+}
+
/// Test the case when Alice restores a backup older than 60 days
/// with outdated member list.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -3428,13 +3838,11 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
+ let charlie = &tcm.charlie().await;
let fiona = &tcm.fiona().await;
- let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
- let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
-
- let charlie_addr = "charlie@example.com";
- let alice_charlie_contact_id = Contact::create(alice, "Charlie", charlie_addr).await?;
+ let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
+ let alice_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3456,7 +3864,6 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1);
let remove_message = alice.pop_sent_msg().await;
- assert_eq!(remove_message.payload.contains(charlie_addr), true);
bob.recv_msg(&remove_message).await;
// 60 days pass.
@@ -3465,8 +3872,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
// Bob adds Fiona to the chat.
- let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap();
- let bob_fiona_contact_id = Contact::create(bob, "Fiona", &fiona_addr).await?;
+ let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
add_contact_to_chat(bob, bob_chat_id, bob_fiona_contact_id).await?;
let add_message = bob.pop_sent_msg().await;
@@ -3517,8 +3923,10 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
alice.recv_msg(&bob_sent_text).await;
fiona.recv_msg(&bob_sent_text).await;
+ // Alice did not knew that Charlie is not part of the group
+ // when sending a message, so sent it to Charlie.
bob.recv_msg(&alice_sent_text).await;
- fiona.recv_msg(&alice_sent_text).await;
+ charlie.recv_msg(&alice_sent_text).await;
// Alice should have learned about Charlie not being part of the group
// by receiving Bob's message.
@@ -3530,7 +3938,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
// Charlie is not part of the chat.
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
assert_eq!(get_past_chat_contacts(bob, bob_chat_id).await?.len(), 0);
- let bob_charlie_contact_id = Contact::create(bob, "Charlie", charlie_addr).await?;
+ let bob_charlie_contact_id = bob.add_or_lookup_contact_id(charlie).await;
assert!(!is_contact_in_chat(bob, bob_chat_id, bob_charlie_contact_id).await?);
assert_eq!(get_chat_contacts(fiona, fiona_chat_id).await?.len(), 3);
@@ -3547,3 +3955,228 @@ async fn test_one_to_one_chat_no_group_member_timestamps() {
let payload = sent.payload;
assert!(!payload.contains("Chat-Group-Member-Timestamps:"));
}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_send_edit_request() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let alice_chat = alice.create_chat(bob).await;
+
+ // Alice sends a message with typos, followed by a correction message
+ let sent1 = alice.send_text(alice_chat.id, "zext me in delra.cat").await;
+ let alice_msg = sent1.load_from_db().await;
+ assert_eq!(alice_msg.text, "zext me in delra.cat");
+
+ send_edit_request(alice, alice_msg.id, "Text me on Delta.Chat".to_string()).await?;
+ let sent2 = alice.pop_sent_msg().await;
+ let test = Message::load_from_db(alice, alice_msg.id).await?;
+ assert_eq!(test.text, "Text me on Delta.Chat");
+
+ // Bob receives both messages and has the correct text at the end
+ let bob_msg = bob.recv_msg(&sent1).await;
+ assert_eq!(bob_msg.text, "zext me in delra.cat");
+
+ bob.recv_msg_opt(&sent2).await;
+ let test = Message::load_from_db(bob, bob_msg.id).await?;
+ assert_eq!(test.text, "Text me on Delta.Chat");
+ assert!(test.is_edited());
+
+ // alice has another device, and sees the correction also there
+ let alice2 = tcm.alice().await;
+ let alice2_msg = alice2.recv_msg(&sent1).await;
+ assert_eq!(alice2_msg.text, "zext me in delra.cat");
+
+ alice2.recv_msg_opt(&sent2).await;
+ let test = Message::load_from_db(&alice2, alice2_msg.id).await?;
+ assert_eq!(test.text, "Text me on Delta.Chat");
+ assert!(test.is_edited());
+
+ // Alice forwards the edited message, the new message shouldn't have the "edited" mark.
+ forward_msgs(&alice2, &[test.id], test.chat_id).await?;
+ let forwarded = alice2.get_last_msg().await;
+ assert!(!forwarded.is_edited());
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_receive_edit_request_after_removal() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let alice_chat = alice.create_chat(bob).await;
+
+ // Alice sends a messag with typos, followed by a correction message
+ let sent1 = alice.send_text(alice_chat.id, "zext me in delra.cat").await;
+ let alice_msg = sent1.load_from_db().await;
+ send_edit_request(alice, alice_msg.id, "Text me on Delta.Chat".to_string()).await?;
+ let sent2 = alice.pop_sent_msg().await;
+
+ // Bob receives first message, deletes it and then ignores the correction
+ let bob_msg = bob.recv_msg(&sent1).await;
+ let bob_chat_id = bob_msg.chat_id;
+ assert_eq!(bob_msg.text, "zext me in delra.cat");
+ assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 1);
+
+ delete_msgs(bob, &[bob_msg.id]).await?;
+ assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
+
+ bob.recv_msg_trash(&sent2).await;
+ assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_cannot_send_edit_request() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let chat_id = alice
+ .create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[bob])
+ .await;
+
+ // Alice can edit her message
+ let sent1 = alice.send_text(chat_id, "foo").await;
+ send_edit_request(alice, sent1.sender_msg_id, "bar".to_string()).await?;
+
+ // Bob cannot edit Alice's message
+ let msg = bob.recv_msg(&sent1).await;
+ assert!(send_edit_request(bob, msg.id, "bar".to_string())
+ .await
+ .is_err());
+
+ // HTML messages cannot be edited
+ let mut msg = Message::new_text("plain text".to_string());
+ msg.set_html(Some("html text".to_string()));
+ let sent2 = alice.send_msg(chat_id, &mut msg).await;
+ assert!(msg.has_html());
+ assert!(
+ send_edit_request(alice, sent2.sender_msg_id, "foo".to_string())
+ .await
+ .is_err()
+ );
+
+ // Info messages cannot be edited
+ set_chat_name(alice, chat_id, "bar").await?;
+ let msg = alice.get_last_msg().await;
+ assert!(msg.is_info());
+ assert_eq!(msg.from_id, ContactId::SELF);
+ assert!(send_edit_request(alice, msg.id, "bar".to_string())
+ .await
+ .is_err());
+
+ // Videochat invitations cannot be edited
+ alice
+ .set_config(Config::WebrtcInstance, Some("https://foo.bar"))
+ .await?;
+ let msg_id = send_videochat_invitation(alice, chat_id).await?;
+ assert!(send_edit_request(alice, msg_id, "bar".to_string())
+ .await
+ .is_err());
+
+ // If not text was given initally, there is nothing to edit
+ // (this also avoids complexity in UI element changes; focus is typos and rewordings)
+ let mut msg = Message::new(Viewtype::File);
+ msg.make_vcard(alice, &[ContactId::SELF]).await?;
+ let sent3 = alice.send_msg(chat_id, &mut msg).await;
+ assert!(msg.text.is_empty());
+ assert!(
+ send_edit_request(alice, sent3.sender_msg_id, "bar".to_string())
+ .await
+ .is_err()
+ );
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_send_delete_request() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let alice_chat = alice.create_chat(bob).await;
+ let bob_chat = bob.create_chat(alice).await;
+
+ // Bobs sends a message to Alice, so Alice learns Bob's key
+ let sent0 = bob.send_text(bob_chat.id, "¡ola!").await;
+ alice.recv_msg(&sent0).await;
+
+ // Alice sends a message, then sends a deletion request
+ let sent1 = alice.send_text(alice_chat.id, "wtf").await;
+ let alice_msg = sent1.load_from_db().await;
+ assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 2);
+
+ message::delete_msgs_ex(alice, &[alice_msg.id], true).await?;
+ let sent2 = alice.pop_sent_msg().await;
+ assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
+
+ // Bob receives both messages and has nothing the end
+ let bob_msg = bob.recv_msg(&sent1).await;
+ assert_eq!(bob_msg.text, "wtf");
+ assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2);
+
+ bob.recv_msg_opt(&sent2).await;
+ assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1);
+
+ // Alice has another device, and there is also nothing at the end
+ let alice2 = &tcm.alice().await;
+ alice2.recv_msg(&sent0).await;
+ let alice2_msg = alice2.recv_msg(&sent1).await;
+ assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 2);
+
+ alice2.recv_msg_opt(&sent2).await;
+ assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 1);
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_send_delete_request_no_encryption() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+ let alice_chat = alice.create_email_chat(bob).await;
+
+ // Alice sends a message, then tries to send a deletion request which fails.
+ let sent1 = alice.send_text(alice_chat.id, "wtf").await;
+ assert!(message::delete_msgs_ex(alice, &[sent1.sender_msg_id], true)
+ .await
+ .is_err());
+ sent1.load_from_db().await;
+ assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
+ Ok(())
+}
+
+/// Tests that in multi-device setup
+/// second device learns the key of a contact
+/// via Autocrypt-Gossip in 1:1 chats.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_oneone_gossip() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let alice2 = &tcm.alice().await;
+ let bob = &tcm.bob().await;
+
+ tcm.section("Alice imports Bob's vCard and sends a message from the first device");
+ let alice_chat = alice.create_chat(bob).await;
+ let sent_msg = alice.send_text(alice_chat.id, "Hello Bob!").await;
+
+ tcm.section("Alice receives a copy on second device");
+ let rcvd_msg = alice2.recv_msg(&sent_msg).await;
+ assert_eq!(rcvd_msg.get_showpadlock(), true);
+
+ tcm.section("Alice sends a message from the second device");
+ let alice2_chat_id = rcvd_msg.chat_id;
+ let sent_msg2 = alice2
+ .send_text(alice2_chat_id, "Hello from second device!")
+ .await;
+
+ tcm.section("Bob receives a message from the second device");
+ let rcvd_msg2 = bob.recv_msg(&sent_msg2).await;
+ assert_eq!(rcvd_msg2.get_showpadlock(), true);
+ assert_eq!(rcvd_msg2.text, "Hello from second device!");
+
+ Ok(())
+}
diff --git a/src/chatlist.rs b/src/chatlist.rs
index 85a6067fa6..d8f7043406 100644
--- a/src/chatlist.rs
+++ b/src/chatlist.rs
@@ -1,7 +1,7 @@
//! # Chat list module.
use anyhow::{ensure, Context as _, Result};
-use once_cell::sync::Lazy;
+use std::sync::LazyLock;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
@@ -17,8 +17,8 @@ use crate::summary::Summary;
use crate::tools::IsNoneOrEmpty;
/// Regex to find out if a query should filter by unread messages.
-pub static IS_UNREAD_FILTER: Lazy =
- Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
+pub static IS_UNREAD_FILTER: LazyLock =
+ LazyLock::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
/// An object representing a single chatlist in memory.
///
@@ -322,7 +322,7 @@ impl Chatlist {
(chat_id, MessageState::OutDraft),
)
.await
- .with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
+ .with_context(|| format!("failed to get msg ID for chat {chat_id}"))?;
ids.push((chat_id, msg_id));
}
Ok(Chatlist { ids })
@@ -407,16 +407,17 @@ impl Chatlist {
let lastcontact = if let Some(lastmsg) = &lastmsg {
if lastmsg.from_id == ContactId::SELF {
None
+ } else if chat.typ == Chattype::Group
+ || chat.typ == Chattype::Broadcast
+ || chat.typ == Chattype::Mailinglist
+ || chat.is_self_talk()
+ {
+ let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
+ .await
+ .context("loading contact failed")?;
+ Some(lastcontact)
} else {
- match chat.typ {
- Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
- let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
- .await
- .context("loading contact failed")?;
- Some(lastcontact)
- }
- Chattype::Single => None,
- }
+ None
}
} else {
None
@@ -479,6 +480,7 @@ pub async fn get_last_message_for_chat(
#[cfg(test)]
mod tests {
use super::*;
+ use crate::chat::save_msgs;
use crate::chat::{
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
send_text_msg, ProtectionStatus,
@@ -486,22 +488,24 @@ mod tests {
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
+ use crate::test_utils::TestContextManager;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_try_load() {
- let t = TestContext::new_bob().await;
- let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
+ let mut tcm = TestContextManager::new();
+ let bob = &tcm.bob().await;
+ let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
- let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
+ let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
- let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
+ let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
// check that the chatlist starts with the most recent message
- let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
+ let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id3);
assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2);
@@ -517,51 +521,49 @@ mod tests {
// 2s here.
for chat_id in &[chat_id1, chat_id3, chat_id2] {
let mut msg = Message::new_text("hello".to_string());
- chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
+ chat_id.set_draft(bob, Some(&mut msg)).await.unwrap();
}
- let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
+ let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id2);
// check chatlist query and archive functionality
- let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
+ let chats = Chatlist::try_load(bob, 0, Some("b"), None).await.unwrap();
assert_eq!(chats.len(), 1);
// receive a message from alice
- let alice = TestContext::new_alice().await;
- let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat")
+ let alice = &tcm.alice().await;
+ let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat")
.await
.unwrap();
add_contact_to_chat(
- &alice,
+ alice,
alice_chat_id,
- Contact::create(&alice, "bob", "bob@example.net")
- .await
- .unwrap(),
+ alice.add_or_lookup_contact_id(bob).await,
)
.await
.unwrap();
- send_text_msg(&alice, alice_chat_id, "hi".into())
+ send_text_msg(alice, alice_chat_id, "hi".into())
.await
.unwrap();
let sent_msg = alice.pop_sent_msg().await;
- t.recv_msg(&sent_msg).await;
- let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
+ bob.recv_msg(&sent_msg).await;
+ let chats = Chatlist::try_load(bob, 0, Some("is:unread"), None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
- let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
+ let chats = Chatlist::try_load(bob, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 0);
chat_id1
- .set_visibility(&t, ChatVisibility::Archived)
+ .set_visibility(bob, ChatVisibility::Archived)
.await
.ok();
- let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
+ let chats = Chatlist::try_load(bob, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
@@ -787,6 +789,31 @@ mod tests {
assert!(summary_res.is_ok());
}
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+ async fn test_get_summary_for_saved_messages() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice = tcm.alice().await;
+ let bob = tcm.bob().await;
+ let chat_alice = alice.create_chat(&bob).await;
+
+ send_text_msg(&alice, chat_alice.id, "hi".into()).await?;
+ let sent1 = alice.pop_sent_msg().await;
+ save_msgs(&alice, &[sent1.sender_msg_id]).await?;
+ let chatlist = Chatlist::try_load(&alice, 0, None, None).await?;
+ let summary = chatlist.get_summary(&alice, 0, None).await?;
+ assert_eq!(summary.prefix.unwrap().to_string(), "Me");
+ assert_eq!(summary.text, "hi");
+
+ let msg = bob.recv_msg(&sent1).await;
+ save_msgs(&bob, &[msg.id]).await?;
+ let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
+ let summary = chatlist.get_summary(&bob, 0, None).await?;
+ assert_eq!(summary.prefix.unwrap().to_string(), "alice@example.org");
+ assert_eq!(summary.text, "hi");
+
+ Ok(())
+ }
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;
diff --git a/src/config.rs b/src/config.rs
index 8ac186be75..1fa11d49ec 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -4,7 +4,7 @@ use std::env;
use std::path::Path;
use std::str::FromStr;
-use anyhow::{ensure, Context as _, Result};
+use anyhow::{bail, ensure, Context as _, Result};
use base64::Engine as _;
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
use serde::{Deserialize, Serialize};
@@ -13,10 +13,12 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use tokio::fs;
use crate::blob::BlobObject;
+use crate::configure::EnteredLoginParam;
use crate::constants;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
+use crate::login_param::ConfiguredLoginParam;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
use crate::sync::{self, Sync::*, SyncData};
@@ -182,21 +184,11 @@ pub enum Config {
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
- /// If set to "1", on the first time `start_io()` is called after configuring,
- /// the newest existing messages are fetched.
- /// Existing recipients are added to the contact database regardless of this setting.
- #[strum(props(default = "0"))]
- FetchExistingMsgs,
-
/// If set to "1", then existing messages are considered to be already fetched.
/// This flag is reset after successful configuration.
#[strum(props(default = "1"))]
FetchedExistingMsgs,
- /// Type of the OpenPGP key to generate.
- #[strum(props(default = "0"))]
- KeyGenType,
-
/// Timer in seconds after which the message is deleted from the
/// server.
///
@@ -220,9 +212,6 @@ pub enum Config {
/// `ProviderOptions::delete_to_trash`.
DeleteToTrash,
- /// Save raw MIME messages with headers in the database if true.
- SaveMimeHeaders,
-
/// The primary email address. Also see `SecondaryAddrs`.
ConfiguredAddr,
@@ -455,6 +444,13 @@ pub enum Config {
/// If it has not changed, we do not store
/// the device token again.
DeviceToken,
+
+ /// Device token encrypted with OpenPGP.
+ ///
+ /// We store encrypted token next to `device_token`
+ /// to avoid encrypting it differently and
+ /// storing the same token multiple times on the server.
+ EncryptedDeviceToken,
}
impl Config {
@@ -481,7 +477,10 @@ impl Config {
/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
- matches!(self, Config::OnlyFetchMvbox | Config::SentboxWatch)
+ matches!(
+ self,
+ Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
+ )
}
}
@@ -528,21 +527,22 @@ impl Context {
// Default values
let val = match key {
Config::BccSelf => match Box::pin(self.is_chatmail()).await? {
- false => Some("1"),
- true => Some("0"),
+ false => Some("1".to_string()),
+ true => Some("0".to_string()),
},
- Config::ConfiguredInboxFolder => Some("INBOX"),
+ Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
Config::DeleteServerAfter => {
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
&& Box::pin(self.is_chatmail()).await?
{
- true => Some("1"),
- false => Some("0"),
+ true => Some("1".to_string()),
+ false => Some("0".to_string()),
}
}
- _ => key.get_str("default"),
+ Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
+ _ => key.get_str("default").map(|s| s.to_string()),
};
- Ok(val.map(|s| s.to_string()))
+ Ok(val)
}
/// Returns Some(T) if a value for the given key is set and was successfully parsed.
@@ -707,9 +707,7 @@ impl Context {
| Config::SentboxWatch
| Config::MvboxMove
| Config::OnlyFetchMvbox
- | Config::FetchExistingMsgs
| Config::DeleteToTrash
- | Config::SaveMimeHeaders
| Config::Configured
| Config::Bot
| Config::NotifyAboutWrongPw
@@ -810,6 +808,19 @@ impl Context {
.set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None)
.await?;
}
+ Config::ConfiguredAddr => {
+ if self.is_configured().await? {
+ bail!("Cannot change ConfiguredAddr");
+ }
+ if let Some(addr) = value {
+ info!(self, "Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!");
+ ConfiguredLoginParam::from_json(&format!(
+ r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
+ ))?
+ .save_to_transports_table(self, &EnteredLoginParam::default())
+ .await?;
+ }
+ }
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
@@ -896,6 +907,7 @@ impl Context {
/// primary address (if exists) as a secondary address.
///
/// This should only be used by test code and during configure.
+ #[cfg(test)] // AEAP is disabled, but there are still tests for it
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
self.quota.write().await.take();
@@ -909,7 +921,8 @@ impl Context {
)
.await?;
- self.set_config_internal(Config::ConfiguredAddr, Some(primary_new))
+ self.sql
+ .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
.await?;
self.emit_event(EventType::ConnectivityChanged);
Ok(())
@@ -956,397 +969,4 @@ fn get_config_keys_string() -> String {
}
#[cfg(test)]
-mod tests {
- use num_traits::FromPrimitive;
-
- use super::*;
- use crate::test_utils::{sync, TestContext, TestContextManager};
-
- #[test]
- fn test_to_string() {
- assert_eq!(Config::MailServer.to_string(), "mail_server");
- assert_eq!(Config::from_str("mail_server"), Ok(Config::MailServer));
-
- assert_eq!(Config::SysConfigKeys.to_string(), "sys.config_keys");
- assert_eq!(
- Config::from_str("sys.config_keys"),
- Ok(Config::SysConfigKeys)
- );
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_set_config_addr() {
- let t = TestContext::new().await;
-
- // Test that uppercase address get lowercased.
- assert!(t
- .set_config(Config::Addr, Some("Foobar@eXample.oRg"))
- .await
- .is_ok());
- assert_eq!(
- t.get_config(Config::Addr).await.unwrap().unwrap(),
- "foobar@example.org"
- );
- }
-
- /// Tests that "bot" config can only be set to "0" or "1".
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_set_config_bot() {
- let t = TestContext::new().await;
-
- assert!(t.set_config(Config::Bot, None).await.is_ok());
- assert!(t.set_config(Config::Bot, Some("0")).await.is_ok());
- assert!(t.set_config(Config::Bot, Some("1")).await.is_ok());
- assert!(t.set_config(Config::Bot, Some("2")).await.is_err());
- assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err());
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_media_quality_config_option() {
- let t = TestContext::new().await;
- let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
- assert_eq!(media_quality, 0);
- let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
- assert_eq!(media_quality, constants::MediaQuality::Balanced);
-
- t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
-
- let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
- assert_eq!(media_quality, 1);
- assert_eq!(constants::MediaQuality::Worse as i32, 1);
- let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
- assert_eq!(media_quality, constants::MediaQuality::Worse);
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_ui_config() -> Result<()> {
- let t = TestContext::new().await;
-
- assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None);
-
- t.set_ui_config("ui.android.screen_security", Some("safe"))
- .await?;
- assert_eq!(
- t.get_ui_config("ui.android.screen_security").await?,
- Some("safe".to_string())
- );
-
- t.set_ui_config("ui.android.screen_security", None).await?;
- assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None);
-
- assert!(t.set_ui_config("configured", Some("bar")).await.is_err());
-
- Ok(())
- }
-
- /// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_set_config_bool() -> Result<()> {
- let t = TestContext::new().await;
-
- // We need some config that defaults to true
- let c = Config::E2eeEnabled;
- assert_eq!(t.get_config_bool(c).await?, true);
- t.set_config_bool(c, false).await?;
- assert_eq!(t.get_config_bool(c).await?, false);
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_self_addrs() -> Result<()> {
- let alice = TestContext::new_alice().await;
-
- assert!(alice.is_self_addr("alice@example.org").await?);
- assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]);
- assert!(!alice.is_self_addr("alice@alice.com").await?);
-
- // Test adding the same primary address
- alice.set_primary_self_addr("alice@example.org").await?;
- alice.set_primary_self_addr("Alice@Example.Org").await?;
- assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);
-
- // Test adding a new (primary) self address
- // The address is trimmed during configure by `LoginParam::from_database()`,
- // so `set_primary_self_addr()` doesn't have to trim it.
- alice.set_primary_self_addr("Alice@alice.com").await?;
- assert!(alice.is_self_addr("aliCe@example.org").await?);
- assert!(alice.is_self_addr("alice@alice.com").await?);
- assert_eq!(
- alice.get_all_self_addrs().await?,
- vec!["Alice@alice.com", "Alice@Example.Org"]
- );
-
- // Check that the entry is not duplicated
- alice.set_primary_self_addr("alice@alice.com").await?;
- alice.set_primary_self_addr("alice@alice.com").await?;
- assert_eq!(
- alice.get_all_self_addrs().await?,
- vec!["alice@alice.com", "Alice@Example.Org"]
- );
-
- // Test switching back
- alice.set_primary_self_addr("alice@example.org").await?;
- assert_eq!(
- alice.get_all_self_addrs().await?,
- vec!["alice@example.org", "alice@alice.com"]
- );
-
- // Test setting a new primary self address, the previous self address
- // should be kept as a secondary self address
- alice.set_primary_self_addr("alice@alice.xyz").await?;
- assert_eq!(
- alice.get_all_self_addrs().await?,
- vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"]
- );
- assert!(alice.is_self_addr("alice@example.org").await?);
- assert!(alice.is_self_addr("alice@alice.com").await?);
- assert!(alice.is_self_addr("Alice@alice.xyz").await?);
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_mdns_default_behaviour() -> Result<()> {
- let t = &TestContext::new_alice().await;
- assert!(t.should_request_mdns().await?);
- assert!(t.should_send_mdns().await?);
- assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
- // The setting should be displayed correctly.
- assert!(t.get_config_bool(Config::MdnsEnabled).await?);
-
- t.set_config_bool(Config::Bot, true).await?;
- assert!(!t.should_request_mdns().await?);
- assert!(t.should_send_mdns().await?);
- assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
- assert!(t.get_config_bool(Config::MdnsEnabled).await?);
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_delete_server_after_default() -> Result<()> {
- let t = &TestContext::new_alice().await;
-
- // Check that the settings are displayed correctly.
- assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string()));
- assert_eq!(
- t.get_config(Config::DeleteServerAfter).await?,
- Some("0".to_string())
- );
-
- // Leaving emails on the server even w/o `BccSelf` is a good default at least because other
- // MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail
- // does).
- t.set_config_bool(Config::BccSelf, false).await?;
- assert_eq!(
- t.get_config(Config::DeleteServerAfter).await?,
- Some("0".to_string())
- );
- Ok(())
- }
-
- const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_sync() -> Result<()> {
- let alice0 = TestContext::new_alice().await;
- let alice1 = TestContext::new_alice().await;
- for a in [&alice0, &alice1] {
- a.set_config_bool(Config::SyncMsgs, true).await?;
- }
-
- let mdns_enabled = alice0.get_config_bool(Config::MdnsEnabled).await?;
- // Alice1 has a different config value.
- alice1
- .set_config_bool(Config::MdnsEnabled, !mdns_enabled)
- .await?;
- // This changes nothing, but still sends a sync message.
- alice0
- .set_config_bool(Config::MdnsEnabled, mdns_enabled)
- .await?;
- sync(&alice0, &alice1).await;
- assert_eq!(
- alice1.get_config_bool(Config::MdnsEnabled).await?,
- mdns_enabled
- );
-
- // Reset to default. Test that it's not synced because defaults may differ across client
- // versions.
- alice0.set_config(Config::MdnsEnabled, None).await?;
- alice0.set_config_bool(Config::MdnsEnabled, false).await?;
- sync(&alice0, &alice1).await;
- assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
-
- for key in [Config::ShowEmails, Config::MvboxMove] {
- let val = alice0.get_config_bool(key).await?;
- alice0.set_config_bool(key, !val).await?;
- sync(&alice0, &alice1).await;
- assert_eq!(alice1.get_config_bool(key).await?, !val);
- }
-
- // `Config::SyncMsgs` mustn't be synced.
- alice0.set_config_bool(Config::SyncMsgs, false).await?;
- alice0.set_config_bool(Config::SyncMsgs, true).await?;
- alice0.set_config_bool(Config::MdnsEnabled, true).await?;
- sync(&alice0, &alice1).await;
- assert!(alice1.get_config_bool(Config::MdnsEnabled).await?);
-
- // Usual sync scenario.
- async fn test_config_str(
- alice0: &TestContext,
- alice1: &TestContext,
- key: Config,
- val: &str,
- ) -> Result<()> {
- alice0.set_config(key, Some(val)).await?;
- sync(alice0, alice1).await;
- assert_eq!(alice1.get_config(key).await?, Some(val.to_string()));
- Ok(())
- }
- test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?;
- test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?;
-
- assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
- let file = alice0.dir.path().join("avatar.png");
- let bytes = include_bytes!("../test-data/image/avatar64x64.png");
- tokio::fs::write(&file, bytes).await?;
- alice0
- .set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
- .await?;
- sync(&alice0, &alice1).await;
- // There was a bug that a sync message creates the self-chat with the user avatar instead of
- // the special icon and that remains so when the self-chat becomes user-visible. Let's check
- // this.
- let self_chat = alice0.get_self_chat().await;
- let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap();
- assert_eq!(
- self_chat_avatar_path,
- alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE)
- );
- assert!(alice1
- .get_config(Config::Selfavatar)
- .await?
- .filter(|path| path.ends_with(".png"))
- .is_some());
- alice0.set_config(Config::Selfavatar, None).await?;
- sync(&alice0, &alice1).await;
- assert!(alice1.get_config(Config::Selfavatar).await?.is_none());
-
- Ok(())
- }
-
- /// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_no_sync_on_self_sent_msg() -> Result<()> {
- let mut tcm = TestContextManager::new();
- let alice0 = &tcm.alice().await;
- let alice1 = &tcm.alice().await;
- for a in [alice0, alice1] {
- a.set_config_bool(Config::SyncMsgs, true).await?;
- }
-
- let status = "Synced via usual message";
- alice0.set_config(Config::Selfstatus, Some(status)).await?;
- alice0.send_sync_msg().await?;
- alice0.pop_sent_sync_msg().await;
- let status1 = "Synced via sync message";
- alice1.set_config(Config::Selfstatus, Some(status1)).await?;
- tcm.send_recv(alice0, alice1, "hi Alice!").await;
- assert_eq!(
- alice1.get_config(Config::Selfstatus).await?,
- Some(status.to_string())
- );
- sync(alice1, alice0).await;
- assert_eq!(
- alice0.get_config(Config::Selfstatus).await?,
- Some(status1.to_string())
- );
-
- // Need a chat with another contact to send self-avatar.
- let bob = &tcm.bob().await;
- let a0b_chat_id = tcm.send_recv_accept(bob, alice0, "hi").await.chat_id;
- let file = alice0.dir.path().join("avatar.png");
- let bytes = include_bytes!("../test-data/image/avatar64x64.png");
- tokio::fs::write(&file, bytes).await?;
- alice0
- .set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
- .await?;
- alice0.send_sync_msg().await?;
- alice0.pop_sent_sync_msg().await;
- let file = alice1.dir.path().join("avatar.jpg");
- let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
- tokio::fs::write(&file, bytes).await?;
- alice1
- .set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
- .await?;
- let sent_msg = alice0.send_text(a0b_chat_id, "hi").await;
- alice1.recv_msg(&sent_msg).await;
- assert!(alice1
- .get_config(Config::Selfavatar)
- .await?
- .filter(|path| path.ends_with(".png"))
- .is_some());
- sync(alice1, alice0).await;
- assert!(alice0
- .get_config(Config::Selfavatar)
- .await?
- .filter(|path| path.ends_with(".jpg"))
- .is_some());
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_event_config_synced() -> Result<()> {
- let alice0 = TestContext::new_alice().await;
- let alice1 = TestContext::new_alice().await;
- for a in [&alice0, &alice1] {
- a.set_config_bool(Config::SyncMsgs, true).await?;
- }
-
- alice0
- .set_config(Config::Displayname, Some("Alice Sync"))
- .await?;
- alice0
- .evtracker
- .get_matching(|e| {
- matches!(
- e,
- EventType::ConfigSynced {
- key: Config::Displayname
- }
- )
- })
- .await;
- sync(&alice0, &alice1).await;
- assert_eq!(
- alice1.get_config(Config::Displayname).await?,
- Some("Alice Sync".to_string())
- );
- alice1
- .evtracker
- .get_matching(|e| {
- matches!(
- e,
- EventType::ConfigSynced {
- key: Config::Displayname
- }
- )
- })
- .await;
-
- alice0.set_config(Config::Displayname, None).await?;
- alice0
- .evtracker
- .get_matching(|e| {
- matches!(
- e,
- EventType::ConfigSynced {
- key: Config::Displayname
- }
- )
- })
- .await;
-
- Ok(())
- }
-}
+mod config_tests;
diff --git a/src/config/config_tests.rs b/src/config/config_tests.rs
new file mode 100644
index 0000000000..c35e9a0230
--- /dev/null
+++ b/src/config/config_tests.rs
@@ -0,0 +1,392 @@
+use num_traits::FromPrimitive;
+
+use super::*;
+use crate::test_utils::{sync, TestContext, TestContextManager};
+
+#[test]
+fn test_to_string() {
+ assert_eq!(Config::MailServer.to_string(), "mail_server");
+ assert_eq!(Config::from_str("mail_server"), Ok(Config::MailServer));
+
+ assert_eq!(Config::SysConfigKeys.to_string(), "sys.config_keys");
+ assert_eq!(
+ Config::from_str("sys.config_keys"),
+ Ok(Config::SysConfigKeys)
+ );
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_set_config_addr() {
+ let t = TestContext::new().await;
+
+ // Test that uppercase address get lowercased.
+ assert!(t
+ .set_config(Config::Addr, Some("Foobar@eXample.oRg"))
+ .await
+ .is_ok());
+ assert_eq!(
+ t.get_config(Config::Addr).await.unwrap().unwrap(),
+ "foobar@example.org"
+ );
+}
+
+/// Tests that "bot" config can only be set to "0" or "1".
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_set_config_bot() {
+ let t = TestContext::new().await;
+
+ assert!(t.set_config(Config::Bot, None).await.is_ok());
+ assert!(t.set_config(Config::Bot, Some("0")).await.is_ok());
+ assert!(t.set_config(Config::Bot, Some("1")).await.is_ok());
+ assert!(t.set_config(Config::Bot, Some("2")).await.is_err());
+ assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_media_quality_config_option() {
+ let t = TestContext::new().await;
+ let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
+ assert_eq!(media_quality, 0);
+ let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
+ assert_eq!(media_quality, constants::MediaQuality::Balanced);
+
+ t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
+
+ let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
+ assert_eq!(media_quality, 1);
+ assert_eq!(constants::MediaQuality::Worse as i32, 1);
+ let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
+ assert_eq!(media_quality, constants::MediaQuality::Worse);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_ui_config() -> Result<()> {
+ let t = TestContext::new().await;
+
+ assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None);
+
+ t.set_ui_config("ui.android.screen_security", Some("safe"))
+ .await?;
+ assert_eq!(
+ t.get_ui_config("ui.android.screen_security").await?,
+ Some("safe".to_string())
+ );
+
+ t.set_ui_config("ui.android.screen_security", None).await?;
+ assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None);
+
+ assert!(t.set_ui_config("configured", Some("bar")).await.is_err());
+
+ Ok(())
+}
+
+/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_set_config_bool() -> Result<()> {
+ let t = TestContext::new().await;
+
+ // We need some config that defaults to true
+ let c = Config::MdnsEnabled;
+ assert_eq!(t.get_config_bool(c).await?, true);
+ t.set_config_bool(c, false).await?;
+ assert_eq!(t.get_config_bool(c).await?, false);
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_self_addrs() -> Result<()> {
+ let alice = TestContext::new_alice().await;
+
+ assert!(alice.is_self_addr("alice@example.org").await?);
+ assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]);
+ assert!(!alice.is_self_addr("alice@alice.com").await?);
+
+ // Test adding the same primary address
+ alice.set_primary_self_addr("alice@example.org").await?;
+ alice.set_primary_self_addr("Alice@Example.Org").await?;
+ assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);
+
+ // Test adding a new (primary) self address
+ // The address is trimmed during configure by `LoginParam::from_database()`,
+ // so `set_primary_self_addr()` doesn't have to trim it.
+ alice.set_primary_self_addr("Alice@alice.com").await?;
+ assert!(alice.is_self_addr("aliCe@example.org").await?);
+ assert!(alice.is_self_addr("alice@alice.com").await?);
+ assert_eq!(
+ alice.get_all_self_addrs().await?,
+ vec!["Alice@alice.com", "Alice@Example.Org"]
+ );
+
+ // Check that the entry is not duplicated
+ alice.set_primary_self_addr("alice@alice.com").await?;
+ alice.set_primary_self_addr("alice@alice.com").await?;
+ assert_eq!(
+ alice.get_all_self_addrs().await?,
+ vec!["alice@alice.com", "Alice@Example.Org"]
+ );
+
+ // Test switching back
+ alice.set_primary_self_addr("alice@example.org").await?;
+ assert_eq!(
+ alice.get_all_self_addrs().await?,
+ vec!["alice@example.org", "alice@alice.com"]
+ );
+
+ // Test setting a new primary self address, the previous self address
+ // should be kept as a secondary self address
+ alice.set_primary_self_addr("alice@alice.xyz").await?;
+ assert_eq!(
+ alice.get_all_self_addrs().await?,
+ vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"]
+ );
+ assert!(alice.is_self_addr("alice@example.org").await?);
+ assert!(alice.is_self_addr("alice@alice.com").await?);
+ assert!(alice.is_self_addr("Alice@alice.xyz").await?);
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_mdns_default_behaviour() -> Result<()> {
+ let t = &TestContext::new_alice().await;
+ assert!(t.should_request_mdns().await?);
+ assert!(t.should_send_mdns().await?);
+ assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
+ // The setting should be displayed correctly.
+ assert!(t.get_config_bool(Config::MdnsEnabled).await?);
+
+ t.set_config_bool(Config::Bot, true).await?;
+ assert!(!t.should_request_mdns().await?);
+ assert!(t.should_send_mdns().await?);
+ assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
+ assert!(t.get_config_bool(Config::MdnsEnabled).await?);
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_delete_server_after_default() -> Result<()> {
+ let t = &TestContext::new_alice().await;
+
+ // Check that the settings are displayed correctly.
+ assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string()));
+ assert_eq!(
+ t.get_config(Config::DeleteServerAfter).await?,
+ Some("0".to_string())
+ );
+
+ // Leaving emails on the server even w/o `BccSelf` is a good default at least because other
+ // MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail
+ // does).
+ t.set_config_bool(Config::BccSelf, false).await?;
+ assert_eq!(
+ t.get_config(Config::DeleteServerAfter).await?,
+ Some("0".to_string())
+ );
+ Ok(())
+}
+
+const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_sync() -> Result<()> {
+ let alice0 = TestContext::new_alice().await;
+ let alice1 = TestContext::new_alice().await;
+ for a in [&alice0, &alice1] {
+ a.set_config_bool(Config::SyncMsgs, true).await?;
+ }
+
+ let mdns_enabled = alice0.get_config_bool(Config::MdnsEnabled).await?;
+ // Alice1 has a different config value.
+ alice1
+ .set_config_bool(Config::MdnsEnabled, !mdns_enabled)
+ .await?;
+ // This changes nothing, but still sends a sync message.
+ alice0
+ .set_config_bool(Config::MdnsEnabled, mdns_enabled)
+ .await?;
+ sync(&alice0, &alice1).await;
+ assert_eq!(
+ alice1.get_config_bool(Config::MdnsEnabled).await?,
+ mdns_enabled
+ );
+
+ // Reset to default. Test that it's not synced because defaults may differ across client
+ // versions.
+ alice0.set_config(Config::MdnsEnabled, None).await?;
+ alice0.set_config_bool(Config::MdnsEnabled, false).await?;
+ sync(&alice0, &alice1).await;
+ assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
+
+ for key in [Config::ShowEmails, Config::MvboxMove] {
+ let val = alice0.get_config_bool(key).await?;
+ alice0.set_config_bool(key, !val).await?;
+ sync(&alice0, &alice1).await;
+ assert_eq!(alice1.get_config_bool(key).await?, !val);
+ }
+
+ // `Config::SyncMsgs` mustn't be synced.
+ alice0.set_config_bool(Config::SyncMsgs, false).await?;
+ alice0.set_config_bool(Config::SyncMsgs, true).await?;
+ alice0.set_config_bool(Config::MdnsEnabled, true).await?;
+ sync(&alice0, &alice1).await;
+ assert!(alice1.get_config_bool(Config::MdnsEnabled).await?);
+
+ // Usual sync scenario.
+ async fn test_config_str(
+ alice0: &TestContext,
+ alice1: &TestContext,
+ key: Config,
+ val: &str,
+ ) -> Result<()> {
+ alice0.set_config(key, Some(val)).await?;
+ sync(alice0, alice1).await;
+ assert_eq!(alice1.get_config(key).await?, Some(val.to_string()));
+ Ok(())
+ }
+ test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?;
+ test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?;
+
+ assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
+ let file = alice0.dir.path().join("avatar.png");
+ let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
+ tokio::fs::write(&file, bytes).await?;
+ alice0
+ .set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
+ .await?;
+ sync(&alice0, &alice1).await;
+ // There was a bug that a sync message creates the self-chat with the user avatar instead of
+ // the special icon and that remains so when the self-chat becomes user-visible. Let's check
+ // this.
+ let self_chat = alice0.get_self_chat().await;
+ let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap();
+ assert_eq!(
+ self_chat_avatar_path,
+ alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE)
+ );
+ assert!(alice1
+ .get_config(Config::Selfavatar)
+ .await?
+ .filter(|path| path.ends_with(".png"))
+ .is_some());
+ alice0.set_config(Config::Selfavatar, None).await?;
+ sync(&alice0, &alice1).await;
+ assert!(alice1.get_config(Config::Selfavatar).await?.is_none());
+
+ Ok(())
+}
+
+/// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_no_sync_on_self_sent_msg() -> Result<()> {
+ let mut tcm = TestContextManager::new();
+ let alice0 = &tcm.alice().await;
+ let alice1 = &tcm.alice().await;
+ for a in [alice0, alice1] {
+ a.set_config_bool(Config::SyncMsgs, true).await?;
+ }
+
+ let status = "Synced via usual message";
+ alice0.set_config(Config::Selfstatus, Some(status)).await?;
+ alice0.send_sync_msg().await?;
+ alice0.pop_sent_sync_msg().await;
+ let status1 = "Synced via sync message";
+ alice1.set_config(Config::Selfstatus, Some(status1)).await?;
+ tcm.send_recv(alice0, alice1, "hi Alice!").await;
+ assert_eq!(
+ alice1.get_config(Config::Selfstatus).await?,
+ Some(status.to_string())
+ );
+ sync(alice1, alice0).await;
+ assert_eq!(
+ alice0.get_config(Config::Selfstatus).await?,
+ Some(status1.to_string())
+ );
+
+ // Need a chat with another contact to send self-avatar.
+ let bob = &tcm.bob().await;
+ let a0b_chat_id = tcm.send_recv_accept(bob, alice0, "hi").await.chat_id;
+ let file = alice0.dir.path().join("avatar.png");
+ let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
+ tokio::fs::write(&file, bytes).await?;
+ alice0
+ .set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
+ .await?;
+ alice0.send_sync_msg().await?;
+ alice0.pop_sent_sync_msg().await;
+ let file = alice1.dir.path().join("avatar.jpg");
+ let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
+ tokio::fs::write(&file, bytes).await?;
+ alice1
+ .set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
+ .await?;
+ let sent_msg = alice0.send_text(a0b_chat_id, "hi").await;
+ alice1.recv_msg(&sent_msg).await;
+ assert!(alice1
+ .get_config(Config::Selfavatar)
+ .await?
+ .filter(|path| path.ends_with(".png"))
+ .is_some());
+ sync(alice1, alice0).await;
+ assert!(alice0
+ .get_config(Config::Selfavatar)
+ .await?
+ .filter(|path| path.ends_with(".jpg"))
+ .is_some());
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_event_config_synced() -> Result<()> {
+ let alice0 = TestContext::new_alice().await;
+ let alice1 = TestContext::new_alice().await;
+ for a in [&alice0, &alice1] {
+ a.set_config_bool(Config::SyncMsgs, true).await?;
+ }
+
+ alice0
+ .set_config(Config::Displayname, Some("Alice Sync"))
+ .await?;
+ alice0
+ .evtracker
+ .get_matching(|e| {
+ matches!(
+ e,
+ EventType::ConfigSynced {
+ key: Config::Displayname
+ }
+ )
+ })
+ .await;
+ sync(&alice0, &alice1).await;
+ assert_eq!(
+ alice1.get_config(Config::Displayname).await?,
+ Some("Alice Sync".to_string())
+ );
+ alice1
+ .evtracker
+ .get_matching(|e| {
+ matches!(
+ e,
+ EventType::ConfigSynced {
+ key: Config::Displayname
+ }
+ )
+ })
+ .await;
+
+ alice0.set_config(Config::Displayname, None).await?;
+ alice0
+ .evtracker
+ .get_matching(|e| {
+ matches!(
+ e,
+ EventType::ConfigSynced {
+ key: Config::Displayname
+ }
+ )
+ })
+ .await;
+
+ Ok(())
+}
diff --git a/src/configure.rs b/src/configure.rs
index 1fd561fbc0..f5b957cd4b 100644
--- a/src/configure.rs
+++ b/src/configure.rs
@@ -16,7 +16,7 @@ pub(crate) mod server_params;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
-use deltachat_contact_tools::EmailAddress;
+use deltachat_contact_tools::{addr_normalize, EmailAddress};
use futures::FutureExt;
use futures_lite::FutureExt as _;
use percent_encoding::utf8_percent_encode;
@@ -28,17 +28,18 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::LogExt;
+pub use crate::login_param::EnteredLoginParam;
use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
- ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
+ ConnectionCandidate, EnteredCertificateChecks, ProxyConfig,
};
use crate::message::Message;
use crate::oauth2::get_oauth2_addr;
-use crate::provider::{Protocol, Socket, UsernamePattern};
+use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::sync::Sync::*;
use crate::tools::time;
-use crate::{chat, e2ee, provider};
+use crate::{chat, provider};
use crate::{stock_str, EventType};
use deltachat_contact_tools::addr_cmp;
@@ -61,11 +62,62 @@ macro_rules! progress {
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> Result {
- self.sql.get_raw_config_bool("configured").await
+ self.sql.exists("SELECT COUNT(*) FROM transports", ()).await
}
- /// Configures this account with the currently set parameters.
+ /// Configures this account with the currently provided parameters.
+ ///
+ /// Deprecated since 2025-02; use `add_transport_from_qr()`
+ /// or `add_or_update_transport()` instead.
pub async fn configure(&self) -> Result<()> {
+ let mut param = EnteredLoginParam::load(self).await?;
+
+ self.add_transport_inner(&mut param).await
+ }
+
+ /// Configures a new email account using the provided parameters
+ /// and adds it as a transport.
+ ///
+ /// If the email address is the same as an existing transport,
+ /// then this existing account will be reconfigured instead of a new one being added.
+ ///
+ /// This function stops and starts IO as needed.
+ ///
+ /// Usually it will be enough to only set `addr` and `imap.password`,
+ /// and all the other settings will be autoconfigured.
+ ///
+ /// During configuration, ConfigureProgress events are emitted;
+ /// they indicate a successful configuration as well as errors
+ /// and may be used to create a progress bar.
+ /// This function will return after configuration is finished.
+ ///
+ /// If configuration is successful,
+ /// the working server parameters will be saved
+ /// and used for connecting to the server.
+ /// The parameters entered by the user will be saved separately
+ /// so that they can be prefilled when the user opens the server-configuration screen again.
+ ///
+ /// See also:
+ /// - [Self::is_configured()] to check whether there is
+ /// at least one working transport.
+ /// - [Self::add_transport_from_qr()] to add a transport
+ /// from a server encoded in a QR code.
+ /// - [Self::list_transports()] to get a list of all configured transports.
+ /// - [Self::delete_transport()] to remove a transport.
+ pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
+ self.stop_io().await;
+ let result = self.add_transport_inner(param).await;
+ if result.is_err() {
+ if let Ok(true) = self.is_configured().await {
+ self.start_io().await;
+ }
+ return result;
+ }
+ self.start_io().await;
+ Ok(())
+ }
+
+ async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
ensure!(
!self.scheduler.is_running().await,
"cannot configure, already running"
@@ -74,55 +126,116 @@ impl Context {
self.sql.is_open().await,
"cannot configure, database not opened."
);
+ param.addr = addr_normalize(¶m.addr);
+ let old_addr = self.get_config(Config::ConfiguredAddr).await?;
+ if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), ¶m.addr) {
+ let error_msg = "Changing your email address is not supported right now. Check back in a few months!";
+ progress!(self, 0, Some(error_msg.to_string()));
+ bail!(error_msg);
+ }
let cancel_channel = self.alloc_ongoing().await?;
let res = self
- .inner_configure()
+ .inner_configure(param)
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
.await;
self.free_ongoing().await;
if let Err(err) = res.as_ref() {
- progress!(
- self,
- 0,
- Some(
- stock_str::configuration_failed(
- self,
- // We are using Anyhow's .context() and to show the
- // inner error, too, we need the {:#}:
- &format!("{err:#}"),
- )
- .await
- )
- );
+ // We are using Anyhow's .context() and to show the
+ // inner error, too, we need the {:#}:
+ let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await;
+ progress!(self, 0, Some(error_msg.clone()));
+ bail!(error_msg);
} else {
+ param.save(self).await?;
progress!(self, 1000);
}
res
}
- async fn inner_configure(&self) -> Result<()> {
+ /// Adds a new email account as a transport
+ /// using the server encoded in the QR code.
+ /// See [Self::add_or_update_transport].
+ pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
+ self.stop_io().await;
+
+ // This code first sets the deprecated Config::Addr, Config::MailPw, etc.
+ // and then calls configure(), which loads them again.
+ // At some point, we will remove configure()
+ // and then simplify the code
+ // to directly create an EnteredLoginParam.
+ let result = async move {
+ match crate::qr::check_qr(self, qr).await? {
+ crate::qr::Qr::Account { .. } => crate::qr::set_account_from_qr(self, qr).await?,
+ crate::qr::Qr::Login { address, options } => {
+ crate::qr::configure_from_login_qr(self, &address, options).await?
+ }
+ _ => bail!("QR code does not contain account"),
+ }
+ self.configure().await?;
+ Ok(())
+ }
+ .await;
+
+ if result.is_err() {
+ if let Ok(true) = self.is_configured().await {
+ self.start_io().await;
+ }
+ return result;
+ }
+ self.start_io().await;
+ Ok(())
+ }
+
+ /// Returns the list of all email accounts that are used as a transport in the current profile.
+ /// Use [Self::add_or_update_transport()] to add or change a transport
+ /// and [Self::delete_transport()] to delete a transport.
+ pub async fn list_transports(&self) -> Result> {
+ let transports = self
+ .sql
+ .query_map(
+ "SELECT entered_param FROM transports",
+ (),
+ |row| row.get::<_, String>(0),
+ |rows| {
+ rows.flatten()
+ .map(|s| Ok(serde_json::from_str(&s)?))
+ .collect::>>()
+ },
+ )
+ .await?;
+
+ Ok(transports)
+ }
+
+ /// Removes the transport with the specified email address
+ /// (i.e. [EnteredLoginParam::addr]).
+ #[expect(clippy::unused_async)]
+ pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
+ bail!("Adding and removing additional transports is not supported yet. Check back in a few months!")
+ }
+
+ async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
info!(self, "Configure ...");
- let param = EnteredLoginParam::load(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
- let configured_param = configure(self, ¶m).await?;
+ let provider = configure(self, param).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
- on_configure_completed(self, configured_param, old_addr).await?;
+ on_configure_completed(self, provider, old_addr).await?;
Ok(())
}
}
async fn on_configure_completed(
context: &Context,
- param: ConfiguredLoginParam,
+ provider: Option<&'static Provider>,
old_addr: Option,
) -> Result<()> {
- if let Some(provider) = param.provider {
+ if let Some(provider) = provider {
if let Some(config_defaults) = provider.config_defaults {
for def in config_defaults {
if !context.config_exists(def.key).await? {
@@ -185,8 +298,7 @@ async fn get_configured_param(
param.smtp.password.clone()
};
- let proxy_config = param.proxy_config.clone();
- let proxy_enabled = proxy_config.is_some();
+ let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
let mut addr = param.addr.clone();
if param.oauth2 {
@@ -345,7 +457,6 @@ async fn get_configured_param(
.collect(),
smtp_user: param.smtp.user.clone(),
smtp_password,
- proxy_config: param.proxy_config.clone(),
provider,
certificate_checks: match param.certificate_checks {
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
@@ -360,14 +471,15 @@ async fn get_configured_param(
Ok(configured_login_param)
}
-async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result {
+async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result> {
progress!(ctx, 1);
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
let configured_param = get_configured_param(ctx, param).await?;
- let strict_tls = configured_param.strict_tls();
+ let proxy_config = ProxyConfig::load(ctx).await?;
+ let strict_tls = configured_param.strict_tls(proxy_config.is_some());
progress!(ctx, 550);
@@ -377,15 +489,15 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result Result Result session,
- Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await),
+ Err(err) => bail!(
+ "{}",
+ nicer_configuration_error(ctx, format!("{err:#}")).await
+ ),
};
progress!(ctx, 850);
@@ -442,7 +557,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result Result Result = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
+pub static DC_VERSION_STR: LazyLock =
+ LazyLock::new(|| env!("CARGO_PKG_VERSION").to_string());
/// Set of characters to percent-encode in email addresses and names.
pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
@@ -58,25 +60,6 @@ pub enum MediaQuality {
Worse = 1,
}
-/// Type of the key to generate.
-#[derive(
- Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
-)]
-#[repr(u8)]
-pub enum KeyGenType {
- #[default]
- Default = 0,
-
- /// 2048-bit RSA.
- Rsa2048 = 1,
-
- /// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption.
- Ed25519 = 2,
-
- /// 4096-bit RSA.
- Rsa4096 = 3,
-}
-
/// Video chat URL type.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
@@ -105,7 +88,6 @@ pub const DC_GCL_NO_SPECIALS: usize = 0x02;
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
-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
@@ -197,16 +179,15 @@ pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
/// if none of these flags are set, the default is chosen
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
-/// How many existing messages shall be fetched after configuration.
-pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
-
// max. weight of images to send w/o recoding
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
pub const WORSE_IMAGE_BYTES: usize = 130_000;
-// max. width/height of an avatar
-pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256;
+// max. width/height and bytes of an avatar
+pub(crate) const BALANCED_AVATAR_SIZE: u32 = 512;
+pub(crate) const BALANCED_AVATAR_BYTES: usize = 60_000;
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
+pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outlook servers don't allowing headers larger than 32k.
// max. width/height of images scaled down because of being too huge
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
@@ -234,6 +215,23 @@ pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
/// on mobile devices. See also [`crate::chat::CantSendReason::SecurejoinWait`].
pub(crate) const SECUREJOIN_WAIT_TIMEOUT: u64 = 15;
+// To make text edits clearer for Non-Delta-MUA or old Delta Chats, edited text will be prefixed by EDITED_PREFIX.
+// Newer Delta Chats will remove the prefix as needed.
+pub(crate) const EDITED_PREFIX: &str = "✏️";
+
+// Strings needed to render the Autocrypt Setup Message.
+// Left untranslated as not being supported/recommended workflow and as translations would require deep knowledge.
+pub(crate) const ASM_SUBJECT: &str = "Autocrypt Setup Message";
+pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \
+ used to transfer your end-to-end setup between clients.
+
+ To decrypt and use your setup, \
+ open the message in an Autocrypt-compliant client \
+ and enter the setup code presented on the generating device.
+
+ If you see this message in a chatmail client (Delta Chat, Arcane Chat, Delta Touch ...), \
+ use \"Settings / Add Second Device\" instead.";
+
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;
@@ -249,16 +247,6 @@ mod tests {
assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap());
}
- #[test]
- fn test_keygentype_values() {
- // values may be written to disk and must not change
- assert_eq!(KeyGenType::Default, KeyGenType::default());
- assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
- assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
- assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
- assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap());
- }
-
#[test]
fn test_showemails_values() {
// values may be written to disk and must not change
diff --git a/src/contact.rs b/src/contact.rs
index a27b8e3057..7f923348fe 100644
--- a/src/contact.rs
+++ b/src/contact.rs
@@ -25,7 +25,7 @@ use crate::blob::BlobObject;
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
use crate::color::str_to_color;
use crate::config::Config;
-use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
+use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
@@ -95,6 +95,50 @@ impl ContactId {
self.0
}
+ /// Sets display name for existing contact.
+ ///
+ /// Display name may be an empty string,
+ /// in which case the name displayed in the UI
+ /// for this contact will switch to the
+ /// contact's authorized name.
+ pub async fn set_name(self, context: &Context, name: &str) -> Result<()> {
+ let addr = context
+ .sql
+ .transaction(|transaction| {
+ let is_changed = transaction.execute(
+ "UPDATE contacts SET name=?1 WHERE id=?2 AND name!=?1",
+ (name, self),
+ )? > 0;
+ if is_changed {
+ update_chat_names(context, transaction, self)?;
+ let addr = transaction.query_row(
+ "SELECT addr FROM contacts WHERE id=?",
+ (self,),
+ |row| {
+ let addr: String = row.get(0)?;
+ Ok(addr)
+ },
+ )?;
+ Ok(Some(addr))
+ } else {
+ Ok(None)
+ }
+ })
+ .await?;
+
+ if let Some(addr) = addr {
+ chat::sync(
+ context,
+ chat::SyncId::ContactAddr(addr.to_string()),
+ chat::SyncAction::Rename(name.to_string()),
+ )
+ .await
+ .log_err(context)
+ .ok();
+ }
+ Ok(())
+ }
+
/// Mark contact as bot.
pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> {
context
@@ -247,7 +291,16 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result
+ // is merged.
+ Ok(contact_tools::make_vcard(&vcard_contacts)
+ .trim_end()
+ .to_string())
}
/// Imports contacts from the given vCard.
@@ -496,7 +549,11 @@ pub enum Origin {
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
SecurejoinInvited = 0x0100_0000,
- /// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
+ /// Set on Bob's side for contacts scanned from a QR code.
+ /// Only means the contact has been scanned from the QR code,
+ /// but does not mean that securejoin succeeded
+ /// or the key has not changed since the last scan.
+ /// Getting the current key verification status requires calling contact_is_verified() !
SecurejoinJoined = 0x0200_0000,
/// contact added manually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
@@ -830,44 +887,48 @@ impl Contact {
let mut update_addr = false;
- let row_id = context.sql.transaction(|transaction| {
- let row = transaction.query_row(
- "SELECT id, name, addr, origin, authname
+ let row_id = context
+ .sql
+ .transaction(|transaction| {
+ let row = transaction
+ .query_row(
+ "SELECT id, name, addr, origin, authname
FROM contacts WHERE addr=? COLLATE NOCASE",
- [addr.to_string()],
- |row| {
- let row_id: isize = row.get(0)?;
- let row_name: String = row.get(1)?;
- let row_addr: String = row.get(2)?;
- let row_origin: Origin = row.get(3)?;
- let row_authname: String = row.get(4)?;
-
- Ok((row_id, row_name, row_addr, row_origin, row_authname))
- }).optional()?;
-
- let row_id;
- if let Some((id, row_name, row_addr, row_origin, row_authname)) = row {
- let update_name = manual && name != row_name;
- let update_authname = !manual
- && name != row_authname
- && !name.is_empty()
- && (origin >= row_origin
- || origin == Origin::IncomingUnknownFrom
- || row_authname.is_empty());
-
- row_id = u32::try_from(id)?;
- if origin >= row_origin && addr.as_ref() != row_addr {
- update_addr = true;
- }
- if update_name || update_authname || update_addr || origin > row_origin {
- let new_name = if update_name {
- name.to_string()
- } else {
- row_name
- };
+ (addr,),
+ |row| {
+ let row_id: u32 = row.get(0)?;
+ let row_name: String = row.get(1)?;
+ let row_addr: String = row.get(2)?;
+ let row_origin: Origin = row.get(3)?;
+ let row_authname: String = row.get(4)?;
- transaction
- .execute(
+ Ok((row_id, row_name, row_addr, row_origin, row_authname))
+ },
+ )
+ .optional()?;
+
+ let row_id;
+ if let Some((id, row_name, row_addr, row_origin, row_authname)) = row {
+ let update_name = manual && name != row_name;
+ let update_authname = !manual
+ && name != row_authname
+ && !name.is_empty()
+ && (origin >= row_origin
+ || origin == Origin::IncomingUnknownFrom
+ || row_authname.is_empty());
+
+ row_id = id;
+ if origin >= row_origin && addr.as_ref() != row_addr {
+ update_addr = true;
+ }
+ if update_name || update_authname || update_addr || origin > row_origin {
+ let new_name = if update_name {
+ name.to_string()
+ } else {
+ row_name
+ };
+
+ transaction.execute(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
(
new_name,
@@ -886,88 +947,38 @@ impl Contact {
} else {
row_authname
},
- row_id
+ row_id,
),
)?;
- if update_name || update_authname {
- // Update the contact name also if it is used as a group name.
- // This is one of the few duplicated data, however, getting the chat list is easier this way.
- let chat_id: Option = transaction.query_row(
- "SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
- (Chattype::Single, isize::try_from(row_id)?),
- |row| {
- let chat_id: ChatId = row.get(0)?;
- Ok(chat_id)
- }
- ).optional()?;
-
- if let Some(chat_id) = chat_id {
+ if update_name || update_authname {
let contact_id = ContactId::new(row_id);
- let (addr, name, authname) =
- transaction.query_row(
- "SELECT addr, name, authname
- FROM contacts
- WHERE id=?",
- (contact_id,),
- |row| {
- let addr: String = row.get(0)?;
- let name: String = row.get(1)?;
- let authname: String = row.get(2)?;
- Ok((addr, name, authname))
- })?;
-
- let chat_name = if !name.is_empty() {
- name
- } else if !authname.is_empty() {
- authname
- } else {
- addr
- };
-
- let count = transaction.execute(
- "UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
- (chat_name, chat_id))?;
-
- if count > 0 {
- // Chat name updated
- context.emit_event(EventType::ChatModified(chat_id));
- chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
- }
+ update_chat_names(context, transaction, contact_id)?;
}
+ sth_modified = Modifier::Modified;
}
- sth_modified = Modifier::Modified;
- }
- } else {
- let update_name = manual;
- let update_authname = !manual;
+ } else {
+ let update_name = manual;
+ let update_authname = !manual;
- transaction
- .execute(
+ transaction.execute(
"INSERT INTO contacts (name, addr, origin, authname)
VALUES (?, ?, ?, ?);",
- (
- if update_name {
- name.to_string()
- } else {
- "".to_string()
- },
+ (
+ if update_name { &name } else { "" },
&addr,
origin,
- if update_authname {
- name.to_string()
- } else {
- "".to_string()
- }
+ if update_authname { &name } else { "" },
),
)?;
- sth_modified = Modifier::Created;
- row_id = u32::try_from(transaction.last_insert_rowid())?;
- info!(context, "Added contact id={row_id} addr={addr}.");
- }
- Ok(row_id)
- }).await?;
+ sth_modified = Modifier::Created;
+ row_id = u32::try_from(transaction.last_insert_rowid())?;
+ info!(context, "Added contact id={row_id} addr={addr}.");
+ }
+ Ok(row_id)
+ })
+ .await?;
let contact_id = ContactId::new(row_id);
@@ -1030,9 +1041,8 @@ impl Contact {
///
/// `listflags` is a combination of flags:
/// - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
- /// - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
- /// if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned.
- /// `query` is a string to filter the list.
+ ///
+ /// `query` is a string to filter the list.
pub async fn get_all(
context: &Context,
listflags: u32,
@@ -1045,14 +1055,13 @@ impl Contact {
.collect::>();
let mut add_self = false;
let mut ret = Vec::new();
- let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
let minimal_origin = if context.get_config_bool(Config::Bot).await? {
Origin::Unknown
} else {
Origin::IncomingReplyTo
};
- if flag_verified_only || query.is_some() {
+ if query.is_some() {
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
context
.sql
@@ -1063,14 +1072,12 @@ impl Contact {
AND c.origin>=? \
AND c.blocked=0 \
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
- AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \
ORDER BY c.last_seen DESC, c.id DESC;",
(
ContactId::LAST_SPECIAL,
minimal_origin,
&s3str_like_cmd,
&s3str_like_cmd,
- if flag_verified_only { 0i32 } else { 1i32 },
),
|row| {
let id: ContactId = row.get(0)?;
@@ -1266,9 +1273,16 @@ impl Contact {
.map(|k| k.dc_fingerprint().to_string())
.unwrap_or_default();
if addr < peerstate.addr {
- cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
cat_fingerprint(
&mut ret,
+ &stock_str::self_msg(context).await,
+ &addr,
+ &fingerprint_self,
+ "",
+ );
+ cat_fingerprint(
+ &mut ret,
+ contact.get_display_name(),
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
@@ -1276,11 +1290,18 @@ impl Contact {
} else {
cat_fingerprint(
&mut ret,
+ contact.get_display_name(),
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
- cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
+ cat_fingerprint(
+ &mut ret,
+ &stock_str::self_msg(context).await,
+ &addr,
+ &fingerprint_self,
+ "",
+ );
}
Ok(ret)
@@ -1381,16 +1402,13 @@ impl Contact {
&self.addr
}
- /// Get a summary of authorized name and address.
- ///
- /// The returned string is either "Name (email@domain.com)" or just
- /// "email@domain.com" if the name is unset.
+ /// Get authorized name or address.
///
/// This string is suitable for sending over email
/// as it does not leak the locally set name.
- pub fn get_authname_n_addr(&self) -> String {
+ pub(crate) fn get_authname_or_addr(&self) -> String {
if !self.authname.is_empty() {
- format!("{} ({})", self.authname, self.addr)
+ (&self.authname).into()
} else {
(&self.addr).into()
}
@@ -1422,7 +1440,7 @@ impl Contact {
pub async fn get_profile_image(&self, context: &Context) -> Result> {
if self.id == ContactId::SELF {
if let Some(p) = context.get_config(Config::Selfavatar).await? {
- return Ok(Some(PathBuf::from(p)));
+ return Ok(Some(PathBuf::from(p))); // get_config() calls get_abs_path() internally already
}
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
@@ -1593,6 +1611,60 @@ impl Contact {
}
}
+// Updates the names of the chats which use the contact name.
+//
+// This is one of the few duplicated data, however, getting the chat list is easier this way.
+fn update_chat_names(
+ context: &Context,
+ transaction: &rusqlite::Connection,
+ contact_id: ContactId,
+) -> Result<()> {
+ let chat_id: Option = transaction.query_row(
+ "SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
+ (Chattype::Single, contact_id),
+ |row| {
+ let chat_id: ChatId = row.get(0)?;
+ Ok(chat_id)
+ }
+ ).optional()?;
+
+ if let Some(chat_id) = chat_id {
+ let (addr, name, authname) = transaction.query_row(
+ "SELECT addr, name, authname
+ FROM contacts
+ WHERE id=?",
+ (contact_id,),
+ |row| {
+ let addr: String = row.get(0)?;
+ let name: String = row.get(1)?;
+ let authname: String = row.get(2)?;
+ Ok((addr, name, authname))
+ },
+ )?;
+
+ let chat_name = if !name.is_empty() {
+ name
+ } else if !authname.is_empty() {
+ authname
+ } else {
+ addr
+ };
+
+ let count = transaction.execute(
+ "UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
+ (chat_name, chat_id),
+ )?;
+
+ if count > 0 {
+ // Chat name updated
+ context.emit_event(EventType::ChatModified(chat_id));
+ chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
+ }
+ }
+
+ Ok(())
+}
+
pub(crate) async fn set_blocked(
context: &Context,
sync: sync::Sync,
@@ -1783,12 +1855,14 @@ pub(crate) async fn update_last_seen(
fn cat_fingerprint(
ret: &mut String,
+ name: &str,
addr: &str,
fingerprint_verified: &str,
fingerprint_unverified: &str,
) {
*ret += &format!(
- "\n\n{}:\n{}",
+ "\n\n{} ({}):\n{}",
+ name,
addr,
if !fingerprint_verified.is_empty() {
fingerprint_verified
@@ -1800,7 +1874,7 @@ fn cat_fingerprint(
&& !fingerprint_unverified.is_empty()
&& fingerprint_verified != fingerprint_unverified
{
- *ret += &format!("\n\n{addr} (alternative):\n{fingerprint_unverified}");
+ *ret += &format!("\n\n{name} (alternative):\n{fingerprint_unverified}");
}
}
diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs
index 3691d5752a..98724cc9d1 100644
--- a/src/contact/contact_tests.rs
+++ b/src/contact/contact_tests.rs
@@ -763,11 +763,11 @@ async fn test_contact_get_encrinfo() -> Result<()> {
"End-to-end encryption preferred.
Fingerprints:
-alice@example.org:
+Me (alice@example.org):
2E6F A2CB 23B5 32D7 2863
4B58 64B0 8F61 A9ED 9443
-bob@example.net:
+Bob (bob@example.net):
CCCB 5AA9 F6E1 141C 9431
65F1 DB18 B18C BCF7 0487"
);
@@ -1050,8 +1050,9 @@ async fn test_sync_create() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_make_n_import_vcard() -> Result<()> {
- let alice = &TestContext::new_alice().await;
- let bob = &TestContext::new_bob().await;
+ let mut tcm = TestContextManager::new();
+ let alice = &tcm.alice().await;
+ let bob = &tcm.bob().await;
bob.set_config(Config::Displayname, Some("Bob")).await?;
let avatar_path = bob.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
@@ -1207,16 +1208,16 @@ async fn test_reset_encryption() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
- let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
- assert_eq!(msg.get_showpadlock(), false);
-
- let msg = tcm.send_recv(bob, alice, "Hi!").await;
+ let msg = tcm.send_recv_accept(bob, alice, "Hi!").await;
assert_eq!(msg.get_showpadlock(), true);
+
+ let alice_bob_chat_id = msg.chat_id;
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
- let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
+ let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
+ let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
@@ -1235,6 +1236,7 @@ async fn test_reset_verified_encryption() -> Result<()> {
let alice_bob_chat_id = msg.chat_id;
let alice_bob_contact_id = msg.from_id;
+
alice_bob_contact_id.reset_encryption(alice).await?;
// Check that the contact is still verified after resetting encryption.
@@ -1250,7 +1252,8 @@ async fn test_reset_verified_encryption() -> Result<()> {
"bob@example.net sent a message from another device."
);
- let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
+ let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
+ let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
diff --git a/src/context.rs b/src/context.rs
index 60a3c60f32..5af9b2ee0c 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -903,12 +903,6 @@ impl Context {
}
res.insert("secondary_addrs", secondary_addrs);
- res.insert(
- "fetch_existing_msgs",
- self.get_config_int(Config::FetchExistingMsgs)
- .await?
- .to_string(),
- );
res.insert(
"fetched_existing_msgs",
self.get_config_bool(Config::FetchedExistingMsgs)
@@ -919,12 +913,6 @@ impl Context {
"show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
- res.insert(
- "save_mime_headers",
- self.get_config_bool(Config::SaveMimeHeaders)
- .await?
- .to_string(),
- );
res.insert(
"download_limit",
self.get_config_int(Config::DownloadLimit)
@@ -944,10 +932,6 @@ impl Context {
res.insert("configured_trash_folder", configured_trash_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("e2ee_enabled", e2ee_enabled.to_string());
- res.insert(
- "key_gen_type",
- 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("disable_idle", disable_idle.to_string());
@@ -1077,21 +1061,21 @@ impl Context {
)
.await?
.unwrap_or_default();
- res += &format!("num_msgs {}\n", num_msgs);
+ res += &format!("num_msgs {num_msgs}\n");
let num_chats: u32 = self
.sql
.query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ())
.await?
.unwrap_or_default();
- res += &format!("num_chats {}\n", num_chats);
+ res += &format!("num_chats {num_chats}\n");
let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
- res += &format!("db_size_bytes {}\n", db_size);
+ res += &format!("db_size_bytes {db_size}\n");
let secret_key = &load_self_secret_key(self).await?.primary_key;
let key_created = secret_key.created_at().timestamp();
- res += &format!("key_created {}\n", key_created);
+ res += &format!("key_created {key_created}\n");
// how many of the chats active in the last months are:
// - protected
@@ -1171,7 +1155,7 @@ impl Context {
id
}
};
- res += &format!("self_reporting_id {}", self_reporting_id);
+ res += &format!("self_reporting_id {self_reporting_id}");
Ok(res)
}
@@ -1480,653 +1464,4 @@ pub fn get_version_str() -> &'static str {
}
#[cfg(test)]
-mod tests {
- 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, MuteDuration};
- use crate::chatlist::Chatlist;
- use crate::constants::Chattype;
- use crate::mimeparser::SystemMessage;
- use crate::receive_imf::receive_imf;
- use crate::test_utils::{get_chat_msg, TestContext};
- use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_wrong_db() -> Result<()> {
- let tmp = tempfile::tempdir()?;
- let dbfile = tmp.path().join("db.sqlite");
- tokio::fs::write(&dbfile, b"123").await?;
- let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
-
- // Broken database is indistinguishable from encrypted one.
- assert_eq!(res.is_open().await, false);
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_get_fresh_msgs() {
- let t = TestContext::new().await;
- let fresh = t.get_fresh_msgs().await.unwrap();
- assert!(fresh.is_empty())
- }
-
- async fn receive_msg(t: &TestContext, chat: &Chat) {
- let members = get_chat_contacts(t, chat.id).await.unwrap();
- let contact = Contact::get_by_id(t, *members.first().unwrap())
- .await
- .unwrap();
- let msg = format!(
- "From: {}\n\
- To: alice@example.org\n\
- Message-ID: <{}>\n\
- Chat-Version: 1.0\n\
- Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
- \n\
- hello\n",
- contact.get_addr(),
- create_outgoing_rfc724_mid()
- );
- println!("{msg}");
- receive_imf(t, msg.as_bytes(), false).await.unwrap();
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_get_fresh_msgs_and_muted_chats() {
- // receive various mails in 3 chats
- let t = TestContext::new_alice().await;
- let bob = t.create_chat_with_contact("", "bob@g.it").await;
- let claire = t.create_chat_with_contact("", "claire@g.it").await;
- let dave = t.create_chat_with_contact("", "dave@g.it").await;
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
-
- receive_msg(&t, &bob).await;
- assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
- assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
-
- receive_msg(&t, &claire).await;
- receive_msg(&t, &claire).await;
- assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 2);
- assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
-
- receive_msg(&t, &dave).await;
- receive_msg(&t, &dave).await;
- receive_msg(&t, &dave).await;
- assert_eq!(get_chat_msgs(&t, dave.id).await.unwrap().len(), 3);
- assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
-
- // mute one of the chats
- set_muted(&t, claire.id, MuteDuration::Forever)
- .await
- .unwrap();
- assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
-
- // receive more messages
- receive_msg(&t, &bob).await;
- receive_msg(&t, &claire).await;
- receive_msg(&t, &dave).await;
- assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 3);
- assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
-
- // unmute claire again
- set_muted(&t, claire.id, MuteDuration::NotMuted)
- .await
- .unwrap();
- assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_get_fresh_msgs_and_muted_until() {
- let t = TestContext::new_alice().await;
- let bob = t.create_chat_with_contact("", "bob@g.it").await;
- receive_msg(&t, &bob).await;
- assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
-
- // chat is unmuted by default, here and in the following assert(),
- // we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
- // have the same view to the database.
- assert!(!bob.is_muted());
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
-
- // test get_fresh_msgs() with mute_until in the future
- set_muted(
- &t,
- bob.id,
- MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
- )
- .await
- .unwrap();
- let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
- assert!(bob.is_muted());
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
-
- // to test get_fresh_msgs() with mute_until in the past,
- // we need to modify the database directly
- t.sql
- .execute(
- "UPDATE chats SET muted_until=? WHERE id=?;",
- (time() - 3600, bob.id),
- )
- .await
- .unwrap();
- let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
- assert!(!bob.is_muted());
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
-
- // test get_fresh_msgs() with "forever" mute_until
- set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
- let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
- assert!(bob.is_muted());
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
-
- // to test get_fresh_msgs() with invalid mute_until (everything < -1),
- // that results in "muted forever" by definition.
- t.sql
- .execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
- .await
- .unwrap();
- let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
- assert!(!bob.is_muted());
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_muted_context() -> Result<()> {
- let t = TestContext::new_alice().await;
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
- t.set_config(Config::IsMuted, Some("1")).await?;
- let chat = t.create_chat_with_contact("", "bob@g.it").await;
- receive_msg(&t, &chat).await;
-
- // muted contexts should still show dimmed badge counters eg. in the sidebars,
- // (same as muted chats show dimmed badge counters in the chatlist)
- // therefore the fresh messages count should not be affected.
- assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_blobdir_exists() {
- let tmp = tempfile::tempdir().unwrap();
- let dbfile = tmp.path().join("db.sqlite");
- Context::new(&dbfile, 1, Events::new(), StockStrings::new())
- .await
- .unwrap();
- let blobdir = tmp.path().join("db.sqlite-blobs");
- assert!(blobdir.is_dir());
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_wrong_blogdir() {
- let tmp = tempfile::tempdir().unwrap();
- let dbfile = tmp.path().join("db.sqlite");
- let blobdir = tmp.path().join("db.sqlite-blobs");
- tokio::fs::write(&blobdir, b"123").await.unwrap();
- let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
- assert!(res.is_err());
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_sqlite_parent_not_exists() {
- let tmp = tempfile::tempdir().unwrap();
- let subdir = tmp.path().join("subdir");
- let dbfile = subdir.join("db.sqlite");
- let dbfile2 = dbfile.clone();
- Context::new(&dbfile, 1, Events::new(), StockStrings::new())
- .await
- .unwrap();
- assert!(subdir.is_dir());
- assert!(dbfile2.is_file());
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_with_empty_blobdir() {
- let tmp = tempfile::tempdir().unwrap();
- let dbfile = tmp.path().join("db.sqlite");
- let blobdir = PathBuf::new();
- let res = Context::with_blobdir(
- dbfile,
- blobdir,
- 1,
- Events::new(),
- StockStrings::new(),
- Default::default(),
- );
- assert!(res.is_err());
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_with_blobdir_not_exists() {
- let tmp = tempfile::tempdir().unwrap();
- let dbfile = tmp.path().join("db.sqlite");
- let blobdir = tmp.path().join("blobs");
- let res = Context::with_blobdir(
- dbfile,
- blobdir,
- 1,
- Events::new(),
- StockStrings::new(),
- Default::default(),
- );
- assert!(res.is_err());
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn no_crashes_on_context_deref() {
- let t = TestContext::new().await;
- std::mem::drop(t);
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_get_info() {
- let t = TestContext::new().await;
-
- let info = t.get_info().await.unwrap();
- assert!(info.contains_key("database_dir"));
- }
-
- #[test]
- fn test_get_info_no_context() {
- let info = get_info();
- assert!(info.contains_key("deltachat_core_version"));
- assert!(!info.contains_key("database_dir"));
- assert_eq!(info.get("level").unwrap(), "awesome");
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_get_info_completeness() {
- // For easier debugging,
- // get_info() shall return all important information configurable by the Config-values.
- //
- // There are exceptions for Config-values considered to be unimportant,
- // too sensitive or summarized in another item.
- let skip_from_get_info = vec![
- "addr",
- "displayname",
- "imap_certificate_checks",
- "mail_server",
- "mail_user",
- "mail_pw",
- "mail_port",
- "mail_security",
- "notify_about_wrong_pw",
- "self_reporting_id",
- "selfstatus",
- "send_server",
- "send_user",
- "send_pw",
- "send_port",
- "send_security",
- "server_flags",
- "skip_start_messages",
- "smtp_certificate_checks",
- "proxy_url", // May contain passwords, don't leak it to the logs.
- "socks5_enabled", // SOCKS5 options are deprecated.
- "socks5_host",
- "socks5_port",
- "socks5_user",
- "socks5_password",
- "key_id",
- "webxdc_integration",
- "device_token",
- ];
- let t = TestContext::new().await;
- let info = t.get_info().await.unwrap();
- for key in Config::iter() {
- let key: String = key.to_string();
- if !skip_from_get_info.contains(&&*key)
- && !key.starts_with("configured")
- && !key.starts_with("sys.")
- {
- assert!(
- info.contains_key(&*key),
- "'{key}' missing in get_info() output"
- );
- }
- }
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_search_msgs() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
- let chat = alice
- .create_chat_with_contact("Bob", "bob@example.org")
- .await;
-
- // Global search finds nothing.
- let res = alice.search_msgs(None, "foo").await?;
- assert!(res.is_empty());
-
- // Search in chat with Bob finds nothing.
- let res = alice.search_msgs(Some(chat.id), "foo").await?;
- assert!(res.is_empty());
-
- // Add messages to chat with Bob.
- let mut msg1 = Message::new_text("foobar".to_string());
- send_msg(&alice, chat.id, &mut msg1).await?;
-
- let mut msg2 = Message::new_text("barbaz".to_string());
- send_msg(&alice, chat.id, &mut msg2).await?;
-
- alice.send_text(chat.id, "Δ-Chat").await;
-
- // Global search with a part of text finds the message.
- let res = alice.search_msgs(None, "ob").await?;
- assert_eq!(res.len(), 1);
-
- // Global search for "bar" matches both "foobar" and "barbaz".
- let res = alice.search_msgs(None, "bar").await?;
- assert_eq!(res.len(), 2);
-
- // Message added later is returned first.
- assert_eq!(res.first(), Some(&msg2.id));
- assert_eq!(res.get(1), Some(&msg1.id));
-
- // Search is case-insensitive.
- for chat_id in [None, Some(chat.id)] {
- let res = alice.search_msgs(chat_id, "δ-chat").await?;
- assert_eq!(res.len(), 1);
- }
-
- // Global search with longer text does not find any message.
- let res = alice.search_msgs(None, "foobarbaz").await?;
- assert!(res.is_empty());
-
- // Search for random string finds nothing.
- let res = alice.search_msgs(None, "abc").await?;
- assert!(res.is_empty());
-
- // Search in chat with Bob finds the message.
- let res = alice.search_msgs(Some(chat.id), "foo").await?;
- assert_eq!(res.len(), 1);
-
- // Search in Saved Messages does not find the message.
- let res = alice.search_msgs(Some(self_talk), "foo").await?;
- assert!(res.is_empty());
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_search_unaccepted_requests() -> Result<()> {
- let t = TestContext::new_alice().await;
- receive_imf(
- &t,
- b"From: BobBar \n\
- To: alice@example.org\n\
- Subject: foo\n\
- Message-ID: \n\
- Chat-Version: 1.0\n\
- Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
- \n\
- hello bob, foobar test!\n",
- false,
- )
- .await?;
- let chat_id = t.get_last_msg().await.get_chat_id();
- let chat = Chat::load_from_db(&t, chat_id).await?;
- assert_eq!(chat.get_type(), Chattype::Single);
- assert!(chat.is_contact_request());
-
- assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
- assert_eq!(
- Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
- 1
- );
- assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
- assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
-
- chat_id.block(&t).await?;
-
- assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
- assert_eq!(
- Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
- 0
- );
- assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
- assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
-
- let contact_ids = get_chat_contacts(&t, chat_id).await?;
- Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
-
- assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
- assert_eq!(
- Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
- 1
- );
- assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
- assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_limit_search_msgs() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let chat = alice
- .create_chat_with_contact("Bob", "bob@example.org")
- .await;
-
- // Add 999 messages
- let mut msg = Message::new_text("foobar".to_string());
- for _ in 0..999 {
- send_msg(&alice, chat.id, &mut msg).await?;
- }
- let res = alice.search_msgs(None, "foo").await?;
- assert_eq!(res.len(), 999);
-
- // Add one more message, no limit yet
- send_msg(&alice, chat.id, &mut msg).await?;
- let res = alice.search_msgs(None, "foo").await?;
- assert_eq!(res.len(), 1000);
-
- // Add one more message, that one is truncated then
- send_msg(&alice, chat.id, &mut msg).await?;
- let res = alice.search_msgs(None, "foo").await?;
- assert_eq!(res.len(), 1000);
-
- // In-chat should not be not limited
- let res = alice.search_msgs(Some(chat.id), "foo").await?;
- assert_eq!(res.len(), 1001);
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_check_passphrase() -> Result<()> {
- let dir = tempdir()?;
- let dbfile = dir.path().join("db.sqlite");
-
- let context = ContextBuilder::new(dbfile.clone())
- .with_id(1)
- .build()
- .await
- .context("failed to create context")?;
- assert_eq!(context.open("foo".to_string()).await?, true);
- assert_eq!(context.is_open().await, true);
- drop(context);
-
- let context = ContextBuilder::new(dbfile)
- .with_id(2)
- .build()
- .await
- .context("failed to create context")?;
- assert_eq!(context.is_open().await, false);
- assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
- assert_eq!(context.open("false".to_string()).await?, false);
- assert_eq!(context.open("foo".to_string()).await?, true);
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_context_change_passphrase() -> Result<()> {
- let dir = tempdir()?;
- let dbfile = dir.path().join("db.sqlite");
-
- let context = ContextBuilder::new(dbfile)
- .with_id(1)
- .build()
- .await
- .context("failed to create context")?;
- assert_eq!(context.open("foo".to_string()).await?, true);
- assert_eq!(context.is_open().await, true);
-
- context
- .set_config(Config::Addr, Some("alice@example.org"))
- .await?;
-
- context
- .change_passphrase("bar".to_string())
- .await
- .context("Failed to change passphrase")?;
-
- assert_eq!(
- context.get_config(Config::Addr).await?.unwrap(),
- "alice@example.org"
- );
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_ongoing() -> Result<()> {
- let context = TestContext::new().await;
-
- // No ongoing process allocated.
- assert!(context.shall_stop_ongoing().await);
-
- let receiver = context.alloc_ongoing().await?;
-
- // 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());
-
- assert!(!context.shall_stop_ongoing().await);
-
- // Send the stop signal.
- context.stop_ongoing().await;
-
- // Receive stop signal.
- receiver.recv().await?;
-
- assert!(context.shall_stop_ongoing().await);
-
- // Ongoing process is still running even though stop signal was received,
- // so another one cannot be allocated.
- assert!(context.alloc_ongoing().await.is_err());
-
- context.free_ongoing().await;
-
- // 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?;
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_get_next_msgs() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
-
- let alice_chat = alice.create_chat(&bob).await;
-
- assert!(alice.get_next_msgs().await?.is_empty());
- assert!(bob.get_next_msgs().await?.is_empty());
-
- let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
- let received_msg = bob.recv_msg(&sent_msg).await;
-
- let bob_next_msg_ids = bob.get_next_msgs().await?;
- assert_eq!(bob_next_msg_ids.len(), 1);
- assert_eq!(bob_next_msg_ids.first(), Some(&received_msg.id));
-
- bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
- .await?;
- assert!(bob.get_next_msgs().await?.is_empty());
-
- // Next messages include self-sent messages.
- let alice_next_msg_ids = alice.get_next_msgs().await?;
- assert_eq!(alice_next_msg_ids.len(), 1);
- assert_eq!(alice_next_msg_ids.first(), Some(&sent_msg.sender_msg_id));
-
- alice
- .set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
- .await?;
- assert!(alice.get_next_msgs().await?.is_empty());
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_draft_self_report() -> Result<()> {
- let alice = TestContext::new_alice().await;
-
- let chat_id = alice.draft_self_report().await?;
- let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
- assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
-
- let chat = Chat::load_from_db(&alice, chat_id).await?;
- assert!(chat.is_protected());
-
- let mut draft = chat_id.get_draft(&alice).await?.unwrap();
- assert!(draft.text.starts_with("core_version"));
-
- // Test that sending into the protected chat works:
- let _sent = alice.send_msg(chat_id, &mut draft).await;
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
- let alice = TestContext::new_alice().await;
- assert_eq!(
- alice.get_config(Config::ShowEmails).await?,
- Some("2".to_string())
- );
-
- // Change the config circumventing the cache
- // This simulates what the notification plugin on iOS might do
- // because it runs in a different process
- alice
- .sql
- .execute(
- "INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
- (),
- )
- .await?;
-
- // Alice's Delta Chat doesn't know about it yet:
- assert_eq!(
- alice.get_config(Config::ShowEmails).await?,
- Some("2".to_string())
- );
-
- // Starting IO will fail of course because no server settings are configured,
- // but it should invalidate the caches:
- alice.start_io().await;
-
- assert_eq!(
- alice.get_config(Config::ShowEmails).await?,
- Some("0".to_string())
- );
-
- Ok(())
- }
-}
+mod context_tests;
diff --git a/src/context/context_tests.rs b/src/context/context_tests.rs
new file mode 100644
index 0000000000..111311aca2
--- /dev/null
+++ b/src/context/context_tests.rs
@@ -0,0 +1,649 @@
+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, MuteDuration};
+use crate::chatlist::Chatlist;
+use crate::constants::Chattype;
+use crate::mimeparser::SystemMessage;
+use crate::receive_imf::receive_imf;
+use crate::test_utils::{get_chat_msg, TestContext};
+use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_wrong_db() -> Result<()> {
+ let tmp = tempfile::tempdir()?;
+ let dbfile = tmp.path().join("db.sqlite");
+ tokio::fs::write(&dbfile, b"123").await?;
+ let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
+
+ // Broken database is indistinguishable from encrypted one.
+ assert_eq!(res.is_open().await, false);
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_get_fresh_msgs() {
+ let t = TestContext::new().await;
+ let fresh = t.get_fresh_msgs().await.unwrap();
+ assert!(fresh.is_empty())
+}
+
+async fn receive_msg(t: &TestContext, chat: &Chat) {
+ let members = get_chat_contacts(t, chat.id).await.unwrap();
+ let contact = Contact::get_by_id(t, *members.first().unwrap())
+ .await
+ .unwrap();
+ let msg = format!(
+ "From: {}\n\
+ To: alice@example.org\n\
+ Message-ID: <{}>\n\
+ Chat-Version: 1.0\n\
+ Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
+ \n\
+ hello\n",
+ contact.get_addr(),
+ create_outgoing_rfc724_mid()
+ );
+ println!("{msg}");
+ receive_imf(t, msg.as_bytes(), false).await.unwrap();
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_get_fresh_msgs_and_muted_chats() {
+ // receive various mails in 3 chats
+ let t = TestContext::new_alice().await;
+ let bob = t.create_chat_with_contact("", "bob@g.it").await;
+ let claire = t.create_chat_with_contact("", "claire@g.it").await;
+ let dave = t.create_chat_with_contact("", "dave@g.it").await;
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
+
+ receive_msg(&t, &bob).await;
+ assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
+ assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
+
+ receive_msg(&t, &claire).await;
+ receive_msg(&t, &claire).await;
+ assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 2);
+ assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
+
+ receive_msg(&t, &dave).await;
+ receive_msg(&t, &dave).await;
+ receive_msg(&t, &dave).await;
+ assert_eq!(get_chat_msgs(&t, dave.id).await.unwrap().len(), 3);
+ assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
+
+ // mute one of the chats
+ set_muted(&t, claire.id, MuteDuration::Forever)
+ .await
+ .unwrap();
+ assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
+
+ // receive more messages
+ receive_msg(&t, &bob).await;
+ receive_msg(&t, &claire).await;
+ receive_msg(&t, &dave).await;
+ assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 3);
+ assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
+
+ // unmute claire again
+ set_muted(&t, claire.id, MuteDuration::NotMuted)
+ .await
+ .unwrap();
+ assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_get_fresh_msgs_and_muted_until() {
+ let t = TestContext::new_alice().await;
+ let bob = t.create_chat_with_contact("", "bob@g.it").await;
+ receive_msg(&t, &bob).await;
+ assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
+
+ // chat is unmuted by default, here and in the following assert(),
+ // we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
+ // have the same view to the database.
+ assert!(!bob.is_muted());
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
+
+ // test get_fresh_msgs() with mute_until in the future
+ set_muted(
+ &t,
+ bob.id,
+ MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
+ )
+ .await
+ .unwrap();
+ let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
+ assert!(bob.is_muted());
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
+
+ // to test get_fresh_msgs() with mute_until in the past,
+ // we need to modify the database directly
+ t.sql
+ .execute(
+ "UPDATE chats SET muted_until=? WHERE id=?;",
+ (time() - 3600, bob.id),
+ )
+ .await
+ .unwrap();
+ let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
+ assert!(!bob.is_muted());
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
+
+ // test get_fresh_msgs() with "forever" mute_until
+ set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
+ let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
+ assert!(bob.is_muted());
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
+
+ // to test get_fresh_msgs() with invalid mute_until (everything < -1),
+ // that results in "muted forever" by definition.
+ t.sql
+ .execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
+ .await
+ .unwrap();
+ let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
+ assert!(!bob.is_muted());
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_muted_context() -> Result<()> {
+ let t = TestContext::new_alice().await;
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
+ t.set_config(Config::IsMuted, Some("1")).await?;
+ let chat = t.create_chat_with_contact("", "bob@g.it").await;
+ receive_msg(&t, &chat).await;
+
+ // muted contexts should still show dimmed badge counters eg. in the sidebars,
+ // (same as muted chats show dimmed badge counters in the chatlist)
+ // therefore the fresh messages count should not be affected.
+ assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_blobdir_exists() {
+ let tmp = tempfile::tempdir().unwrap();
+ let dbfile = tmp.path().join("db.sqlite");
+ Context::new(&dbfile, 1, Events::new(), StockStrings::new())
+ .await
+ .unwrap();
+ let blobdir = tmp.path().join("db.sqlite-blobs");
+ assert!(blobdir.is_dir());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_wrong_blogdir() {
+ let tmp = tempfile::tempdir().unwrap();
+ let dbfile = tmp.path().join("db.sqlite");
+ let blobdir = tmp.path().join("db.sqlite-blobs");
+ tokio::fs::write(&blobdir, b"123").await.unwrap();
+ let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
+ assert!(res.is_err());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_sqlite_parent_not_exists() {
+ let tmp = tempfile::tempdir().unwrap();
+ let subdir = tmp.path().join("subdir");
+ let dbfile = subdir.join("db.sqlite");
+ let dbfile2 = dbfile.clone();
+ Context::new(&dbfile, 1, Events::new(), StockStrings::new())
+ .await
+ .unwrap();
+ assert!(subdir.is_dir());
+ assert!(dbfile2.is_file());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_with_empty_blobdir() {
+ let tmp = tempfile::tempdir().unwrap();
+ let dbfile = tmp.path().join("db.sqlite");
+ let blobdir = PathBuf::new();
+ let res = Context::with_blobdir(
+ dbfile,
+ blobdir,
+ 1,
+ Events::new(),
+ StockStrings::new(),
+ Default::default(),
+ );
+ assert!(res.is_err());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_with_blobdir_not_exists() {
+ let tmp = tempfile::tempdir().unwrap();
+ let dbfile = tmp.path().join("db.sqlite");
+ let blobdir = tmp.path().join("blobs");
+ let res = Context::with_blobdir(
+ dbfile,
+ blobdir,
+ 1,
+ Events::new(),
+ StockStrings::new(),
+ Default::default(),
+ );
+ assert!(res.is_err());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn no_crashes_on_context_deref() {
+ let t = TestContext::new().await;
+ std::mem::drop(t);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_get_info() {
+ let t = TestContext::new().await;
+
+ let info = t.get_info().await.unwrap();
+ assert!(info.contains_key("database_dir"));
+}
+
+#[test]
+fn test_get_info_no_context() {
+ let info = get_info();
+ assert!(info.contains_key("deltachat_core_version"));
+ assert!(!info.contains_key("database_dir"));
+ assert_eq!(info.get("level").unwrap(), "awesome");
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_get_info_completeness() {
+ // For easier debugging,
+ // get_info() shall return all important information configurable by the Config-values.
+ //
+ // There are exceptions for Config-values considered to be unimportant,
+ // too sensitive or summarized in another item.
+ let skip_from_get_info = vec![
+ "addr",
+ "displayname",
+ "imap_certificate_checks",
+ "mail_server",
+ "mail_user",
+ "mail_pw",
+ "mail_port",
+ "mail_security",
+ "notify_about_wrong_pw",
+ "self_reporting_id",
+ "selfstatus",
+ "send_server",
+ "send_user",
+ "send_pw",
+ "send_port",
+ "send_security",
+ "server_flags",
+ "skip_start_messages",
+ "smtp_certificate_checks",
+ "proxy_url", // May contain passwords, don't leak it to the logs.
+ "socks5_enabled", // SOCKS5 options are deprecated.
+ "socks5_host",
+ "socks5_port",
+ "socks5_user",
+ "socks5_password",
+ "key_id",
+ "webxdc_integration",
+ "device_token",
+ "encrypted_device_token",
+ ];
+ let t = TestContext::new().await;
+ let info = t.get_info().await.unwrap();
+ for key in Config::iter() {
+ let key: String = key.to_string();
+ if !skip_from_get_info.contains(&&*key)
+ && !key.starts_with("configured")
+ && !key.starts_with("sys.")
+ {
+ assert!(
+ info.contains_key(&*key),
+ "'{key}' missing in get_info() output"
+ );
+ }
+ }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_search_msgs() -> Result<()> {
+ let alice = TestContext::new_alice().await;
+ let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
+ let chat = alice
+ .create_chat_with_contact("Bob", "bob@example.org")
+ .await;
+
+ // Global search finds nothing.
+ let res = alice.search_msgs(None, "foo").await?;
+ assert!(res.is_empty());
+
+ // Search in chat with Bob finds nothing.
+ let res = alice.search_msgs(Some(chat.id), "foo").await?;
+ assert!(res.is_empty());
+
+ // Add messages to chat with Bob.
+ let mut msg1 = Message::new_text("foobar".to_string());
+ send_msg(&alice, chat.id, &mut msg1).await?;
+
+ let mut msg2 = Message::new_text("barbaz".to_string());
+ send_msg(&alice, chat.id, &mut msg2).await?;
+
+ alice.send_text(chat.id, "Δ-Chat").await;
+
+ // Global search with a part of text finds the message.
+ let res = alice.search_msgs(None, "ob").await?;
+ assert_eq!(res.len(), 1);
+
+ // Global search for "bar" matches both "foobar" and "barbaz".
+ let res = alice.search_msgs(None, "bar").await?;
+ assert_eq!(res.len(), 2);
+
+ // Message added later is returned first.
+ assert_eq!(res.first(), Some(&msg2.id));
+ assert_eq!(res.get(1), Some(&msg1.id));
+
+ // Search is case-insensitive.
+ for chat_id in [None, Some(chat.id)] {
+ let res = alice.search_msgs(chat_id, "δ-chat").await?;
+ assert_eq!(res.len(), 1);
+ }
+
+ // Global search with longer text does not find any message.
+ let res = alice.search_msgs(None, "foobarbaz").await?;
+ assert!(res.is_empty());
+
+ // Search for random string finds nothing.
+ let res = alice.search_msgs(None, "abc").await?;
+ assert!(res.is_empty());
+
+ // Search in chat with Bob finds the message.
+ let res = alice.search_msgs(Some(chat.id), "foo").await?;
+ assert_eq!(res.len(), 1);
+
+ // Search in Saved Messages does not find the message.
+ let res = alice.search_msgs(Some(self_talk), "foo").await?;
+ assert!(res.is_empty());
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_search_unaccepted_requests() -> Result<()> {
+ let t = TestContext::new_alice().await;
+ receive_imf(
+ &t,
+ b"From: BobBar \n\
+ To: alice@example.org\n\
+ Subject: foo\n\
+ Message-ID: \n\
+ Chat-Version: 1.0\n\
+ Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
+ \n\
+ hello bob, foobar test!\n",
+ false,
+ )
+ .await?;
+ let chat_id = t.get_last_msg().await.get_chat_id();
+ let chat = Chat::load_from_db(&t, chat_id).await?;
+ assert_eq!(chat.get_type(), Chattype::Single);
+ assert!(chat.is_contact_request());
+
+ assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
+ assert_eq!(
+ Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
+ 1
+ );
+ assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
+ assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
+
+ chat_id.block(&t).await?;
+
+ assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
+ assert_eq!(
+ Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
+ 0
+ );
+ assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
+ assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
+
+ let contact_ids = get_chat_contacts(&t, chat_id).await?;
+ Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
+
+ assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
+ assert_eq!(
+ Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
+ 1
+ );
+ assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
+ assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_limit_search_msgs() -> Result<()> {
+ let alice = TestContext::new_alice().await;
+ let chat = alice
+ .create_chat_with_contact("Bob", "bob@example.org")
+ .await;
+
+ // Add 999 messages
+ let mut msg = Message::new_text("foobar".to_string());
+ for _ in 0..999 {
+ send_msg(&alice, chat.id, &mut msg).await?;
+ }
+ let res = alice.search_msgs(None, "foo").await?;
+ assert_eq!(res.len(), 999);
+
+ // Add one more message, no limit yet
+ send_msg(&alice, chat.id, &mut msg).await?;
+ let res = alice.search_msgs(None, "foo").await?;
+ assert_eq!(res.len(), 1000);
+
+ // Add one more message, that one is truncated then
+ send_msg(&alice, chat.id, &mut msg).await?;
+ let res = alice.search_msgs(None, "foo").await?;
+ assert_eq!(res.len(), 1000);
+
+ // In-chat should not be not limited
+ let res = alice.search_msgs(Some(chat.id), "foo").await?;
+ assert_eq!(res.len(), 1001);
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_check_passphrase() -> Result<()> {
+ let dir = tempdir()?;
+ let dbfile = dir.path().join("db.sqlite");
+
+ let context = ContextBuilder::new(dbfile.clone())
+ .with_id(1)
+ .build()
+ .await
+ .context("failed to create context")?;
+ assert_eq!(context.open("foo".to_string()).await?, true);
+ assert_eq!(context.is_open().await, true);
+ drop(context);
+
+ let context = ContextBuilder::new(dbfile)
+ .with_id(2)
+ .build()
+ .await
+ .context("failed to create context")?;
+ assert_eq!(context.is_open().await, false);
+ assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
+ assert_eq!(context.open("false".to_string()).await?, false);
+ assert_eq!(context.open("foo".to_string()).await?, true);
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_context_change_passphrase() -> Result<()> {
+ let dir = tempdir()?;
+ let dbfile = dir.path().join("db.sqlite");
+
+ let context = ContextBuilder::new(dbfile)
+ .with_id(1)
+ .build()
+ .await
+ .context("failed to create context")?;
+ assert_eq!(context.open("foo".to_string()).await?, true);
+ assert_eq!(context.is_open().await, true);
+
+ context
+ .set_config(Config::Addr, Some("alice@example.org"))
+ .await?;
+
+ context
+ .change_passphrase("bar".to_string())
+ .await
+ .context("Failed to change passphrase")?;
+
+ assert_eq!(
+ context.get_config(Config::Addr).await?.unwrap(),
+ "alice@example.org"
+ );
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_ongoing() -> Result<()> {
+ let context = TestContext::new().await;
+
+ // No ongoing process allocated.
+ assert!(context.shall_stop_ongoing().await);
+
+ let receiver = context.alloc_ongoing().await?;
+
+ // 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());
+
+ assert!(!context.shall_stop_ongoing().await);
+
+ // Send the stop signal.
+ context.stop_ongoing().await;
+
+ // Receive stop signal.
+ receiver.recv().await?;
+
+ assert!(context.shall_stop_ongoing().await);
+
+ // Ongoing process is still running even though stop signal was received,
+ // so another one cannot be allocated.
+ assert!(context.alloc_ongoing().await.is_err());
+
+ context.free_ongoing().await;
+
+ // 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?;
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_get_next_msgs() -> Result<()> {
+ let alice = TestContext::new_alice().await;
+ let bob = TestContext::new_bob().await;
+
+ let alice_chat = alice.create_chat(&bob).await;
+
+ assert!(alice.get_next_msgs().await?.is_empty());
+ assert!(bob.get_next_msgs().await?.is_empty());
+
+ let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
+ let received_msg = bob.recv_msg(&sent_msg).await;
+
+ let bob_next_msg_ids = bob.get_next_msgs().await?;
+ assert_eq!(bob_next_msg_ids.len(), 1);
+ assert_eq!(bob_next_msg_ids.first(), Some(&received_msg.id));
+
+ bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
+ .await?;
+ assert!(bob.get_next_msgs().await?.is_empty());
+
+ // Next messages include self-sent messages.
+ let alice_next_msg_ids = alice.get_next_msgs().await?;
+ assert_eq!(alice_next_msg_ids.len(), 1);
+ assert_eq!(alice_next_msg_ids.first(), Some(&sent_msg.sender_msg_id));
+
+ alice
+ .set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
+ .await?;
+ assert!(alice.get_next_msgs().await?.is_empty());
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_draft_self_report() -> Result<()> {
+ let alice = TestContext::new_alice().await;
+
+ let chat_id = alice.draft_self_report().await?;
+ let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
+ assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
+
+ let chat = Chat::load_from_db(&alice, chat_id).await?;
+ assert!(chat.is_protected());
+
+ let mut draft = chat_id.get_draft(&alice).await?.unwrap();
+ assert!(draft.text.starts_with("core_version"));
+
+ // Test that sending into the protected chat works:
+ let _sent = alice.send_msg(chat_id, &mut draft).await;
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
+ let alice = TestContext::new_alice().await;
+ assert_eq!(
+ alice.get_config(Config::ShowEmails).await?,
+ Some("2".to_string())
+ );
+
+ // Change the config circumventing the cache
+ // This simulates what the notification plugin on iOS might do
+ // because it runs in a different process
+ alice
+ .sql
+ .execute(
+ "INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
+ (),
+ )
+ .await?;
+
+ // Alice's Delta Chat doesn't know about it yet:
+ assert_eq!(
+ alice.get_config(Config::ShowEmails).await?,
+ Some("2".to_string())
+ );
+
+ // Starting IO will fail of course because no server settings are configured,
+ // but it should invalidate the caches:
+ alice.start_io().await;
+
+ assert_eq!(
+ alice.get_config(Config::ShowEmails).await?,
+ Some("0".to_string())
+ );
+
+ Ok(())
+}
diff --git a/src/debug_logging.rs b/src/debug_logging.rs
index 4b0ce441f4..9b437e8cc1 100644
--- a/src/debug_logging.rs
+++ b/src/debug_logging.rs
@@ -9,7 +9,6 @@ use crate::tools::time;
use crate::webxdc::StatusUpdateItem;
use async_channel::{self as channel, Receiver, Sender};
use serde_json::json;
-use std::path::PathBuf;
use tokio::task;
#[derive(Debug)]
@@ -100,9 +99,7 @@ pub async fn maybe_set_logging_xdc(
context,
msg.get_viewtype(),
chat_id,
- msg.param
- .get_path(Param::Filename, context)
- .unwrap_or_default(),
+ msg.param.get(Param::Filename),
msg.get_id(),
)
.await?;
@@ -115,18 +112,16 @@ pub async fn maybe_set_logging_xdc_inner(
context: &Context,
viewtype: Viewtype,
chat_id: ChatId,
- filename: Option,
+ filename: Option<&str>,
msg_id: MsgId,
) -> anyhow::Result<()> {
if viewtype == Viewtype::Webxdc {
- if let Some(file) = filename {
- if let Some(file_name) = file.file_name().and_then(|name| name.to_str()) {
- if file_name.starts_with("debug_logging")
- && file_name.ends_with(".xdc")
- && chat_id.is_self_talk(context).await?
- {
- set_debug_logging_xdc(context, Some(msg_id)).await?;
- }
+ if let Some(filename) = filename {
+ if filename.starts_with("debug_logging")
+ && filename.ends_with(".xdc")
+ && chat_id.is_self_talk(context).await?
+ {
+ set_debug_logging_xdc(context, Some(msg_id)).await?;
}
}
}
diff --git a/src/dehtml.rs b/src/dehtml.rs
index 8ae56240e0..b7f742ee08 100644
--- a/src/dehtml.rs
+++ b/src/dehtml.rs
@@ -3,8 +3,8 @@
//! A module to remove HTML tags from the email text
use std::io::BufRead;
+use std::sync::LazyLock;
-use once_cell::sync::Lazy;
use quick_xml::{
events::{BytesEnd, BytesStart, BytesText},
Reader,
@@ -176,7 +176,8 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
}
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
- static LINE_RE: Lazy = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
+ static LINE_RE: LazyLock =
+ LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
diff --git a/src/download.rs b/src/download.rs
index c8a75227a0..f1f3647749 100644
--- a/src/download.rs
+++ b/src/download.rs
@@ -220,7 +220,6 @@ impl Session {
vec![uid],
&uid_message_ids,
false,
- false,
)
.await?;
if last_uid.is_none() {
@@ -369,7 +368,6 @@ mod tests {
header.as_bytes(),
false,
Some(100000),
- false,
)
.await?;
let msg = t.get_last_msg().await;
@@ -385,7 +383,6 @@ mod tests {
format!("{header}\n\n100k text...").as_bytes(),
false,
None,
- false,
)
.await?;
let msg = t.get_last_msg().await;
@@ -420,7 +417,6 @@ mod tests {
Content-Type: text/plain",
false,
Some(100000),
- false,
)
.await?;
assert_eq!(
@@ -457,7 +453,6 @@ mod tests {
sent2.payload().as_bytes(),
false,
Some(sent2.payload().len() as u32),
- false,
)
.await?;
let msg = bob.get_last_msg().await;
@@ -473,7 +468,6 @@ mod tests {
sent2.payload().as_bytes(),
false,
None,
- false,
)
.await?;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
@@ -517,15 +511,7 @@ mod tests {
";
// not downloading the mdn results in an placeholder
- receive_imf_from_inbox(
- &bob,
- "bar@example.org",
- raw,
- false,
- Some(raw.len() as u32),
- false,
- )
- .await?;
+ receive_imf_from_inbox(&bob, "bar@example.org", raw, false, Some(raw.len() as u32)).await?;
let msg = bob.get_last_msg().await;
let chat_id = msg.chat_id;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1);
@@ -533,7 +519,7 @@ mod tests {
// downloading the mdn afterwards expands to nothing and deletes the placeholder directly
// (usually mdn are too small for not being downloaded directly)
- receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None, false).await?;
+ receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None).await?;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
assert!(Message::load_from_db_optional(&bob, msg.id)
.await?
diff --git a/src/e2ee.rs b/src/e2ee.rs
index 70c835cf2b..011e8b8c06 100644
--- a/src/e2ee.rs
+++ b/src/e2ee.rs
@@ -1,6 +1,10 @@
//! End-to-end encryption support.
-use anyhow::{format_err, Context as _, Result};
+use std::collections::BTreeSet;
+use std::io::Cursor;
+
+use anyhow::{bail, Result};
+use mail_builder::mime::MimePart;
use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
@@ -39,88 +43,76 @@ impl EncryptHelper {
}
/// Determines if we can and should encrypt.
- ///
- /// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
- /// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
- ///
- /// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
pub(crate) async fn should_encrypt(
&self,
context: &Context,
- e2ee_guaranteed: bool,
peerstates: &[(Option, String)],
) -> Result {
let is_chatmail = context.is_chatmail().await?;
- let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
- 1
- } else {
- 0
- };
- for (peerstate, addr) in peerstates {
- match peerstate {
- Some(peerstate) => {
- let prefer_encrypt = peerstate.prefer_encrypt;
- info!(context, "Peerstate for {addr:?} is {prefer_encrypt}.");
- if match peerstate.prefer_encrypt {
- EncryptPreference::NoPreference | EncryptPreference::Reset => {
- (peerstate.prefer_encrypt != EncryptPreference::Reset || is_chatmail)
- && self.prefer_encrypt == EncryptPreference::Mutual
- }
- EncryptPreference::Mutual => true,
- } {
- prefer_encrypt_count += 1;
- }
- }
- None => {
- let msg = format!("Peerstate for {addr:?} missing, cannot encrypt");
- if e2ee_guaranteed {
- return Err(format_err!("{msg}"));
- } else {
- info!(context, "{msg}.");
- return Ok(false);
- }
+ for (peerstate, _addr) in peerstates {
+ if let Some(peerstate) = peerstate {
+ // For chatmail we ignore the encryption preference,
+ // because we can either send encrypted or not at all.
+ if is_chatmail || peerstate.prefer_encrypt != EncryptPreference::Reset {
+ continue;
}
}
+ return Ok(false);
}
-
- // Count number of recipients, including self.
- // This does not depend on whether we send a copy to self or not.
- let recipients_count = peerstates.len() + 1;
-
- Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
+ Ok(true)
}
- /// Tries to encrypt the passed in `mail`.
- pub async fn encrypt(
- self,
+ /// Constructs a vector of public keys for given peerstates.
+ ///
+ /// In addition returns the set of recipient addresses
+ /// for which there is no key available.
+ ///
+ /// Returns an error if there are recipients
+ /// other than self, but no recipient keys are available.
+ pub(crate) fn encryption_keyring(
+ &self,
context: &Context,
verified: bool,
- mail_to_encrypt: lettre_email::PartBuilder,
- peerstates: Vec<(Option, String)>,
- compress: bool,
- ) -> Result {
- let mut keyring: Vec = Vec::new();
+ peerstates: &[(Option, String)],
+ ) -> Result<(Vec, BTreeSet)> {
+ // Encrypt to self unconditionally,
+ // even for a single-device setup.
+ let mut keyring = vec![self.public_key.clone()];
+ let mut missing_key_addresses = BTreeSet::new();
+
+ if peerstates.is_empty() {
+ return Ok((keyring, missing_key_addresses));
+ }
let mut verifier_addresses: Vec<&str> = Vec::new();
- for (peerstate, addr) in peerstates
- .iter()
- .filter_map(|(state, addr)| state.clone().map(|s| (s, addr)))
- {
- let key = peerstate
- .take_key(verified)
- .with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
- keyring.push(key);
- verifier_addresses.push(addr);
+ for (peerstate, addr) in peerstates {
+ if let Some(peerstate) = peerstate {
+ if let Some(key) = peerstate.clone().take_key(verified) {
+ keyring.push(key);
+ verifier_addresses.push(addr);
+ } else {
+ warn!(context, "Encryption key for {addr} is missing.");
+ missing_key_addresses.insert(addr.clone());
+ }
+ } else {
+ warn!(context, "Peerstate for {addr} is missing.");
+ missing_key_addresses.insert(addr.clone());
+ }
}
- // Encrypt to self.
- keyring.push(self.public_key.clone());
+ debug_assert!(
+ !keyring.is_empty(),
+ "At least our own key is in the keyring"
+ );
+ if keyring.len() <= 1 {
+ bail!("No recipient keys are available, cannot encrypt");
+ }
// Encrypt to secondary verified keys
// if we also encrypt to the introducer ("verifier") of the key.
if verified {
- for (peerstate, _addr) in &peerstates {
+ for (peerstate, _addr) in peerstates {
if let Some(peerstate) = peerstate {
if let (Some(key), Some(verifier)) = (
peerstate.secondary_verified_key.as_ref(),
@@ -134,9 +126,22 @@ impl EncryptHelper {
}
}
+ Ok((keyring, missing_key_addresses))
+ }
+
+ /// Tries to encrypt the passed in `mail`.
+ pub async fn encrypt(
+ self,
+ context: &Context,
+ keyring: Vec,
+ mail_to_encrypt: MimePart<'static>,
+ compress: bool,
+ ) -> Result {
let sign_key = load_self_secret_key(context).await?;
- let raw_message = mail_to_encrypt.build().as_string().into_bytes();
+ let mut raw_message = Vec::new();
+ let cursor = Cursor::new(&mut raw_message);
+ mail_to_encrypt.clone().write_part(cursor).ok();
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key), compress).await?;
@@ -145,15 +150,13 @@ impl EncryptHelper {
/// Signs the passed-in `mail` using the private key from `context`.
/// Returns the payload and the signature.
- pub async fn sign(
- self,
- context: &Context,
- mail: lettre_email::PartBuilder,
- ) -> Result<(lettre_email::MimeMessage, String)> {
+ pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result {
let sign_key = load_self_secret_key(context).await?;
- let mime_message = mail.build();
- let signature = pgp::pk_calc_signature(mime_message.as_string().as_bytes(), &sign_key)?;
- Ok((mime_message, signature))
+ let mut buffer = Vec::new();
+ let cursor = Cursor::new(&mut buffer);
+ mail.clone().write_part(cursor).ok();
+ let signature = pgp::pk_calc_signature(&buffer, &sign_key)?;
+ Ok(signature)
}
}
@@ -173,6 +176,7 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
mod tests {
use super::*;
use crate::chat::send_text_msg;
+ use crate::config::Config;
use crate::key::DcKey;
use crate::message::{Message, Viewtype};
use crate::param::Param;
@@ -225,8 +229,8 @@ Sent with my Delta Chat Messenger: https://delta.chat";
let alice = tcm.alice().await;
let bob = tcm.bob().await;
- let chat_alice = alice.create_chat(&bob).await.id;
- let chat_bob = bob.create_chat(&alice).await.id;
+ let chat_alice = alice.create_email_chat(&bob).await.id;
+ let chat_bob = bob.create_email_chat(&alice).await.id;
// Alice sends unencrypted message to Bob
let mut msg = Message::new(Viewtype::Text);
@@ -326,81 +330,20 @@ Sent with my Delta Chat Messenger: https://delta.chat";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_should_encrypt() -> Result<()> {
let t = TestContext::new_alice().await;
- assert!(t.get_config_bool(Config::E2eeEnabled).await?);
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
let ps = new_peerstates(EncryptPreference::NoPreference);
- assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
- // Own preference is `Mutual` and we have the peer's key.
- assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
+ assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
let ps = new_peerstates(EncryptPreference::Reset);
- assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
- assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
+ assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
let ps = new_peerstates(EncryptPreference::Mutual);
- assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
- assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
+ assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
// test with missing peerstate
let ps = vec![(None, "bob@foo.bar".to_string())];
- assert!(encrypt_helper.should_encrypt(&t, true, &ps).await.is_err());
- assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_should_encrypt_e2ee_disabled() -> Result<()> {
- let t = &TestContext::new_alice().await;
- t.set_config_bool(Config::E2eeEnabled, false).await?;
- let encrypt_helper = EncryptHelper::new(t).await.unwrap();
-
- let ps = new_peerstates(EncryptPreference::NoPreference);
- assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?);
-
- let ps = new_peerstates(EncryptPreference::Reset);
- assert!(encrypt_helper.should_encrypt(t, true, &ps).await?);
-
- let mut ps = new_peerstates(EncryptPreference::Mutual);
- // Own preference is `NoPreference` and there's no majority with `Mutual`.
- assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?);
- // Now the majority wants to encrypt. Let's encrypt, anyway there are other cases when we
- // can't send unencrypted, e.g. protected groups.
- ps.push(ps[0].clone());
- assert!(encrypt_helper.should_encrypt(t, false, &ps).await?);
-
- // Test with missing peerstate.
- let ps = vec![(None, "bob@foo.bar".to_string())];
- assert!(encrypt_helper.should_encrypt(t, true, &ps).await.is_err());
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_chatmail_prefers_to_encrypt() -> Result<()> {
- let mut tcm = TestContextManager::new();
- let alice = &tcm.alice().await;
- let bob = &tcm.bob().await;
- bob.set_config_bool(Config::IsChatmail, true).await?;
-
- let bob_chat_id = tcm
- .send_recv_accept(alice, bob, "Hello from DC")
- .await
- .chat_id;
- receive_imf(
- bob,
- b"From: alice@example.org\n\
- To: bob@example.net\n\
- Message-ID: <2222@example.org>\n\
- Date: Sun, 22 Mar 3000 22:37:58 +0000\n\
- \n\
- Hello from another MUA\n",
- false,
- )
- .await?;
- send_text_msg(bob, bob_chat_id, "hi".to_string()).await?;
- let sent_msg = bob.pop_sent_msg().await;
- let msg = Message::load_from_db(bob, sent_msg.sender_msg_id).await?;
- assert!(msg.get_showpadlock());
+ assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
Ok(())
}
diff --git a/src/ephemeral.rs b/src/ephemeral.rs
index e0c00246ab..29143089b6 100644
--- a/src/ephemeral.rs
+++ b/src/ephemeral.rs
@@ -713,808 +713,4 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
}
#[cfg(test)]
-mod tests {
- use super::*;
- use crate::chat::{marknoticed_chat, set_muted, ChatVisibility, MuteDuration};
- use crate::config::Config;
- use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
- use crate::download::DownloadState;
- use crate::location;
- use crate::message::markseen_msgs;
- use crate::receive_imf::receive_imf;
- use crate::test_utils::{TestContext, TestContextManager};
- use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
- use crate::{
- chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus},
- tools::IsNoneOrEmpty,
- };
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_stock_ephemeral_messages() {
- let context = TestContext::new().await;
-
- assert_eq!(
- stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await,
- "You disabled message deletion timer."
- );
-
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled { duration: 1 },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 1 s."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled { duration: 30 },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 30 s."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled { duration: 60 },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 1 minute."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled { duration: 90 },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 1.5 minutes."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled { duration: 30 * 60 },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 30 minutes."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled { duration: 60 * 60 },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 1 hour."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled { duration: 5400 },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 1.5 hours."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled {
- duration: 2 * 60 * 60
- },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 2 hours."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled {
- duration: 24 * 60 * 60
- },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 1 day."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled {
- duration: 2 * 24 * 60 * 60
- },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 2 days."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled {
- duration: 7 * 24 * 60 * 60
- },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 1 week."
- );
- assert_eq!(
- stock_ephemeral_timer_changed(
- &context,
- Timer::Enabled {
- duration: 4 * 7 * 24 * 60 * 60
- },
- ContactId::SELF
- )
- .await,
- "You set message deletion timer to 4 weeks."
- );
- }
-
- /// Test enabling and disabling ephemeral timer remotely.
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_ephemeral_enable_disable() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
-
- let chat_alice = alice.create_chat(&bob).await.id;
- let chat_bob = bob.create_chat(&alice).await.id;
-
- chat_alice
- .set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
- .await?;
- let sent = alice.pop_sent_msg().await;
- bob.recv_msg(&sent).await;
- assert_eq!(
- chat_bob.get_ephemeral_timer(&bob.ctx).await?,
- Timer::Enabled { duration: 60 }
- );
-
- chat_alice
- .set_ephemeral_timer(&alice.ctx, Timer::Disabled)
- .await?;
- let sent = alice.pop_sent_msg().await;
- bob.recv_msg(&sent).await;
- assert_eq!(
- chat_bob.get_ephemeral_timer(&bob.ctx).await?,
- Timer::Disabled
- );
-
- Ok(())
- }
-
- /// Test that enabling ephemeral timer in unpromoted group does not send a message.
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_ephemeral_unpromoted() -> Result<()> {
- let alice = TestContext::new_alice().await;
-
- let chat_id =
- create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
-
- // Group is unpromoted, the timer can be changed without sending a message.
- assert!(chat_id.is_unpromoted(&alice).await?);
- chat_id
- .set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 })
- .await?;
- let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
- assert!(sent.is_none());
- assert_eq!(
- chat_id.get_ephemeral_timer(&alice).await?,
- Timer::Enabled { duration: 60 }
- );
-
- // Promote the group.
- send_text_msg(&alice, chat_id, "hi!".to_string()).await?;
- assert!(chat_id.is_promoted(&alice).await?);
- let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
- assert!(sent.is_some());
-
- chat_id
- .set_ephemeral_timer(&alice.ctx, Timer::Disabled)
- .await?;
- let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
- assert!(sent.is_some());
- assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
-
- Ok(())
- }
-
- /// Test that timer is enabled even if the message explicitly enabling the timer is lost.
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_ephemeral_enable_lost() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
-
- let chat_alice = alice.create_chat(&bob).await.id;
- let chat_bob = bob.create_chat(&alice).await.id;
-
- // Alice enables the timer.
- chat_alice
- .set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
- .await?;
- assert_eq!(
- chat_alice.get_ephemeral_timer(&alice.ctx).await?,
- Timer::Enabled { duration: 60 }
- );
- // The message enabling the timer is lost.
- let _sent = alice.pop_sent_msg().await;
- assert_eq!(
- chat_bob.get_ephemeral_timer(&bob.ctx).await?,
- Timer::Disabled,
- );
-
- // Alice sends a text message.
- let mut msg = Message::new(Viewtype::Text);
- chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
- let sent = alice.pop_sent_msg().await;
-
- // Bob receives text message and enables the timer, even though explicit timer update was
- // lost previously.
- bob.recv_msg(&sent).await;
- assert_eq!(
- chat_bob.get_ephemeral_timer(&bob.ctx).await?,
- Timer::Enabled { duration: 60 }
- );
-
- Ok(())
- }
-
- /// Test that Alice replying to the chat without a timer at the same time as Bob enables the
- /// timer does not result in disabling the timer on the Bob's side.
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_ephemeral_timer_rollback() -> Result<()> {
- let alice = TestContext::new_alice().await;
- let bob = TestContext::new_bob().await;
-
- let chat_alice = alice.create_chat(&bob).await.id;
- let chat_bob = bob.create_chat(&alice).await.id;
-
- // Alice sends message to Bob
- let mut msg = Message::new(Viewtype::Text);
- chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
- let sent = alice.pop_sent_msg().await;
- bob.recv_msg(&sent).await;
-
- // Alice sends second message to Bob, with no timer
- let mut msg = Message::new(Viewtype::Text);
- chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
- let sent = alice.pop_sent_msg().await;
-
- assert_eq!(
- chat_bob.get_ephemeral_timer(&bob.ctx).await?,
- Timer::Disabled
- );
-
- // Bob sets ephemeral timer and sends a message about timer change
- chat_bob
- .set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
- .await?;
- let sent_timer_change = bob.pop_sent_msg().await;
-
- assert_eq!(
- chat_bob.get_ephemeral_timer(&bob.ctx).await?,
- Timer::Enabled { duration: 60 }
- );
-
- // Bob receives message from Alice.
- // Alice message has no timer. However, Bob should not disable timer,
- // because Alice replies to old message.
- bob.recv_msg(&sent).await;
-
- assert_eq!(
- chat_alice.get_ephemeral_timer(&alice.ctx).await?,
- Timer::Disabled
- );
- assert_eq!(
- chat_bob.get_ephemeral_timer(&bob.ctx).await?,
- Timer::Enabled { duration: 60 }
- );
-
- // Alice receives message from Bob
- alice.recv_msg(&sent_timer_change).await;
-
- assert_eq!(
- chat_alice.get_ephemeral_timer(&alice.ctx).await?,
- Timer::Enabled { duration: 60 }
- );
-
- // Bob disables the chat timer.
- // Note that the last message in the Bob's chat is from Alice and has no timer,
- // but the chat timer is enabled.
- chat_bob
- .set_ephemeral_timer(&bob.ctx, Timer::Disabled)
- .await?;
- alice.recv_msg(&bob.pop_sent_msg().await).await;
- assert_eq!(
- chat_alice.get_ephemeral_timer(&alice.ctx).await?,
- Timer::Disabled
- );
-
- Ok(())
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_ephemeral_delete_msgs() -> Result<()> {
- let t = TestContext::new_alice().await;
- let self_chat = t.get_self_chat().await;
-
- assert_eq!(next_expiration_timestamp(&t).await, None);
-
- t.send_text(self_chat.id, "Saved message, which we delete manually")
- .await;
- let msg = t.get_last_msg_in(self_chat.id).await;
- msg.id.trash(&t, false).await?;
- check_msg_is_deleted(&t, &self_chat, msg.id).await;
-
- self_chat
- .id
- .set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 })
- .await
- .unwrap();
-
- // Send a saved message which will be deleted after 3600s
- let now = time();
- let msg = t.send_text(self_chat.id, "Message text").await;
-
- check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
- .await
- .unwrap();
-
- // Set DeleteDeviceAfter to 1800s. Then send a saved message which will
- // still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
- t.set_config(Config::DeleteDeviceAfter, Some("1800"))
- .await?;
-
- let now = time();
- let msg = t.send_text(self_chat.id, "Message text").await;
-
- check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
- .await
- .unwrap();
-
- // Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter.
- let bob_chat = t.create_chat_with_contact("", "bob@example.net").await;
- let now = time();
- let msg = t.send_text(bob_chat.id, "Message text").await;
-
- check_msg_will_be_deleted(
- &t,
- msg.sender_msg_id,
- &bob_chat,
- now + 1799,
- // The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and
- // therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later.
- time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE,
- )
- .await
- .unwrap();
-
- // Enable ephemeral messages with Bob -> message will be deleted after 60s.
- // This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time).
- bob_chat
- .id
- .set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
- .await?;
-
- let now = time();
- let msg = t.send_text(bob_chat.id, "Message text").await;
-
- check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
- .await
- .unwrap();
-
- Ok(())
- }
-
- async fn check_msg_will_be_deleted(
- t: &TestContext,
- msg_id: MsgId,
- chat: &Chat,
- not_deleted_at: i64,
- deleted_at: i64,
- ) -> Result<()> {
- let next_expiration = next_expiration_timestamp(t).await.unwrap();
-
- assert!(next_expiration > not_deleted_at);
- delete_expired_messages(t, not_deleted_at).await?;
-
- let loaded = Message::load_from_db(t, msg_id).await?;
- assert!(!loaded.text.is_empty());
- assert_eq!(loaded.chat_id, chat.id);
-
- assert!(next_expiration < deleted_at);
- delete_expired_messages(t, deleted_at).await?;
- t.evtracker
- .get_matching(|evt| {
- if let EventType::MsgDeleted {
- msg_id: event_msg_id,
- ..
- } = evt
- {
- *event_msg_id == msg_id
- } else {
- false
- }
- })
- .await;
-
- let loaded = Message::load_from_db_optional(t, msg_id).await?;
- assert!(loaded.is_none());
-
- // Check that the msg was deleted locally.
- check_msg_is_deleted(t, chat, msg_id).await;
-
- Ok(())
- }
-
- async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
- let chat_items = chat::get_chat_msgs(t, chat.id).await.unwrap();
- // Check that the chat is empty except for possibly info messages:
- for item in &chat_items {
- if let ChatItem::Message { msg_id } = item {
- let msg = Message::load_from_db(t, *msg_id).await.unwrap();
- assert!(msg.is_info())
- }
- }
-
- // Check that if there is a message left, the text and metadata are gone
- if let Ok(msg) = Message::load_from_db(t, msg_id).await {
- assert_eq!(msg.from_id, ContactId::UNDEFINED);
- assert_eq!(msg.to_id, ContactId::UNDEFINED);
- assert_eq!(msg.text, "");
- let rawtxt: Option = t
- .sql
- .query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
- .await
- .unwrap();
- assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
- }
- }
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_delete_expired_imap_messages() -> Result<()> {
- let t = TestContext::new_alice().await;
- const HOUR: i64 = 60 * 60;
- let now = time();
- for (id, timestamp, ephemeral_timestamp) in &[
- (900, now - 2 * HOUR, 0),
- (1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
- (1010, now - 23 * HOUR, 0),
- (1020, now - 21 * HOUR, 0),
- (1030, now - 19 * HOUR, 0),
- (2000, now - 18 * HOUR, now - HOUR),
- (2020, now - 17 * HOUR, now + HOUR),
- (3000, now + HOUR, 0),
- ] {
- let message_id = id.to_string();
- t.sql
- .execute(
- "INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
- (id, &message_id, timestamp, ephemeral_timestamp),
- )
- .await?;
- t.sql
- .execute(
- "INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
- (&message_id, id),
- )
- .await?;
- }
-
- async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
- assert_eq!(
- context
- .sql
- .count(
- "SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
- (id.to_string(),),
- )
- .await?,
- 1
- );
- Ok(())
- }
-
- async fn remove_uid(context: &Context, id: u32) -> Result<()> {
- context
- .sql
- .execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
- .await?;
- Ok(())
- }
-
- // This should mark message 2000 for deletion.
- delete_expired_imap_messages(&t).await?;
- test_marked_for_deletion(&t, 2000).await?;
- remove_uid(&t, 2000).await?;
- // No other messages are marked for deletion.
- assert_eq!(
- t.sql
- .count("SELECT COUNT(*) FROM imap WHERE target=''", ())
- .await?,
- 0
- );
-
- t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
- .await?;
- delete_expired_imap_messages(&t).await?;
- test_marked_for_deletion(&t, 1000).await?;
-
- MsgId::new(1000)
- .update_download_state(&t, DownloadState::Available)
- .await?;
- t.sql
- .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
- .await?;
- delete_expired_imap_messages(&t).await?;
- test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
- remove_uid(&t, 1000).await?;
-
- t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
- .await?;
- delete_expired_imap_messages(&t).await?;
- test_marked_for_deletion(&t, 1010).await?;
- t.sql
- .execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
- .await?;
-
- MsgId::new(1010)
- .update_download_state(&t, DownloadState::Available)
- .await?;
- delete_expired_imap_messages(&t).await?;
- // Keep downloadable for now.
- assert_eq!(
- t.sql
- .count("SELECT COUNT(*) FROM imap WHERE target=''", ())
- .await?,
- 0
- );
-
- t.set_config(Config::DeleteServerAfter, Some("1")).await?;
- delete_expired_imap_messages(&t).await?;
- test_marked_for_deletion(&t, 3000).await?;
-
- Ok(())
- }
-
- // Regression test for a bug in the timer rollback protection.
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_ephemeral_timer_references() -> Result<()> {
- let alice = TestContext::new_alice().await;
-
- // Message with Message-ID and no timer is received.
- receive_imf(
- &alice,
- b"From: Bob \n\
- To: Alice \n\
- Chat-Version: 1.0\n\
- Subject: Subject\n\
- Message-ID: \n\
- Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
- \n\
- hello\n",
- false,
- )
- .await?;
-
- let msg = alice.get_last_msg().await;
- let chat_id = msg.chat_id;
- assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
-
- // Message with Message-ID is received.
- receive_imf(
- &alice,
- b"From: Bob \n\
- To: Alice \n\
- Chat-Version: 1.0\n\
- Subject: Subject\n\
- Message-ID: \n\
- Date: Sun, 22 Mar 2020 00:11:00 +0000\n\
- Ephemeral-Timer: 60\n\
- \n\
- second message\n",
- false,
- )
- .await?;
- assert_eq!(
- chat_id.get_ephemeral_timer(&alice).await?,
- Timer::Enabled { duration: 60 }
- );
- let msg = alice.get_last_msg().await;
-
- // Message is deleted when its timer expires.
- msg.id.trash(&alice, false).await?;
-
- // Message with Message-ID , referencing and
- // , is received. The message is not in the
- // database anymore, so the timer should be applied unconditionally without rollback
- // protection.
- //
- // Previously Delta Chat fallen back to using in this case and
- // compared received timer value to the timer value of the . Because
- // their timer values are the same ("disabled"), Delta Chat assumed that the timer was not
- // changed explicitly and the change should be ignored.
- //
- // The message also contains a quote of the first message to test that only References:
- // header and not In-Reply-To: is consulted by the rollback protection.
- receive_imf(
- &alice,
- b"From: Bob \n\
- To: Alice