mirror of
https://github.com/chatmail/core.git
synced 2026-05-18 14:26:31 +03:00
Compare commits
10 Commits
simon/impr
...
simon/i770
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb87e1fba4 | ||
|
|
420b36fcde | ||
|
|
252fc8480e | ||
|
|
659d21aa9d | ||
|
|
8f604e74ec | ||
|
|
e1ebf3e96d | ||
|
|
76171aea2e | ||
|
|
96b8d1720e | ||
|
|
47b49fd02e | ||
|
|
f50e3d6ffa |
2
STYLE.md
2
STYLE.md
@@ -21,7 +21,7 @@ text TEXT DEFAULT '' NOT NULL -- message text
|
||||
```
|
||||
|
||||
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
||||
or [`indoc!](https://docs.rs/indoc).
|
||||
or [`indoc!`](https://docs.rs/indoc).
|
||||
Do not escape newlines like this:
|
||||
```
|
||||
sql.execute(
|
||||
|
||||
@@ -894,7 +894,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
||||
* the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
|
||||
* chats
|
||||
* - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
* and hides the "Device chat" and contact requests.
|
||||
* and hides the "Device chat", contact requests and incoming broadcasts.
|
||||
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
* to also hide the archive link.
|
||||
* - if the flag DC_GCL_NO_SPECIALS is set, archive link is not added
|
||||
|
||||
@@ -16,7 +16,6 @@ We also include a JavaScript and TypeScript client for the JSON-RPC API. The sou
|
||||
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/chatmail/yerpc)). Find the source in the [`typescript`](typescript) folder.
|
||||
|
||||
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
|
||||
|
||||
```sh
|
||||
cd typescript
|
||||
npm install
|
||||
@@ -25,28 +24,11 @@ npm run build
|
||||
|
||||
The JavaScript client is [published on NPM](https://www.npmjs.com/package/@deltachat/jsonrpc-client).
|
||||
|
||||
###### Usage
|
||||
|
||||
```typescript
|
||||
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||
import { C } from "@deltachat/jsonrpc-client";
|
||||
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
const accounts = await dc.rpc.getAllAccounts();
|
||||
console.log("accounts", accounts);
|
||||
dc.close();
|
||||
```
|
||||
|
||||
##### Generate TypeScript/JavaScript documentation
|
||||
|
||||
A script is included to build autogenerated documentation, which includes all RPC methods:
|
||||
|
||||
```sh
|
||||
cd typescript
|
||||
npm run docs
|
||||
```
|
||||
|
||||
Then open the [`typescript/docs`](typescript/docs) folder in a web browser.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -54,6 +54,9 @@ pub struct EnteredLoginParam {
|
||||
/// If true, login via OAUTH2 (not recommended anymore).
|
||||
/// Default: false
|
||||
pub oauth2: Option<bool>,
|
||||
|
||||
/// IP addresses for prefilling DNS
|
||||
pub dns_prefill: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||
@@ -75,6 +78,7 @@ impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||
smtp_password: param.smtp.password.into_option(),
|
||||
certificate_checks: certificate_checks.into_option(),
|
||||
oauth2: param.oauth2.into_option(),
|
||||
dns_prefill: param.dns_prefill,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,6 +105,7 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
||||
},
|
||||
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
|
||||
oauth2: param.oauth2.unwrap_or_default(),
|
||||
dns_prefill: param.dns_prefill,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
# @deltachat/jsonrpc-client
|
||||
|
||||
This package is a client for the jsonrpc server.
|
||||
|
||||
> If you are looking for the functions in the documentation, they are under [`RawClient`](https://js.jsonrpc.delta.chat/classes/RawClient.html).
|
||||
|
||||
### Important Terms
|
||||
|
||||
- [chat mail core (formerly delta chat core)](https://github.com/deltachat/deltachat-core-rust/) is heart of all Delta Chat clients. Handels all the heavy lifting (email, encryption, ...) and provides an easy api for bots and clients (`getChatlist`, `getChat`, `getContact`, ...).
|
||||
- [jsonrpc](https://www.jsonrpc.org/specification) is a JSON based protocol
|
||||
for applications to speak to each other by [remote procedure calls](https://en.wikipedia.org/wiki/Remote_procedure_call) (short RPC),
|
||||
which basically means that the client can call methods on the server by sending a JSON messages.
|
||||
- [`deltachat-rpc-server`](https://github.com/deltachat/deltachat-core-rust/tree/main/deltachat-rpc-server) provides the jsonrpc api over stdio (stdin/stdout)
|
||||
- [`@deltachat/stdio-rpc-server`](https://www.npmjs.com/package/@deltachat/stdio-rpc-server) is an easy way to install `deltachat-rpc-server` from npm and use it from nodejs.
|
||||
|
||||
#### Transport
|
||||
|
||||
You need to connect this client to an instance of chatmail-core via a transport.
|
||||
|
||||
For this you can use the `StdioTransport`, which you can use by importing `StdioDeltaChat`.
|
||||
|
||||
You can also make your own transport, for example deltachat desktop uses a custom transport that sends the json messages over electron ipc.
|
||||
Just implement your transport based on the `Transport` interface - look at how the [stdio transport is implemented](https://github.com/deltachat/deltachat-core-rust/blob/7121675d226e69fd85d0194d4b9c4442e4dd8299/deltachat-jsonrpc/typescript/src/client.ts#L113) for an example, it's not hard.
|
||||
|
||||
The other transports that exist (but note that those are not standalone, they list here only serves as reference on how to implement those.):
|
||||
|
||||
- Electron IPC (from Delta Chat Desktop)
|
||||
- [transport](https://github.com/deltachat/deltachat-desktop/blob/0a4fdb2065c7b14fa097769181afd05e3f552f54/packages/target-electron/runtime-electron/runtime.ts#L49-L123), [request handling](https://github.com/deltachat/deltachat-desktop/blob/0a4fdb2065c7b14fa097769181afd05e3f552f54/packages/target-electron/src/deltachat/controller.ts#L199-L201), [responses](https://github.com/deltachat/deltachat-desktop/blob/0a4fdb2065c7b14fa097769181afd05e3f552f54/packages/target-electron/src/deltachat/controller.ts#L93-L113)
|
||||
- Tauri IPC (from DC Desktop Tauri edition)
|
||||
- [transport](https://github.com/deltachat/deltachat-desktop/blob/0a4fdb2065c7b14fa097769181afd05e3f552f54/packages/target-tauri/runtime-tauri/runtime.ts#L86-L118), backend: [request handling](https://github.com/deltachat/deltachat-desktop/blob/0a4fdb2065c7b14fa097769181afd05e3f552f54/packages/target-tauri/src-tauri/src/lib.rs#L85-L94), [responses](https://github.com/deltachat/deltachat-desktop/blob/0a4fdb2065c7b14fa097769181afd05e3f552f54/packages/target-tauri/src-tauri/src/state/deltachat.rs#L43-L95)
|
||||
- Authenticated Websocket (from DC Desktop Browser edition)
|
||||
- [transport](https://github.com/deltachat/deltachat-desktop/blob/0a4fdb2065c7b14fa097769181afd05e3f552f54/packages/target-browser/runtime-browser/runtime.ts#L37-L80), backend: [authentication](https://github.com/deltachat/deltachat-desktop/blob/0a4fdb2065c7b14fa097769181afd05e3f552f54/packages/target-browser/src/index.ts#L258-L275), [web socket server](https://github.com/deltachat/deltachat-desktop/blob/0a4fdb2065c7b14fa097769181afd05e3f552f54/packages/target-browser/src/deltachat-rpc.ts#L93) (this also contains some unrelated code, it's easier than it looks at first glance)
|
||||
|
||||
## Usage
|
||||
|
||||
> The **minimum** nodejs version for `@deltachat/stdio-rpc-server` is `16`
|
||||
|
||||
```
|
||||
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
|
||||
```
|
||||
|
||||
```js
|
||||
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||
// Import constants you might need later
|
||||
import { C } from "@deltachat/jsonrpc-client";
|
||||
|
||||
async function main() {
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
dc.close();
|
||||
}
|
||||
main();
|
||||
```
|
||||
|
||||
For a more complete example refer to <https://github.com/deltachat-bot/echo/tree/master/nodejs_stdio_jsonrpc>.
|
||||
|
||||
### Listening for events
|
||||
|
||||
```ts
|
||||
dc.on("Info", (accountId, { msg }) =>
|
||||
console.info(accountId, "[core:info]", msg),
|
||||
);
|
||||
// Or get an event emitter for only one account
|
||||
const emitter = dc.getContextEvents(accountId);
|
||||
emitter.on("IncomingMsg", async ({ chatId, msgId }) => {
|
||||
const message = await dc.rpc.getMessage(accountId, msgId);
|
||||
console.log("got message in chat " + chatId + " : ", message.text);
|
||||
});
|
||||
```
|
||||
|
||||
### Getting Started
|
||||
|
||||
This section describes how to handle the Delta Chat core library over the jsonrpc bindings.
|
||||
For general information about Delta Chat itself,
|
||||
see <https://delta.chat> and <https://github.com/deltachat>.
|
||||
|
||||
Let's start.
|
||||
|
||||
First of all, you have to start the deltachat-rpc-server process.
|
||||
|
||||
```js
|
||||
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
```
|
||||
|
||||
Then we have to create an Account (also called Context or profile) that is bound to a database.
|
||||
The database is a normal SQLite file with a "blob directory" beside it.
|
||||
But these details are handled by deltachat's account manager.
|
||||
So you just have to tell the account manager to create a new account:
|
||||
|
||||
```js
|
||||
const accountId = await dc.rpc.addAccount();
|
||||
```
|
||||
|
||||
After that, register event listeners so you can see what core is doing:
|
||||
Intenally `@deltachat/jsonrpc-client` implments a loop that waits for new events and then emits them to javascript land.
|
||||
|
||||
```js
|
||||
dc.on("Info", (accountId, { msg }) =>
|
||||
console.info(accountId, "[core:info]", msg),
|
||||
);
|
||||
```
|
||||
|
||||
Now you can **configure the account:**
|
||||
|
||||
```js
|
||||
// use some real test credentials here
|
||||
await dc.rpc.setConfig(accountId, "addr", "alice@example.org");
|
||||
await dc.rpc.setConfig(accountId, "mail_pw", "***");
|
||||
// you can also set multiple config options in one call
|
||||
await dc.rpc.batchSetConfig(accountId, {
|
||||
addr: "alice@example.org",
|
||||
mail_pw: "***",
|
||||
});
|
||||
|
||||
// after setting the credentials attempt to login
|
||||
await dc.rpc.configure(accountId);
|
||||
```
|
||||
|
||||
`configure()` returns a promise that is rejected on error (with await is is thrown).
|
||||
The configuration itself may take a while. You can monitor it's progress like this:
|
||||
|
||||
```js
|
||||
dc.on("ConfigureProgress", (accountId, { progress, comment }) => {
|
||||
console.log(accountId, "ConfigureProgress", progress, comment);
|
||||
});
|
||||
// make sure to register this event handler before calling `dc.rpc.configure()`
|
||||
```
|
||||
|
||||
The configuration result is saved in the database.
|
||||
On subsequent starts it is not needed to call `dc.rpc.configure(accountId)`
|
||||
(you can check this using `dc.rpc.isConfigured(accountId)`).
|
||||
|
||||
On a successfully configuration delta chat core automatically connects to the server, however subsequent starts you **need to do that manually** by calling `dc.rpc.startIo(accountId)` or `dc.rpc.startIoForAllAccounts()`.
|
||||
|
||||
```js
|
||||
if (!(await dc.rpc.isConfigured(accountId))) {
|
||||
// use some real test credentials here
|
||||
await dc.rpc.batchSetConfig(accountId, {
|
||||
addr: "alice@example.org",
|
||||
mail_pw: "***",
|
||||
});
|
||||
await dc.rpc.configure(accountId);
|
||||
} else {
|
||||
await dc.rpc.startIo(accountId);
|
||||
}
|
||||
```
|
||||
|
||||
Now you can **send the first message:**
|
||||
|
||||
```js
|
||||
const contactId = await dc.rpc.createContact(
|
||||
accountId,
|
||||
"bob@example.org",
|
||||
null /* optional name */,
|
||||
);
|
||||
const chatId = await dc.rpc.createChatByContactId(accountId, contactId);
|
||||
|
||||
await dc.rpc.miscSendTextMessage(
|
||||
accountId,
|
||||
chatId,
|
||||
"Hi, here is my first message!",
|
||||
);
|
||||
```
|
||||
|
||||
`dc.rpc.miscSendTextMessage()` returns immediately;
|
||||
the sending itself is done in the background.
|
||||
If you check the testing address (bob),
|
||||
you should receive a normal e-mail.
|
||||
Answer this e-mail in any e-mail program with "Got it!",
|
||||
and the IO you started above will **receive the message**.
|
||||
|
||||
You can then **list all messages** of a chat as follows:
|
||||
|
||||
```js
|
||||
let i = 0;
|
||||
for (const msgId of await exp.rpc.getMessageIds(120, 12, false, false)) {
|
||||
i++;
|
||||
console.log(`Message: ${i}`, (await dc.rpc.getMessage(120, msgId)).text);
|
||||
}
|
||||
```
|
||||
|
||||
This will output the following two lines:
|
||||
|
||||
```
|
||||
Message 1: Hi, here is my first message!
|
||||
Message 2: Got it!
|
||||
```
|
||||
|
||||
<!-- TODO: ### Clean shutdown? - seems to be more advanced to call async functions on exit, also is this needed in this usecase? -->
|
||||
|
||||
## Further information
|
||||
|
||||
- `@deltachat/stdio-rpc-server`
|
||||
- [package on npm](https://www.npmjs.com/package/@deltachat/stdio-rpc-server)
|
||||
- [source code on github](https://github.com/deltachat/deltachat-core-rust/tree/main/deltachat-rpc-server/npm-package)
|
||||
- [use `@deltachat/stdio-rpc-server` on an usuported platform](https://github.com/deltachat/deltachat-core-rust/tree/main/deltachat-rpc-server/npm-package#how-to-use-on-an-unsupported-platform)
|
||||
- The issue-tracker for the core library is here: <https://github.com/deltachat/deltachat-core-rust/issues>
|
||||
|
||||
If you need further assistance,
|
||||
please do not hesitate to contact us
|
||||
through the channels shown at https://delta.chat/en/contribute
|
||||
|
||||
Please keep in mind, that your derived work
|
||||
must respect the Mozilla Public License 2.0 of deltachat-rpc-server
|
||||
and the respective licenses of the libraries deltachat-rpc-server links with.
|
||||
@@ -5,14 +5,14 @@ import { RawClient } from "../generated/client.js";
|
||||
import { BaseTransport, Request } from "yerpc";
|
||||
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||
|
||||
export type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
accountId: number,
|
||||
event: Extract<EventType, { kind: Property }>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
event: Extract<EventType, { kind: Property }>,
|
||||
) => void;
|
||||
|
||||
@@ -44,8 +44,13 @@ class AttrDict(dict):
|
||||
super().__setattr__(attr, val)
|
||||
|
||||
|
||||
def _forever(_event: AttrDict) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def run_client_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
until: Callable[[AttrDict], bool] = _forever,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
@@ -55,10 +60,11 @@ def run_client_cli(
|
||||
"""
|
||||
from .client import Client
|
||||
|
||||
_run_cli(Client, hooks, argv, **kwargs)
|
||||
_run_cli(Client, until, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
def run_bot_cli(
|
||||
until: Callable[[AttrDict], bool] = _forever,
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -69,11 +75,12 @@ def run_bot_cli(
|
||||
"""
|
||||
from .client import Bot
|
||||
|
||||
_run_cli(Bot, hooks, argv, **kwargs)
|
||||
_run_cli(Bot, until, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
def _run_cli(
|
||||
client_type: Type["Client"],
|
||||
until: Callable[[AttrDict], bool] = _forever,
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -111,7 +118,7 @@ def _run_cli(
|
||||
kwargs={"email": args.email, "password": args.password},
|
||||
)
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
client.run_until(until)
|
||||
|
||||
|
||||
def extract_addr(text: str) -> str:
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import (
|
||||
|
||||
from ._utils import (
|
||||
AttrDict,
|
||||
_forever,
|
||||
parse_system_add_remove,
|
||||
parse_system_image_changed,
|
||||
parse_system_title_changed,
|
||||
@@ -91,19 +92,28 @@ class Client:
|
||||
|
||||
def run_forever(self) -> None:
|
||||
"""Process events forever."""
|
||||
self.run_until(lambda _: False)
|
||||
self.run_until(_forever)
|
||||
|
||||
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True.
|
||||
|
||||
The callable should accept an AttrDict object representing the
|
||||
last processed event. The event is returned when the callable
|
||||
evaluates to True.
|
||||
"""
|
||||
"""Start the event processing loop."""
|
||||
self.logger.debug("Listening to incoming events...")
|
||||
if self.is_configured():
|
||||
self.account.start_io()
|
||||
self._process_messages() # Process old messages.
|
||||
return self._process_events(until_func=func) # Loop over incoming events
|
||||
|
||||
def _process_events(
|
||||
self,
|
||||
until_func: Callable[[AttrDict], bool],
|
||||
until_event: EventType = False,
|
||||
) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True,
|
||||
or until a certain event happens.
|
||||
|
||||
The until_func callable should accept an AttrDict object representing
|
||||
the last processed event. The event is returned when the callable
|
||||
evaluates to True.
|
||||
"""
|
||||
while True:
|
||||
event = self.account.wait_for_event()
|
||||
event["kind"] = EventType(event.kind)
|
||||
@@ -112,10 +122,13 @@ class Client:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
self._process_messages()
|
||||
|
||||
stop = func(event)
|
||||
stop = until_func(event)
|
||||
if stop:
|
||||
return event
|
||||
|
||||
if event.kind == until_event:
|
||||
return event
|
||||
|
||||
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||
if evfilter.filter(event):
|
||||
|
||||
@@ -5,8 +5,6 @@ it does not use NAPI bindings but instead uses stdio executables
|
||||
to let you talk to core over jsonrpc over stdio.
|
||||
This simplifies cross-compilation and even reduces binary size (no CFFI layer and no NAPI layer).
|
||||
|
||||
📚 Docs: <https://js.jsonrpc.delta.chat/>
|
||||
|
||||
## Usage
|
||||
|
||||
> The **minimum** nodejs version for this package is `16`
|
||||
@@ -20,47 +18,20 @@ import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||
import { C } from "@deltachat/jsonrpc-client";
|
||||
|
||||
async function main() {
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
dc.close();
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
dc.close()
|
||||
}
|
||||
main();
|
||||
main()
|
||||
```
|
||||
|
||||
For a more complete example refer to <https://github.com/deltachat-bot/echo/tree/master/nodejs_stdio_jsonrpc>.
|
||||
For a more complete example refer to https://github.com/deltachat-bot/echo/pull/69/files (TODO change link when pr is merged).
|
||||
|
||||
## How to use on an unsupported platform
|
||||
|
||||
You need to have rust installed to compile deltachat core for your platform and cpu architecture.
|
||||
<https://rustup.rs/> is the recommended way to install rust.
|
||||
Also your system probably needs more than 4GB RAM to compile core, alternatively your could try to build the debug build, which might take less RAM to build.
|
||||
<!-- todo instructions, will uses an env var for pointing to `deltachat-rpc-server` binary -->
|
||||
|
||||
1. clone the core repo, right next to your project folder: `git clone git@github.com:deltachat/deltachat-core-rust.git`
|
||||
2. go into your core checkout and run `git pull` and `git checkout <version>` to point it to the correct version (needs to be the same version the `@deltachat/jsonrpc-client` package has)
|
||||
3. run `cargo build --release --package deltachat-rpc-server --bin deltachat-rpc-server`
|
||||
|
||||
Then you have 2 options:
|
||||
|
||||
### point to deltachat-rpc-server via direct path:
|
||||
|
||||
```sh
|
||||
# start your app with the DELTA_CHAT_RPC_SERVER env var
|
||||
DELTA_CHAT_RPC_SERVER="../deltachat-core-rust/target/release/deltachat-rpc-server" node myapp.js
|
||||
```
|
||||
|
||||
### install deltachat-rpc-server in your $PATH:
|
||||
|
||||
```sh
|
||||
# use this to install to ~/.cargo/bin
|
||||
cargo install --release --package deltachat-rpc-server --bin deltachat-rpc-server
|
||||
# or manually move deltachat-core-rust/target/release/deltachat-rpc-server
|
||||
# to a location that is included in your $PATH Environment variable.
|
||||
```
|
||||
|
||||
And make sure to enable the `takeVersionFromPATH` option:
|
||||
```js
|
||||
startDeltaChat("data-dir", { takeVersionFromPATH: true });
|
||||
```
|
||||
<!-- todo copy parts from https://github.com/deltachat/deltachat-desktop/blob/7045c6f549e4b9d5caa0709d5bd314bbd9fd53db/docs/UPDATE_CORE.md -->
|
||||
|
||||
## How does it work when you install it
|
||||
|
||||
@@ -75,7 +46,7 @@ references:
|
||||
When you import this package it searches for the rpc server in the following locations and order:
|
||||
|
||||
1. `DELTA_CHAT_RPC_SERVER` environment variable
|
||||
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
|
||||
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
|
||||
3. prebuilds in npm packages
|
||||
|
||||
so by default it uses the prebuilds.
|
||||
|
||||
168
src/calls.rs
168
src/calls.rs
@@ -617,33 +617,7 @@ struct IceServer {
|
||||
pub credential: Option<String>,
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers.
|
||||
async fn create_ice_servers(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<String> {
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, port, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| format!("turn:{addr}"))
|
||||
.collect();
|
||||
|
||||
let ice_server = IceServer {
|
||||
urls,
|
||||
username: Some(username.to_string()),
|
||||
credential: Some(password.to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&[ice_server])?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers from a line received over IMAP METADATA.
|
||||
/// Creates ICE servers from a line received over IMAP METADATA.
|
||||
///
|
||||
/// IMAP METADATA returns a line such as
|
||||
/// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
@@ -653,20 +627,107 @@ async fn create_ice_servers(
|
||||
/// while `8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
/// is the password.
|
||||
pub(crate) async fn create_ice_servers_from_metadata(
|
||||
context: &Context,
|
||||
metadata: &str,
|
||||
) -> Result<(i64, String)> {
|
||||
) -> Result<(i64, Vec<UnresolvedIceServer>)> {
|
||||
let (hostname, rest) = metadata.split_once(':').context("Missing hostname")?;
|
||||
let (port, rest) = rest.split_once(':').context("Missing port")?;
|
||||
let port = u16::from_str(port).context("Failed to parse the port")?;
|
||||
let (ts, password) = rest.split_once(':').context("Missing timestamp")?;
|
||||
let expiration_timestamp = i64::from_str(ts).context("Failed to parse the timestamp")?;
|
||||
let ice_servers = create_ice_servers(context, hostname, port, ts, password).await?;
|
||||
let ice_servers = vec![UnresolvedIceServer::Turn {
|
||||
hostname: hostname.to_string(),
|
||||
port,
|
||||
username: ts.to_string(),
|
||||
credential: password.to_string(),
|
||||
}];
|
||||
Ok((expiration_timestamp, ice_servers))
|
||||
}
|
||||
|
||||
/// STUN or TURN server with unresolved DNS name.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum UnresolvedIceServer {
|
||||
/// STUN server.
|
||||
Stun { hostname: String, port: u16 },
|
||||
|
||||
/// TURN server with the username and password.
|
||||
Turn {
|
||||
hostname: String,
|
||||
port: u16,
|
||||
username: String,
|
||||
credential: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Resolves domain names of ICE servers.
|
||||
///
|
||||
/// On failure to resolve, logs the error
|
||||
/// and skips the server, but does not fail.
|
||||
pub(crate) async fn resolve_ice_servers(
|
||||
context: &Context,
|
||||
unresolved_ice_servers: Vec<UnresolvedIceServer>,
|
||||
) -> Result<String> {
|
||||
let mut result: Vec<IceServer> = Vec::new();
|
||||
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
|
||||
for unresolved_ice_server in unresolved_ice_servers {
|
||||
match unresolved_ice_server {
|
||||
UnresolvedIceServer::Stun { hostname, port } => {
|
||||
match lookup_host_with_cache(context, &hostname, port, "", load_cache).await {
|
||||
Ok(addrs) => {
|
||||
let urls: Vec<String> = addrs
|
||||
.into_iter()
|
||||
.map(|addr| format!("stun:{addr}"))
|
||||
.collect();
|
||||
let stun_server = IceServer {
|
||||
urls,
|
||||
username: None,
|
||||
credential: None,
|
||||
};
|
||||
result.push(stun_server);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to resolve STUN {hostname}:{port}: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
UnresolvedIceServer::Turn {
|
||||
hostname,
|
||||
port,
|
||||
username,
|
||||
credential,
|
||||
} => match lookup_host_with_cache(context, &hostname, port, "", load_cache).await {
|
||||
Ok(addrs) => {
|
||||
let urls: Vec<String> = addrs
|
||||
.into_iter()
|
||||
.map(|addr| format!("turn:{addr}"))
|
||||
.collect();
|
||||
let turn_server = IceServer {
|
||||
urls,
|
||||
username: Some(username),
|
||||
credential: Some(credential),
|
||||
};
|
||||
result.push(turn_server);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to resolve TURN {hostname}:{port}: {err:#}."
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
let json = serde_json::to_string(&result)?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Creates JSON with ICE servers when no TURN servers are known.
|
||||
pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<String> {
|
||||
pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
|
||||
// Do not use public STUN server from https://stunprotocol.org/.
|
||||
// It changes the hostname every year
|
||||
// (e.g. stunserver2025.stunprotocol.org
|
||||
@@ -674,36 +735,18 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
|
||||
// because of bandwidth costs:
|
||||
// <https://github.com/jselbie/stunserver/issues/50>
|
||||
|
||||
let hostname = "nine.testrun.org";
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| format!("stun:{addr}"))
|
||||
.collect();
|
||||
let stun_server = IceServer {
|
||||
urls,
|
||||
username: None,
|
||||
credential: None,
|
||||
};
|
||||
|
||||
let hostname = "turn.delta.chat";
|
||||
// Do not use cache because there is no TLS.
|
||||
let load_cache = false;
|
||||
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|addr| format!("turn:{addr}"))
|
||||
.collect();
|
||||
let turn_server = IceServer {
|
||||
urls,
|
||||
username: Some("public".to_string()),
|
||||
credential: Some("o4tR7yG4rG2slhXqRUf9zgmHz".to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&[stun_server, turn_server])?;
|
||||
Ok(json)
|
||||
vec![
|
||||
UnresolvedIceServer::Stun {
|
||||
hostname: "nine.testrun.org".to_string(),
|
||||
port: STUN_PORT,
|
||||
},
|
||||
UnresolvedIceServer::Turn {
|
||||
hostname: "turn.delta.chat".to_string(),
|
||||
port: STUN_PORT,
|
||||
username: "public".to_string(),
|
||||
credential: "o4tR7yG4rG2slhXqRUf9zgmHz".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Returns JSON with ICE servers.
|
||||
@@ -717,7 +760,8 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
|
||||
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
|
||||
pub async fn ice_servers(context: &Context) -> Result<String> {
|
||||
if let Some(ref metadata) = *context.metadata.read().await {
|
||||
Ok(metadata.ice_servers.clone())
|
||||
let ice_servers = resolve_ice_servers(context, metadata.ice_servers.clone()).await?;
|
||||
Ok(ice_servers)
|
||||
} else {
|
||||
Ok("[]".to_string())
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ impl Chatlist {
|
||||
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
|
||||
/// chats
|
||||
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
/// and hides the device-chat and contact requests
|
||||
/// and hides the device-chat, contact requests and incoming broadcasts.
|
||||
/// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
/// - if the flag DC_GCL_NO_SPECIALS is set, archive link is not added
|
||||
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||
@@ -224,8 +224,9 @@ impl Chatlist {
|
||||
let process_rows = |rows: rusqlite::AndThenRows<_>| {
|
||||
rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row {
|
||||
Ok((chat_id, typ, param, msg_id)) => {
|
||||
if typ == Chattype::Mailinglist
|
||||
&& param.get(Param::ListPost).is_none_or_empty()
|
||||
if typ == Chattype::InBroadcast
|
||||
|| (typ == Chattype::Mailinglist
|
||||
&& param.get(Param::ListPost).is_none_or_empty())
|
||||
{
|
||||
None
|
||||
} else {
|
||||
@@ -597,6 +598,41 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
/// Test that DC_CHAT_TYPE_IN_BROADCAST are hidden
|
||||
/// and DC_CHAT_TYPE_OUT_BROADCAST are shown in chatlist for forwarding.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_visiblity_on_forward() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_broadcast_a_id = create_broadcast(alice, "Channel Alice".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_broadcast_a_id))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_broadcast_a_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let bob_broadcast_b_id = create_broadcast(bob, "Channel Bob".to_string()).await?;
|
||||
|
||||
let chats = Chatlist::try_load(bob, DC_GCL_FOR_FORWARDING, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!chats
|
||||
.iter()
|
||||
.any(|(chat_id, _)| chat_id == &bob_broadcast_a_id),
|
||||
"alice broadcast is not shown in bobs forwarding chatlist"
|
||||
);
|
||||
assert!(
|
||||
chats
|
||||
.iter()
|
||||
.any(|(chat_id, _)| chat_id == &bob_broadcast_b_id),
|
||||
"bobs own broadcast is shown in his forwarding chatlist"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_special_chat_names() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
@@ -13,6 +13,8 @@ mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
pub(crate) mod server_params;
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure, format_err};
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
@@ -557,6 +559,25 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
let ctx2 = ctx.clone();
|
||||
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
|
||||
|
||||
if !param.dns_prefill.is_empty() {
|
||||
let mut ips: Vec<IpAddr> = Vec::new();
|
||||
for ip in ¶m.dns_prefill {
|
||||
match ip.parse::<IpAddr>() {
|
||||
Ok(ip) => ips.push(ip),
|
||||
Err(err) => {
|
||||
error!(
|
||||
ctx,
|
||||
"IP address prefill failed: parsing of address '{ip}' failed: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.dns_memory_cache
|
||||
.write()
|
||||
.await
|
||||
.insert(param.imap.server.clone(), ips);
|
||||
}
|
||||
|
||||
let configured_param = get_configured_param(ctx, param).await?;
|
||||
let proxy_config = ProxyConfig::load(ctx).await?;
|
||||
let strict_tls = configured_param.strict_tls(proxy_config.is_some());
|
||||
|
||||
@@ -318,6 +318,11 @@ pub struct InnerContext {
|
||||
) -> mail_builder::mime::MimePart<'a>,
|
||||
>,
|
||||
>,
|
||||
|
||||
/// Short lived DNS cache which only lives in memory.
|
||||
/// Used for configuration from `dcaccount` links with ip address.
|
||||
/// Like `dcaccount:example.org?a=127.0.0.1,[::1]`
|
||||
pub(crate) dns_memory_cache: Arc<RwLock<HashMap<String, Vec<std::net::IpAddr>>>>,
|
||||
}
|
||||
|
||||
/// The state of ongoing process.
|
||||
@@ -494,6 +499,7 @@ impl Context {
|
||||
self_fingerprint: OnceLock::new(),
|
||||
connectivities: parking_lot::Mutex::new(Vec::new()),
|
||||
pre_encrypt_mime_hook: None.into(),
|
||||
dns_memory_cache: Arc::new(RwLock::new(HashMap::new())),
|
||||
};
|
||||
|
||||
let ctx = Context {
|
||||
@@ -1120,21 +1126,19 @@ impl Context {
|
||||
let list = self
|
||||
.sql
|
||||
.query_map_vec(
|
||||
concat!(
|
||||
"SELECT m.id",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN contacts ct",
|
||||
" ON m.from_id=ct.id",
|
||||
" LEFT JOIN chats c",
|
||||
" ON m.chat_id=c.id",
|
||||
" WHERE m.state=?",
|
||||
" AND m.hidden=0",
|
||||
" AND m.chat_id>9",
|
||||
" AND ct.blocked=0",
|
||||
" AND c.blocked=0",
|
||||
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
),
|
||||
"SELECT m.id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
LEFT JOIN chats c
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.state=?
|
||||
AND m.hidden=0
|
||||
AND m.chat_id>9
|
||||
AND ct.blocked=0
|
||||
AND c.blocked=0
|
||||
AND NOT(c.muted_until=-1 OR c.muted_until>?)
|
||||
ORDER BY m.timestamp DESC,m.id DESC",
|
||||
(MessageState::InFresh, time()),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
|
||||
25
src/imap.rs
25
src/imap.rs
@@ -23,7 +23,9 @@ use num_traits::FromPrimitive;
|
||||
use ratelimit::Ratelimit;
|
||||
use url::Url;
|
||||
|
||||
use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata};
|
||||
use crate::calls::{
|
||||
UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
|
||||
};
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
@@ -134,16 +136,15 @@ pub(crate) struct ServerMetadata {
|
||||
|
||||
pub iroh_relay: Option<Url>,
|
||||
|
||||
/// JSON with ICE servers for WebRTC calls
|
||||
/// and the expiration timestamp.
|
||||
///
|
||||
/// If JSON is about to expire, new TURN credentials
|
||||
/// should be fetched from the server
|
||||
/// to be ready for WebRTC calls.
|
||||
pub ice_servers: String,
|
||||
/// ICE servers for WebRTC calls.
|
||||
pub ice_servers: Vec<UnresolvedIceServer>,
|
||||
|
||||
/// Timestamp when ICE servers are considered
|
||||
/// expired and should be updated.
|
||||
///
|
||||
/// If ICE servers are about to expire, new TURN credentials
|
||||
/// should be fetched from the server
|
||||
/// to be ready for WebRTC calls.
|
||||
pub ice_servers_expiration_timestamp: i64,
|
||||
}
|
||||
|
||||
@@ -1552,7 +1553,7 @@ impl Session {
|
||||
if m.entry == "/shared/vendor/deltachat/turn"
|
||||
&& let Some(value) = m.value
|
||||
{
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
match create_ice_servers_from_metadata(&value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
old_metadata.ice_servers = parsed_ice_servers;
|
||||
@@ -1569,7 +1570,7 @@ impl Session {
|
||||
info!(context, "Will use fallback ICE servers.");
|
||||
// Set expiration timestamp 7 days in the future so we don't request it again.
|
||||
old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
|
||||
old_metadata.ice_servers = create_fallback_ice_servers(context).await?;
|
||||
old_metadata.ice_servers = create_fallback_ice_servers();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1616,7 +1617,7 @@ impl Session {
|
||||
}
|
||||
"/shared/vendor/deltachat/turn" => {
|
||||
if let Some(value) = m.value {
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
match create_ice_servers_from_metadata(&value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
ice_servers = Some(parsed_ice_servers);
|
||||
@@ -1635,7 +1636,7 @@ impl Session {
|
||||
} else {
|
||||
// Set expiration timestamp 7 days in the future so we don't request it again.
|
||||
ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
|
||||
create_fallback_ice_servers(context).await?
|
||||
create_fallback_ice_servers()
|
||||
};
|
||||
|
||||
*lock = Some(ServerMetadata {
|
||||
|
||||
@@ -97,6 +97,9 @@ pub struct EnteredLoginParam {
|
||||
|
||||
/// If true, login via OAUTH2 (not recommended anymore)
|
||||
pub oauth2: bool,
|
||||
|
||||
/// IP addresses for prefilling DNS
|
||||
pub dns_prefill: Vec<String>,
|
||||
}
|
||||
|
||||
impl EnteredLoginParam {
|
||||
@@ -191,6 +194,7 @@ impl EnteredLoginParam {
|
||||
},
|
||||
certificate_checks,
|
||||
oauth2,
|
||||
dns_prefill: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -360,6 +364,7 @@ mod tests {
|
||||
},
|
||||
certificate_checks: Default::default(),
|
||||
oauth2: false,
|
||||
dns_prefill: Default::default(),
|
||||
};
|
||||
param.save(&t).await?;
|
||||
assert_eq!(
|
||||
|
||||
@@ -87,12 +87,10 @@ impl MsgId {
|
||||
let result = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
concat!(
|
||||
"SELECT m.state, mdns.msg_id",
|
||||
" FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
|
||||
" WHERE id=?",
|
||||
" LIMIT 1",
|
||||
),
|
||||
"SELECT m.state, mdns.msg_id
|
||||
FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
|
||||
WHERE id=?
|
||||
LIMIT 1",
|
||||
(self,),
|
||||
|row| {
|
||||
let state: MessageState = row.get(0)?;
|
||||
@@ -501,40 +499,38 @@ impl Message {
|
||||
let mut msg = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
concat!(
|
||||
"SELECT",
|
||||
" m.id AS id,",
|
||||
" rfc724_mid AS rfc724mid,",
|
||||
" pre_rfc724_mid AS pre_rfc724mid,",
|
||||
" m.mime_in_reply_to AS mime_in_reply_to,",
|
||||
" m.chat_id AS chat_id,",
|
||||
" m.from_id AS from_id,",
|
||||
" m.to_id AS to_id,",
|
||||
" m.timestamp AS timestamp,",
|
||||
" m.timestamp_sent AS timestamp_sent,",
|
||||
" m.timestamp_rcvd AS timestamp_rcvd,",
|
||||
" m.ephemeral_timer AS ephemeral_timer,",
|
||||
" m.ephemeral_timestamp AS ephemeral_timestamp,",
|
||||
" m.type AS type,",
|
||||
" m.state AS state,",
|
||||
" mdns.msg_id AS mdn_msg_id,",
|
||||
" m.download_state AS download_state,",
|
||||
" m.error AS error,",
|
||||
" m.msgrmsg AS msgrmsg,",
|
||||
" m.starred AS original_msg_id,",
|
||||
" m.mime_modified AS mime_modified,",
|
||||
" m.txt AS txt,",
|
||||
" m.subject AS subject,",
|
||||
" m.param AS param,",
|
||||
" m.hidden AS hidden,",
|
||||
" m.location_id AS location,",
|
||||
" c.blocked AS blocked",
|
||||
" FROM msgs m",
|
||||
" LEFT JOIN chats c ON c.id=m.chat_id",
|
||||
" LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
|
||||
" WHERE m.id=? AND chat_id!=3",
|
||||
" LIMIT 1",
|
||||
),
|
||||
"SELECT
|
||||
m.id AS id,
|
||||
rfc724_mid AS rfc724mid,
|
||||
pre_rfc724_mid AS pre_rfc724mid,
|
||||
m.mime_in_reply_to AS mime_in_reply_to,
|
||||
m.chat_id AS chat_id,
|
||||
m.from_id AS from_id,
|
||||
m.to_id AS to_id,
|
||||
m.timestamp AS timestamp,
|
||||
m.timestamp_sent AS timestamp_sent,
|
||||
m.timestamp_rcvd AS timestamp_rcvd,
|
||||
m.ephemeral_timer AS ephemeral_timer,
|
||||
m.ephemeral_timestamp AS ephemeral_timestamp,
|
||||
m.type AS type,
|
||||
m.state AS state,
|
||||
mdns.msg_id AS mdn_msg_id,
|
||||
m.download_state AS download_state,
|
||||
m.error AS error,
|
||||
m.msgrmsg AS msgrmsg,
|
||||
m.starred AS original_msg_id,
|
||||
m.mime_modified AS mime_modified,
|
||||
m.txt AS txt,
|
||||
m.subject AS subject,
|
||||
m.param AS param,
|
||||
m.hidden AS hidden,
|
||||
m.location_id AS location,
|
||||
c.blocked AS blocked
|
||||
FROM msgs m
|
||||
LEFT JOIN chats c ON c.id=m.chat_id
|
||||
LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
|
||||
WHERE m.id=? AND chat_id!=3
|
||||
LIMIT 1",
|
||||
(id,),
|
||||
|row| {
|
||||
let state: MessageState = row.get("state")?;
|
||||
|
||||
@@ -426,8 +426,16 @@ impl MimeFactory {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let recipient_ids: Vec<_> = recipient_ids.into_iter().collect();
|
||||
ContactId::scaleup_origin(context, &recipient_ids, Origin::OutgoingTo).await?;
|
||||
let recipient_ids: Vec<_> = recipient_ids
|
||||
.into_iter()
|
||||
.filter(|id| *id != ContactId::SELF)
|
||||
.collect();
|
||||
if recipient_ids.len() == 1
|
||||
&& msg.param.get_cmd() != SystemMessage::MemberRemovedFromGroup
|
||||
&& chat.typ != Chattype::OutBroadcast
|
||||
{
|
||||
ContactId::scaleup_origin(context, &recipient_ids, Origin::OutgoingTo).await?;
|
||||
}
|
||||
|
||||
if !msg.is_system_message()
|
||||
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||
|
||||
@@ -2481,18 +2481,16 @@ async fn handle_mdn(
|
||||
let Some((msg_id, chat_id, has_mdns, is_dup)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
concat!(
|
||||
"SELECT",
|
||||
" m.id AS msg_id,",
|
||||
" c.id AS chat_id,",
|
||||
" mdns.contact_id AS mdn_contact",
|
||||
" FROM msgs m ",
|
||||
" LEFT JOIN chats c ON m.chat_id=c.id",
|
||||
" LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
|
||||
" WHERE rfc724_mid=? AND from_id=1",
|
||||
" ORDER BY msg_id DESC, mdn_contact=? DESC",
|
||||
" LIMIT 1",
|
||||
),
|
||||
"SELECT
|
||||
m.id AS msg_id,
|
||||
c.id AS chat_id,
|
||||
mdns.contact_id AS mdn_contact
|
||||
FROM msgs m
|
||||
LEFT JOIN chats c ON m.chat_id=c.id
|
||||
LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
|
||||
WHERE rfc724_mid=? AND from_id=1
|
||||
ORDER BY msg_id DESC, mdn_contact=? DESC
|
||||
LIMIT 1",
|
||||
(&rfc724_mid, from_id),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get("msg_id")?;
|
||||
|
||||
@@ -860,6 +860,14 @@ pub(crate) async fn lookup_host_with_cache(
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ips) = context.dns_memory_cache.read().await.get(hostname) {
|
||||
for ip in ips {
|
||||
let addr = SocketAddr::new(*ip, port);
|
||||
if !cache.contains(&addr) {
|
||||
cache.push(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
merge_with_cache(resolved_addrs, cache)
|
||||
} else {
|
||||
|
||||
30
src/qr.rs
30
src/qr.rs
@@ -12,6 +12,7 @@ use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
|
||||
use rand::TryRngCore as _;
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
@@ -656,6 +657,7 @@ async fn decode_ideltachat(context: &Context, prefix: &str, qr: &str) -> Result<
|
||||
/// scheme: `DCACCOUNT:example.org`
|
||||
/// or `DCACCOUNT:https://example.org/new`
|
||||
/// or `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
|
||||
/// or `dcaccount:example.org?a=127.0.0.1,[::1]`
|
||||
fn decode_account(qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(DCACCOUNT_SCHEME.len()..)
|
||||
@@ -784,9 +786,33 @@ pub(crate) async fn login_param_from_account_qr(
|
||||
if !payload.starts_with(HTTPS_SCHEME) {
|
||||
let rng = &mut rand::rngs::OsRng.unwrap_err();
|
||||
let username = Alphanumeric.sample_string(rng, 9);
|
||||
let addr = username + "@" + payload;
|
||||
let host = if let Some(start_of_query) = payload.find("?") {
|
||||
payload
|
||||
.get(..start_of_query)
|
||||
.context("failed to ignore query part")?
|
||||
} else {
|
||||
payload
|
||||
};
|
||||
let addr = username + "@" + host;
|
||||
let password = Alphanumeric.sample_string(rng, 50);
|
||||
|
||||
let dns_prefill: Vec<String> = match Url::parse(qr) {
|
||||
Ok(url) => {
|
||||
let options = url.query_pairs();
|
||||
let parameter_map: BTreeMap<String, String> = options
|
||||
.map(|(key, value)| (key.into_owned(), value.into_owned()))
|
||||
.collect();
|
||||
parameter_map
|
||||
.get("a")
|
||||
.map(|ips| ips.split(",").map(|s| s.to_owned()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "error parsing parameter of account url: {err}");
|
||||
Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
let param = EnteredLoginParam {
|
||||
addr,
|
||||
imap: EnteredServerLoginParam {
|
||||
@@ -796,6 +822,7 @@ pub(crate) async fn login_param_from_account_qr(
|
||||
smtp: Default::default(),
|
||||
certificate_checks: EnteredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
dns_prefill,
|
||||
};
|
||||
return Ok(param);
|
||||
}
|
||||
@@ -816,6 +843,7 @@ pub(crate) async fn login_param_from_account_qr(
|
||||
smtp: Default::default(),
|
||||
certificate_checks: EnteredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
dns_prefill: Default::default(),
|
||||
};
|
||||
|
||||
Ok(param)
|
||||
|
||||
@@ -191,6 +191,7 @@ pub(crate) fn login_param_from_login_qr(
|
||||
},
|
||||
certificate_checks: certificate_checks.unwrap_or_default(),
|
||||
oauth2: false,
|
||||
dns_prefill: Default::default(),
|
||||
};
|
||||
Ok(param)
|
||||
}
|
||||
|
||||
@@ -711,6 +711,31 @@ async fn test_decode_account() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_account_with_dns_prefill() -> Result<()> {
|
||||
let ctx = &TestContext::new().await;
|
||||
|
||||
for (qr, prefill_ips) in [
|
||||
(
|
||||
"dcaccount:example.org?a=127.0.0.1,[::1]",
|
||||
vec!["127.0.0.1", "[::1]"],
|
||||
),
|
||||
(
|
||||
"DCACCOUNT:example.org?a=127.0.0.1,[::1]",
|
||||
vec!["127.0.0.1", "[::1]"],
|
||||
),
|
||||
("dcaccount:example.org?a=[::1]", vec!["[::1]"]),
|
||||
("DCACCOUNT:example.org?a=127.0.0.1", vec!["127.0.0.1"]),
|
||||
] {
|
||||
let param = login_param_from_account_qr(ctx, qr).await?;
|
||||
println!("addr {}", param.addr);
|
||||
assert!(param.addr.ends_with("example.org"));
|
||||
assert_eq!(param.dns_prefill, prefill_ips);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_tg_socks_proxy() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
@@ -3878,6 +3878,29 @@ async fn test_group_contacts_goto_bottom() -> Result<()> {
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
assert_eq!(contacts[0], bob_fiona_id);
|
||||
|
||||
send_text_msg(
|
||||
bob,
|
||||
bob_chat_id,
|
||||
"Hi Alice, stay down in my contact list".to_string(),
|
||||
)
|
||||
.await?;
|
||||
bob.pop_sent_msg().await;
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
assert_eq!(contacts[0], bob_fiona_id);
|
||||
|
||||
remove_contact_from_chat(bob, bob_chat_id, bob_fiona_id).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
// Fiona is still the 0th contact. This makes sense, maybe Bob is going to remove Alice from the
|
||||
// chat too, so no need to make Alice a more "important" contact yet.
|
||||
assert_eq!(contacts[0], bob_fiona_id);
|
||||
|
||||
send_text_msg(bob, bob_chat_id, "Alice, jump up!".to_string()).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
let contacts = Contact::get_all(bob, 0, None).await?;
|
||||
let bob_alice_id = bob.add_or_lookup_contact_id(alice).await;
|
||||
assert_eq!(contacts[0], bob_alice_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use anyhow::{Context as _, Result};
|
||||
use deltachat_derive::FromSql;
|
||||
use num_traits::ToPrimitive;
|
||||
use pgp::types::PublicKeyTrait;
|
||||
use rand::distr::SampleString as _;
|
||||
use rusqlite::OptionalExtension;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -21,7 +22,7 @@ use crate::key::load_self_public_keyring;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::securejoin::QrInvite;
|
||||
use crate::tools::{create_id, time};
|
||||
use crate::tools::time;
|
||||
|
||||
pub(crate) const STATISTICS_BOT_EMAIL: &str = "self_reporting@testrun.org";
|
||||
const STATISTICS_BOT_VCARD: &str = include_str!("../assets/statistics-bot.vcf");
|
||||
@@ -390,7 +391,9 @@ pub(crate) async fn stats_id(context: &Context) -> Result<String> {
|
||||
Ok(match context.get_config(Config::StatsId).await? {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let id = create_id();
|
||||
let id = rand::distr::Alphabetic
|
||||
.sample_string(&mut rand::rng(), 25)
|
||||
.to_lowercase();
|
||||
context
|
||||
.set_config_internal(Config::StatsId, Some(&id))
|
||||
.await?;
|
||||
|
||||
Reference in New Issue
Block a user