mirror of
https://github.com/chatmail/core.git
synced 2026-10-08 22:20:17 +03:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ba45134ea | ||
|
|
5d512a726d | ||
|
|
bfa8da092c | ||
|
|
72eef59f25 | ||
|
|
5f3dbe0037 | ||
|
|
a74edb2f86 | ||
|
|
963335e660 | ||
|
|
b1ace43b28 | ||
|
|
1ac520439b | ||
|
|
c4d849a502 | ||
|
|
78bac6021f | ||
|
|
4602b661d9 | ||
|
|
c65ce1b673 |
@@ -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", contact requests and incoming broadcasts.
|
||||
* and hides the "Device chat" and contact requests.
|
||||
* 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
|
||||
@@ -1563,7 +1563,7 @@ dc_array_t* dc_wait_next_msgs (dc_context_t* context);
|
||||
* Mark all messages in a chat as _noticed_.
|
||||
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
* but are still waiting for being marked as "seen" using dc_markseen_msgs()
|
||||
* (read receipts aren't sent for noticed messages).
|
||||
* (IMAP/MDNs is not done for noticed messages).
|
||||
*
|
||||
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
* See also dc_markseen_msgs().
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -24,11 +25,28 @@ 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
|
||||
|
||||
@@ -11,8 +11,8 @@ use deltachat::blob::BlobObject;
|
||||
use deltachat::calls::ice_servers;
|
||||
use deltachat::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
||||
get_chat_msgs_ex, marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat, Chat,
|
||||
ChatId, ChatItem, MessageListOptions,
|
||||
get_chat_msgs_ex, marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem,
|
||||
MessageListOptions,
|
||||
};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::config::{get_all_ui_config_keys, Config};
|
||||
@@ -23,8 +23,8 @@ use deltachat::ephemeral::Timer;
|
||||
use deltachat::imex;
|
||||
use deltachat::location;
|
||||
use deltachat::message::{
|
||||
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
|
||||
markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipts, markseen_msgs, Message,
|
||||
MessageState, MsgId, Viewtype,
|
||||
};
|
||||
use deltachat::peer_channels::{
|
||||
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
||||
@@ -1164,24 +1164,10 @@ impl CommandApi {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Mark all messages in all chats as _noticed_.
|
||||
/// Skips messages from blocked contacts, but does not skip messages in muted chats.
|
||||
///
|
||||
/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
/// but are still waiting for being marked as "seen" using markseen_msgs()
|
||||
/// (read receipts aren't sent for noticed messages).
|
||||
///
|
||||
/// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
/// See also markseen_msgs().
|
||||
pub async fn marknoticed_all_chats(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
marknoticed_all_chats(&ctx).await
|
||||
}
|
||||
|
||||
/// Mark all messages in a chat as _noticed_.
|
||||
/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
/// but are still waiting for being marked as "seen" using markseen_msgs()
|
||||
/// (read receipts aren't sent for noticed messages).
|
||||
/// (IMAP/MDNs is not done for noticed messages).
|
||||
///
|
||||
/// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
/// See also markseen_msgs().
|
||||
@@ -1448,18 +1434,6 @@ impl CommandApi {
|
||||
MessageInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
|
||||
}
|
||||
|
||||
/// Returns count of read receipts on message.
|
||||
///
|
||||
/// This view count is meant as a feedback measure for the channel owner only.
|
||||
async fn get_message_read_receipt_count(
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
) -> Result<usize> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
get_msg_read_receipt_count(&ctx, MsgId::new(message_id)).await
|
||||
}
|
||||
|
||||
/// Returns contacts that sent read receipts and the time of reading.
|
||||
async fn get_message_read_receipts(
|
||||
&self,
|
||||
|
||||
@@ -59,7 +59,7 @@ pub struct FullChat {
|
||||
|
||||
is_device_chat: bool,
|
||||
/// Note that this is different from
|
||||
/// [`ChatListItem::self_in_group`](`crate::api::types::chat_list::ChatListItemFetchResult::ChatListItem::self_in_group`).
|
||||
/// [`ChatListItem::is_self_in_group`](`crate::api::types::chat_list::ChatListItemFetchResult::ChatListItem::is_self_in_group`).
|
||||
/// This property should only be accessed
|
||||
/// when [`FullChat::chat_type`] is [`Chattype::Group`].
|
||||
//
|
||||
@@ -178,9 +178,8 @@ pub struct BasicChat {
|
||||
is_self_talk: bool,
|
||||
color: String,
|
||||
is_contact_request: bool,
|
||||
// deprecated 2026-01, use is_device_talk instead
|
||||
|
||||
is_device_chat: bool,
|
||||
is_device_talk: bool,
|
||||
is_muted: bool,
|
||||
}
|
||||
|
||||
@@ -208,7 +207,6 @@ impl BasicChat {
|
||||
color,
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
is_device_chat: chat.is_device_talk(),
|
||||
is_device_talk: chat.is_device_talk(),
|
||||
is_muted: chat.is_muted(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,15 +17,12 @@ use super::message::MessageViewtype;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
#[expect(clippy::large_enum_variant)]
|
||||
pub enum ChatListItemFetchResult {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatListItem {
|
||||
id: u32,
|
||||
name: String,
|
||||
/// deprecated 2026-01, use profile_image instead
|
||||
avatar_path: Option<String>,
|
||||
profile_image: Option<String>,
|
||||
color: String,
|
||||
chat_type: JsonrpcChatType,
|
||||
last_updated: Option<i64>,
|
||||
@@ -64,14 +61,9 @@ pub enum ChatListItemFetchResult {
|
||||
is_self_talk: bool,
|
||||
is_device_talk: bool,
|
||||
is_sending_location: bool,
|
||||
/// deprecated 2026-01
|
||||
is_self_in_group: bool,
|
||||
/// deprecated 2026-01, use archived instead
|
||||
is_archived: bool,
|
||||
archived: bool,
|
||||
/// deprecated 2026-01, use pinned instead
|
||||
is_pinned: bool,
|
||||
pinned: bool,
|
||||
is_muted: bool,
|
||||
is_contact_request: bool,
|
||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||
@@ -113,7 +105,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
|
||||
let visibility = chat.get_visibility();
|
||||
|
||||
let profile_image = chat
|
||||
let avatar_path = chat
|
||||
.get_profile_image(ctx)
|
||||
.await?
|
||||
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
|
||||
@@ -158,8 +150,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
Ok(ChatListItemFetchResult::ChatListItem {
|
||||
id: chat_id.to_u32(),
|
||||
name: chat.get_name().to_owned(),
|
||||
profile_image: profile_image.clone(),
|
||||
avatar_path: profile_image.clone(),
|
||||
avatar_path,
|
||||
color,
|
||||
chat_type: chat.get_type().into(),
|
||||
last_updated,
|
||||
@@ -174,9 +165,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
is_device_talk: chat.is_device_talk(),
|
||||
is_self_in_group: chat.is_self_in_chat(ctx).await?,
|
||||
is_sending_location: chat.is_sending_locations(),
|
||||
archived: visibility == ChatVisibility::Archived,
|
||||
is_archived: visibility == ChatVisibility::Archived,
|
||||
pinned: visibility == ChatVisibility::Pinned,
|
||||
is_pinned: visibility == ChatVisibility::Pinned,
|
||||
is_muted: chat.is_muted(),
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
# @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";
|
||||
|
||||
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||
export type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
accountId: number,
|
||||
event: Extract<EventType, { kind: Property }>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||
export type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
event: Extract<EventType, { kind: Property }>,
|
||||
) => void;
|
||||
|
||||
@@ -44,13 +44,8 @@ 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:
|
||||
@@ -60,11 +55,10 @@ def run_client_cli(
|
||||
"""
|
||||
from .client import Client
|
||||
|
||||
_run_cli(Client, until, hooks, argv, **kwargs)
|
||||
_run_cli(Client, 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,
|
||||
@@ -75,12 +69,11 @@ def run_bot_cli(
|
||||
"""
|
||||
from .client import Bot
|
||||
|
||||
_run_cli(Bot, until, hooks, argv, **kwargs)
|
||||
_run_cli(Bot, 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,
|
||||
@@ -118,7 +111,7 @@ def _run_cli(
|
||||
kwargs={"email": args.email, "password": args.password},
|
||||
)
|
||||
configure_thread.start()
|
||||
client.run_until(until)
|
||||
client.run_forever()
|
||||
|
||||
|
||||
def extract_addr(text: str) -> str:
|
||||
|
||||
@@ -14,7 +14,6 @@ from typing import (
|
||||
|
||||
from ._utils import (
|
||||
AttrDict,
|
||||
_forever,
|
||||
parse_system_add_remove,
|
||||
parse_system_image_changed,
|
||||
parse_system_title_changed,
|
||||
@@ -92,28 +91,19 @@ class Client:
|
||||
|
||||
def run_forever(self) -> None:
|
||||
"""Process events forever."""
|
||||
self.run_until(_forever)
|
||||
self.run_until(lambda _: False)
|
||||
|
||||
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||
"""Start the event processing loop."""
|
||||
"""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.
|
||||
"""
|
||||
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)
|
||||
@@ -122,13 +112,10 @@ class Client:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
self._process_messages()
|
||||
|
||||
stop = until_func(event)
|
||||
stop = 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):
|
||||
|
||||
@@ -44,14 +44,6 @@ class Message:
|
||||
read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
|
||||
return [AttrDict(read_receipt) for read_receipt in read_receipts]
|
||||
|
||||
def get_read_receipt_count(self) -> int:
|
||||
"""
|
||||
Returns count of read receipts on message.
|
||||
|
||||
This view count is meant as a feedback measure for the channel owner only.
|
||||
"""
|
||||
return self._rpc.get_message_read_receipt_count(self.account.id, self.id)
|
||||
|
||||
def get_reactions(self) -> Optional[AttrDict]:
|
||||
"""Get message reactions."""
|
||||
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
|
||||
@@ -911,47 +911,6 @@ def test_markseen_contact_request(acfactory):
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("team_profile", [True, False])
|
||||
def test_no_markseen_in_team_profile(team_profile, acfactory):
|
||||
"""
|
||||
Test that seen status is synchronized iff `team_profile` isn't set.
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
if team_profile:
|
||||
bob.set_config("team_profile", "1")
|
||||
|
||||
# Bob sets up a second device.
|
||||
bob2 = bob.clone()
|
||||
bob2.start_io()
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
bob_chat_alice = bob.create_chat(alice)
|
||||
bob2.create_chat(alice)
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
|
||||
message = bob.wait_for_incoming_msg()
|
||||
message2 = bob2.wait_for_incoming_msg()
|
||||
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||
|
||||
message.mark_seen()
|
||||
|
||||
# Send a message and wait until it arrives
|
||||
# in order to wait until Bob2 gets the markseen message.
|
||||
# This also tests that outgoing messages
|
||||
# don't mark preceeding messages as seen in team profiles.
|
||||
bob_chat_alice.send_text("Outgoing message")
|
||||
while True:
|
||||
outgoing = bob2.wait_for_msg(EventType.MSGS_CHANGED)
|
||||
if outgoing.id != 0:
|
||||
break
|
||||
assert outgoing.get_snapshot().text == "Outgoing message"
|
||||
|
||||
if team_profile:
|
||||
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||
else:
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
|
||||
def test_read_receipt(acfactory):
|
||||
"""
|
||||
Test sending a read receipt and ensure it is attributed to the correct contact.
|
||||
@@ -971,9 +930,6 @@ def test_read_receipt(acfactory):
|
||||
assert len(read_receipts) == 1
|
||||
assert read_receipts[0].contact_id == alice_contact_bob.id
|
||||
|
||||
read_receipt_cnt = read_msg.get_read_receipt_count()
|
||||
assert read_receipt_cnt == 1
|
||||
|
||||
|
||||
def test_get_http_response(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
@@ -1177,30 +1133,6 @@ def test_leave_broadcast(acfactory, all_devices_online):
|
||||
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
|
||||
|
||||
|
||||
def test_leave_and_delete_group(acfactory, log):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
log.section("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
alice_chat.add_contact(bob)
|
||||
assert len(alice_chat.get_contacts()) == 2 # Alice and Bob
|
||||
alice_chat.send_text("hello")
|
||||
|
||||
log.section("Bob sees the group, and leaves and deletes it")
|
||||
msg = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.text == "hello"
|
||||
msg.chat.accept()
|
||||
|
||||
msg.chat.leave()
|
||||
# Bob deletes the chat. This must not prevent the leave message from being sent.
|
||||
msg.chat.delete()
|
||||
|
||||
log.section("Alice receives the delete message")
|
||||
# After Bob left, only Alice will be left in the group:
|
||||
while len(alice_chat.get_contacts()) != 1:
|
||||
alice.wait_for_event(EventType.CHAT_MODIFIED)
|
||||
|
||||
|
||||
def test_immediate_autodelete(acfactory, direct_imap, log):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ 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`
|
||||
@@ -18,20 +20,47 @@ 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/pull/69/files (TODO change link when pr is merged).
|
||||
For a more complete example refer to <https://github.com/deltachat-bot/echo/tree/master/nodejs_stdio_jsonrpc>.
|
||||
|
||||
## How to use on an unsupported platform
|
||||
|
||||
<!-- todo instructions, will uses an env var for pointing to `deltachat-rpc-server` binary -->
|
||||
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 copy parts from https://github.com/deltachat/deltachat-desktop/blob/7045c6f549e4b9d5caa0709d5bd314bbd9fd53db/docs/UPDATE_CORE.md -->
|
||||
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 });
|
||||
```
|
||||
|
||||
## How does it work when you install it
|
||||
|
||||
@@ -46,7 +75,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.
|
||||
|
||||
+62
-106
@@ -617,7 +617,33 @@ struct IceServer {
|
||||
pub credential: Option<String>,
|
||||
}
|
||||
|
||||
/// Creates ICE servers from a line received over IMAP METADATA.
|
||||
/// 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.
|
||||
///
|
||||
/// IMAP METADATA returns a line such as
|
||||
/// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
@@ -627,107 +653,20 @@ struct IceServer {
|
||||
/// while `8Dqkyyu11MVESBqjbIylmB06rv8=`
|
||||
/// is the password.
|
||||
pub(crate) async fn create_ice_servers_from_metadata(
|
||||
context: &Context,
|
||||
metadata: &str,
|
||||
) -> Result<(i64, Vec<UnresolvedIceServer>)> {
|
||||
) -> Result<(i64, String)> {
|
||||
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 = vec![UnresolvedIceServer::Turn {
|
||||
hostname: hostname.to_string(),
|
||||
port,
|
||||
username: ts.to_string(),
|
||||
credential: password.to_string(),
|
||||
}];
|
||||
let ice_servers = create_ice_servers(context, hostname, port, ts, password).await?;
|
||||
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) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
|
||||
pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<String> {
|
||||
// Do not use public STUN server from https://stunprotocol.org/.
|
||||
// It changes the hostname every year
|
||||
// (e.g. stunserver2025.stunprotocol.org
|
||||
@@ -735,18 +674,36 @@ pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
|
||||
// because of bandwidth costs:
|
||||
// <https://github.com/jselbie/stunserver/issues/50>
|
||||
|
||||
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(),
|
||||
},
|
||||
]
|
||||
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)
|
||||
}
|
||||
|
||||
/// Returns JSON with ICE servers.
|
||||
@@ -760,8 +717,7 @@ pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
|
||||
/// <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 {
|
||||
let ice_servers = resolve_ice_servers(context, metadata.ice_servers.clone()).await?;
|
||||
Ok(ice_servers)
|
||||
Ok(metadata.ice_servers.clone())
|
||||
} else {
|
||||
Ok("[]".to_string())
|
||||
}
|
||||
|
||||
+40
-45
@@ -19,7 +19,6 @@ use strum_macros::EnumIter;
|
||||
|
||||
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::{
|
||||
@@ -43,7 +42,7 @@ use crate::mimefactory::{MimeFactory, RenderedEmail};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::receive_imf::ReceivedMsg;
|
||||
use crate::smtp::{self, send_msg_to_smtp};
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
@@ -52,6 +51,7 @@ use crate::tools::{
|
||||
gm2local_offset, normalize_text, smeared_time, time, truncate_msg_text,
|
||||
};
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
use crate::{chatlist_events, imap};
|
||||
|
||||
pub(crate) const PARAM_BROADCAST_SECRET: Param = Param::Arg3;
|
||||
|
||||
@@ -635,6 +635,10 @@ impl ChatId {
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT pre_rfc724_mid FROM msgs WHERE chat_id=? AND pre_rfc724_mid!='')",
|
||||
(&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,),
|
||||
@@ -2131,11 +2135,10 @@ pub(crate) async fn sync(context: &Context, id: SyncId, action: SyncAction) -> R
|
||||
}
|
||||
|
||||
/// Whether the chat is pinned or archived.
|
||||
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter, Default)]
|
||||
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter)]
|
||||
#[repr(i8)]
|
||||
pub enum ChatVisibility {
|
||||
/// Chat is neither archived nor pinned.
|
||||
#[default]
|
||||
Normal = 0,
|
||||
|
||||
/// Chat is archived.
|
||||
@@ -2835,11 +2838,32 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
// Send BCC to self if it is enabled.
|
||||
//
|
||||
// Previous versions of Delta Chat did not send BCC self
|
||||
// if DeleteServerAfter was set to immediately delete messages
|
||||
// from the server. This is not the case anymore
|
||||
// because BCC-self messages are also used to detect
|
||||
// that message was sent if SMTP server is slow to respond
|
||||
// and connection is frequently lost
|
||||
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
|
||||
// disabled by default is fine.
|
||||
//
|
||||
// `from` must be the last addr, see `receive_imf_inner()` why.
|
||||
recipients.retain(|x| x.to_lowercase() != lowercase_from);
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage
|
||||
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())
|
||||
{
|
||||
smtp::add_self_recipients(context, &mut recipients, needs_encryption).await?;
|
||||
// Avoid sending unencrypted messages to all transports, chatmail relays won't accept
|
||||
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
|
||||
// messages.
|
||||
if needs_encryption {
|
||||
for addr in context.get_secondary_self_addrs().await? {
|
||||
recipients.push(addr);
|
||||
}
|
||||
}
|
||||
recipients.push(from);
|
||||
}
|
||||
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out
|
||||
@@ -3206,36 +3230,6 @@ pub async fn get_chat_msgs_ex(
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Marks all unread messages in all chats as noticed.
|
||||
/// Ignores messages from blocked contacts, but does not ignore messages in muted chats.
|
||||
pub async fn marknoticed_all_chats(context: &Context) -> Result<()> {
|
||||
// The sql statement here is similar to the one in get_fresh_msgs
|
||||
let list = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT DISTINCT(c.id)
|
||||
FROM msgs m
|
||||
INNER JOIN chats c
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.state=?
|
||||
AND m.hidden=0
|
||||
AND m.chat_id>9
|
||||
AND c.blocked=0;",
|
||||
(MessageState::InFresh,),
|
||||
|row| {
|
||||
let msg_id: ChatId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
for chat_id in list {
|
||||
marknoticed_chat(context, chat_id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Marks all messages in the chat as noticed.
|
||||
/// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed.
|
||||
pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
|
||||
@@ -3297,7 +3291,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
let hidden_messages = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT id FROM msgs
|
||||
"SELECT id, rfc724_mid FROM msgs
|
||||
WHERE state=?
|
||||
AND hidden=1
|
||||
AND chat_id=?
|
||||
@@ -3305,11 +3299,16 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
(MessageState::InFresh, chat_id), // No need to check for InNoticed messages, because reactions are never InNoticed
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
let rfc724_mid: String = row.get(1)?;
|
||||
Ok((msg_id, rfc724_mid))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
message::markseen_msgs(context, hidden_messages).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(());
|
||||
}
|
||||
@@ -3331,10 +3330,6 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
context: &Context,
|
||||
mut msgs: Vec<ReceivedMsg>,
|
||||
) -> Result<()> {
|
||||
if context.get_config_bool(Config::TeamProfile).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
msgs.retain(|m| m.state.is_outgoing());
|
||||
if msgs.is_empty() {
|
||||
return Ok(());
|
||||
@@ -4117,7 +4112,7 @@ pub async fn remove_contact_from_chat(
|
||||
} else {
|
||||
let mut sync = Nosync;
|
||||
|
||||
if chat.is_promoted() && chat.typ != Chattype::OutBroadcast {
|
||||
if chat.is_promoted() {
|
||||
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
} else {
|
||||
remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::Event;
|
||||
use crate::chatlist::get_archived_cnt;
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::ephemeral::Timer;
|
||||
@@ -1241,96 +1240,6 @@ async fn test_unarchive_if_muted() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_marknoticed_all_chats() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.section("alice: create chats & promote them by sending a message");
|
||||
|
||||
let alice_chat_normal = alice
|
||||
.create_group_with_members("Chat (normal)", &[alice, bob])
|
||||
.await;
|
||||
send_text_msg(alice, alice_chat_normal, "Hi".to_string()).await?;
|
||||
|
||||
let alice_chat_muted = alice
|
||||
.create_group_with_members("Chat (muted)", &[alice, bob])
|
||||
.await;
|
||||
send_text_msg(alice, alice_chat_muted, "Hi".to_string()).await?;
|
||||
set_muted(&alice.ctx, alice_chat_muted, MuteDuration::Forever).await?;
|
||||
|
||||
let alice_chat_archived_and_muted = alice
|
||||
.create_group_with_members("Chat (archived and muted)", &[alice, bob])
|
||||
.await;
|
||||
send_text_msg(alice, alice_chat_archived_and_muted, "Hi".to_string()).await?;
|
||||
set_muted(
|
||||
&alice.ctx,
|
||||
alice_chat_archived_and_muted,
|
||||
MuteDuration::Forever,
|
||||
)
|
||||
.await?;
|
||||
alice_chat_archived_and_muted
|
||||
.set_visibility(&alice.ctx, ChatVisibility::Archived)
|
||||
.await?;
|
||||
|
||||
tcm.section("bob: receive messages, accept all chats and send a reply to each messsage");
|
||||
|
||||
while let Some(sent_msg) = alice.pop_sent_msg_opt(Duration::default()).await {
|
||||
let bob_message = bob.recv_msg(&sent_msg).await;
|
||||
let bob_chat_id = bob_message.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
send_text_msg(bob, bob_chat_id, "reply".to_string()).await?;
|
||||
}
|
||||
|
||||
tcm.section("alice: receive replies from bob");
|
||||
while let Some(sent_msg) = bob.pop_sent_msg_opt(Duration::default()).await {
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
}
|
||||
// ensure chats have unread messages
|
||||
assert_eq!(alice_chat_normal.get_fresh_msg_cnt(alice).await?, 1);
|
||||
assert_eq!(alice_chat_muted.get_fresh_msg_cnt(alice).await?, 1);
|
||||
assert_eq!(
|
||||
alice_chat_archived_and_muted
|
||||
.get_fresh_msg_cnt(alice)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
|
||||
tcm.section("alice: mark as read");
|
||||
alice.evtracker.clear_events();
|
||||
marknoticed_all_chats(alice).await?;
|
||||
tcm.section("alice: check that chats are no longer unread and that chatlist update events were received");
|
||||
assert_eq!(alice_chat_normal.get_fresh_msg_cnt(alice).await?, 0);
|
||||
assert_eq!(alice_chat_muted.get_fresh_msg_cnt(alice).await?, 0);
|
||||
assert_eq!(
|
||||
alice_chat_archived_and_muted
|
||||
.get_fresh_msg_cnt(alice)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
let emitted_events = alice.evtracker.take_events();
|
||||
for event in &[
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(alice_chat_normal),
|
||||
},
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(alice_chat_muted),
|
||||
},
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(alice_chat_archived_and_muted),
|
||||
},
|
||||
EventType::ChatlistItemChanged {
|
||||
chat_id: Some(DC_CHAT_ID_ARCHIVED_LINK),
|
||||
},
|
||||
] {
|
||||
assert!(emitted_events.iter().any(|Event { typ, .. }| typ == event));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archive_fresh_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -3435,11 +3344,6 @@ async fn test_remove_member_from_broadcast() -> Result<()> {
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
// Alice must not remember old members,
|
||||
// because we would like to remember the minimum information possible
|
||||
let past_contacts = get_past_chat_contacts(alice, alice_chat_id).await?;
|
||||
assert_eq!(past_contacts.len(), 0);
|
||||
|
||||
let remove_msg = alice.pop_sent_msg().await;
|
||||
let rcvd = bob.recv_msg(&remove_msg).await;
|
||||
assert_eq!(rcvd.text, "Member Me removed by alice@example.org.");
|
||||
|
||||
+3
-39
@@ -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, contact requests and incoming broadcasts.
|
||||
/// and hides the device-chat and contact requests
|
||||
/// 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,9 +224,8 @@ 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::InBroadcast
|
||||
|| (typ == Chattype::Mailinglist
|
||||
&& param.get(Param::ListPost).is_none_or_empty())
|
||||
if typ == Chattype::Mailinglist
|
||||
&& param.get(Param::ListPost).is_none_or_empty()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
@@ -598,41 +597,6 @@ 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;
|
||||
|
||||
+15
-13
@@ -1120,19 +1120,21 @@ impl Context {
|
||||
let list = self
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"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",
|
||||
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;"
|
||||
),
|
||||
(MessageState::InFresh, time()),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
|
||||
+14
-13
@@ -23,9 +23,7 @@ use num_traits::FromPrimitive;
|
||||
use ratelimit::Ratelimit;
|
||||
use url::Url;
|
||||
|
||||
use crate::calls::{
|
||||
UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
|
||||
};
|
||||
use crate::calls::{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;
|
||||
@@ -136,15 +134,16 @@ pub(crate) struct ServerMetadata {
|
||||
|
||||
pub iroh_relay: Option<Url>,
|
||||
|
||||
/// ICE servers for WebRTC calls.
|
||||
pub ice_servers: Vec<UnresolvedIceServer>,
|
||||
/// 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,
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -1140,6 +1139,7 @@ impl Session {
|
||||
/// Stores pending `\Seen` flags for messages in `imap_markseen` table.
|
||||
pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
|
||||
if context.get_config_bool(Config::TeamProfile).await? {
|
||||
info!(context, "Team profile, skipping seen flag synchronization.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1215,6 +1215,7 @@ impl Session {
|
||||
}
|
||||
|
||||
if context.get_config_bool(Config::TeamProfile).await? {
|
||||
info!(context, "Team profile, skipping seen flag synchronization.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1551,7 +1552,7 @@ impl Session {
|
||||
if m.entry == "/shared/vendor/deltachat/turn"
|
||||
&& let Some(value) = m.value
|
||||
{
|
||||
match create_ice_servers_from_metadata(&value).await {
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
old_metadata.ice_servers = parsed_ice_servers;
|
||||
@@ -1568,7 +1569,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();
|
||||
old_metadata.ice_servers = create_fallback_ice_servers(context).await?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1615,7 +1616,7 @@ impl Session {
|
||||
}
|
||||
"/shared/vendor/deltachat/turn" => {
|
||||
if let Some(value) = m.value {
|
||||
match create_ice_servers_from_metadata(&value).await {
|
||||
match create_ice_servers_from_metadata(context, &value).await {
|
||||
Ok((parsed_timestamp, parsed_ice_servers)) => {
|
||||
ice_servers_expiration_timestamp = parsed_timestamp;
|
||||
ice_servers = Some(parsed_ice_servers);
|
||||
@@ -1634,7 +1635,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()
|
||||
create_fallback_ice_servers(context).await?
|
||||
};
|
||||
|
||||
*lock = Some(ServerMetadata {
|
||||
|
||||
+47
-71
@@ -87,10 +87,12 @@ impl MsgId {
|
||||
let result = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT m.state, mdns.msg_id
|
||||
FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
|
||||
WHERE id=?
|
||||
LIMIT 1",
|
||||
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",
|
||||
),
|
||||
(self,),
|
||||
|row| {
|
||||
let state: MessageState = row.get(0)?;
|
||||
@@ -452,7 +454,6 @@ pub struct Message {
|
||||
pub(crate) is_dc_message: MessengerMessage,
|
||||
pub(crate) original_msg_id: MsgId,
|
||||
pub(crate) mime_modified: bool,
|
||||
pub(crate) chat_visibility: ChatVisibility,
|
||||
pub(crate) chat_blocked: Blocked,
|
||||
pub(crate) location_id: u32,
|
||||
pub(crate) error: Option<String>,
|
||||
@@ -500,39 +501,40 @@ impl Message {
|
||||
let mut msg = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"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.archived AS visibility,
|
||||
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",
|
||||
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",
|
||||
),
|
||||
(id,),
|
||||
|row| {
|
||||
let state: MessageState = row.get("state")?;
|
||||
@@ -585,7 +587,6 @@ impl Message {
|
||||
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
|
||||
hidden: row.get("hidden")?,
|
||||
location_id: row.get("location")?,
|
||||
chat_visibility: row.get::<_, Option<_>>("visibility")?.unwrap_or_default(),
|
||||
chat_blocked: row
|
||||
.get::<_, Option<Blocked>>("blocked")?
|
||||
.unwrap_or_default(),
|
||||
@@ -1506,16 +1507,6 @@ pub async fn get_msg_read_receipts(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns count of read receipts on message.
|
||||
///
|
||||
/// This view count is meant as a feedback measure for the channel owner only.
|
||||
pub async fn get_msg_read_receipt_count(context: &Context, msg_id: MsgId) -> Result<usize> {
|
||||
context
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?", (msg_id,))
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) fn guess_msgtype_from_suffix(msg: &Message) -> Option<(Viewtype, &'static str)> {
|
||||
msg.param
|
||||
.get(Param::Filename)
|
||||
@@ -1850,7 +1841,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
m.param AS param,
|
||||
m.from_id AS from_id,
|
||||
m.rfc724_mid AS rfc724_mid,
|
||||
m.hidden AS hidden,
|
||||
c.archived AS archived,
|
||||
c.blocked AS blocked
|
||||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
|
||||
@@ -1862,7 +1852,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
|
||||
let from_id: ContactId = row.get("from_id")?;
|
||||
let rfc724_mid: String = row.get("rfc724_mid")?;
|
||||
let hidden: bool = row.get("hidden")?;
|
||||
let visibility: ChatVisibility = row.get("archived")?;
|
||||
let blocked: Option<Blocked> = row.get("blocked")?;
|
||||
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
|
||||
@@ -1874,7 +1863,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
param,
|
||||
from_id,
|
||||
rfc724_mid,
|
||||
hidden,
|
||||
visibility,
|
||||
blocked.unwrap_or_default(),
|
||||
),
|
||||
@@ -1907,7 +1895,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
curr_param,
|
||||
curr_from_id,
|
||||
curr_rfc724_mid,
|
||||
curr_hidden,
|
||||
curr_visibility,
|
||||
curr_blocked,
|
||||
),
|
||||
@@ -1920,9 +1907,8 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
|
||||
markseen_on_imap_table(context, &curr_rfc724_mid).await?;
|
||||
|
||||
// Read receipts for system messages are never sent to contacts.
|
||||
// These messages have no place to display received read receipt
|
||||
// anyway. And since their text is locally generated,
|
||||
// Read receipts for system messages are never sent. These messages have no place to
|
||||
// display received read receipt anyway. And since their text is locally generated,
|
||||
// quoting them is dangerous as it may contain contact names. E.g., for original message
|
||||
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can
|
||||
// be a display name stored in address book rather than the name sent in the From field by
|
||||
@@ -1930,35 +1916,25 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
//
|
||||
// We also don't send read receipts for contact requests.
|
||||
// Read receipts will not be sent even after accepting the chat.
|
||||
let to_id = if curr_blocked == Blocked::Not
|
||||
if curr_blocked == Blocked::Not
|
||||
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
|
||||
&& curr_param.get_cmd() == SystemMessage::Unknown
|
||||
&& context.should_send_mdns().await?
|
||||
{
|
||||
Some(curr_from_id)
|
||||
} else if context.get_config_bool(Config::BccSelf).await? {
|
||||
Some(ContactId::SELF)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(to_id) = to_id {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
|
||||
(id, to_id, curr_rfc724_mid),
|
||||
(id, curr_from_id, curr_rfc724_mid),
|
||||
)
|
||||
.await
|
||||
.context("failed to insert into smtp_mdns")?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
if !curr_hidden {
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
}
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
}
|
||||
archived_chats_maybe_noticed |= curr_state == MessageState::InFresh
|
||||
&& !curr_hidden
|
||||
&& curr_visibility == ChatVisibility::Archived;
|
||||
archived_chats_maybe_noticed |=
|
||||
curr_state == MessageState::InFresh && curr_visibility == ChatVisibility::Archived;
|
||||
}
|
||||
|
||||
for updated_chat_id in updated_chat_ids {
|
||||
|
||||
+11
-45
@@ -106,7 +106,6 @@ pub struct MimeFactory {
|
||||
/// addresses and OpenPGP keys
|
||||
/// to use for encryption.
|
||||
///
|
||||
/// If `Some`, encrypt to self also.
|
||||
/// `None` if the message is not encrypted.
|
||||
encryption_pubkeys: Option<Vec<(String, SignedPublicKey)>>,
|
||||
|
||||
@@ -235,6 +234,7 @@ impl MimeFactory {
|
||||
encryption_pubkeys = if msg.param.get_bool(Param::ForcePlaintext).unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
// Encrypt, but only to self.
|
||||
Some(Vec::new())
|
||||
};
|
||||
} else if chat.is_mailing_list() {
|
||||
@@ -247,37 +247,6 @@ impl MimeFactory {
|
||||
|
||||
// Do not encrypt messages to mailing lists.
|
||||
encryption_pubkeys = None;
|
||||
} else if let Some(fp) = must_have_only_one_recipient(&msg, &chat) {
|
||||
let fp = fp?;
|
||||
// In a broadcast channel, only send member-added/removed messages
|
||||
// to the affected member
|
||||
let (authname, addr) = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT authname, addr FROM contacts WHERE fingerprint=?",
|
||||
(fp,),
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
Ok((authname, addr))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let public_key_bytes: Vec<_> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT public_key FROM public_keys WHERE fingerprint=?",
|
||||
(fp,),
|
||||
)
|
||||
.await?
|
||||
.context("Can't send member addition/removal: missing key")?;
|
||||
|
||||
recipients.push(addr.clone());
|
||||
to.push((authname, addr.clone()));
|
||||
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
|
||||
encryption_pubkeys = Some(vec![(addr, public_key)]);
|
||||
} else {
|
||||
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
msg.param.get(Param::Arg)
|
||||
@@ -337,6 +306,13 @@ impl MimeFactory {
|
||||
for row in rows {
|
||||
let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt) = row?;
|
||||
|
||||
// In a broadcast channel, only send member-added/removed messages
|
||||
// to the affected member:
|
||||
if let Some(fp) = must_have_only_one_recipient(&msg, &chat)
|
||||
&& fp? != fingerprint {
|
||||
continue;
|
||||
}
|
||||
|
||||
let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt {
|
||||
Some(SignedPublicKey::from_slice(public_key_bytes)?)
|
||||
} else {
|
||||
@@ -450,16 +426,8 @@ impl MimeFactory {
|
||||
},
|
||||
)
|
||||
.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?;
|
||||
}
|
||||
let recipient_ids: Vec<_> = recipient_ids.into_iter().collect();
|
||||
ContactId::scaleup_origin(context, &recipient_ids, Origin::OutgoingTo).await?;
|
||||
|
||||
if !msg.is_system_message()
|
||||
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
|
||||
@@ -563,9 +531,7 @@ impl MimeFactory {
|
||||
let timestamp = create_smeared_timestamp(context);
|
||||
|
||||
let addr = contact.get_addr().to_string();
|
||||
let encryption_pubkeys = if from_id == ContactId::SELF {
|
||||
Some(Vec::new())
|
||||
} else if contact.is_key_contact() {
|
||||
let encryption_pubkeys = if contact.is_key_contact() {
|
||||
if let Some(key) = contact.public_key(context).await? {
|
||||
Some(vec![(addr.clone(), key)])
|
||||
} else {
|
||||
|
||||
+21
-13
@@ -2159,9 +2159,9 @@ pub(crate) struct Report {
|
||||
///
|
||||
/// It MUST be present if the original message has a Message-ID according to RFC 8098.
|
||||
/// In case we can't find it (shouldn't happen), this is None.
|
||||
pub original_message_id: Option<String>,
|
||||
original_message_id: Option<String>,
|
||||
/// Additional-Message-IDs
|
||||
pub additional_message_ids: Vec<String>,
|
||||
additional_message_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Delivery Status Notification (RFC 3464, RFC 6533)
|
||||
@@ -2468,23 +2468,31 @@ async fn handle_mdn(
|
||||
timestamp_sent: i64,
|
||||
) -> Result<()> {
|
||||
if from_id == ContactId::SELF {
|
||||
// MDNs to self are handled in receive_imf_inner().
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring MDN sent to self, this is a bug on the sender device."
|
||||
);
|
||||
|
||||
// This is not an error on our side,
|
||||
// we successfully ignored an invalid MDN and return `Ok`.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some((msg_id, chat_id, has_mdns, is_dup)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"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",
|
||||
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",
|
||||
),
|
||||
(&rfc724_mid, from_id),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get("msg_id")?;
|
||||
|
||||
+7
-9
@@ -17,9 +17,7 @@ use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
|
||||
use pgp::types::{
|
||||
CompressionAlgorithm, KeyDetails, Password, PublicKeyTrait, SecretKeyTrait as _, StringToKey,
|
||||
};
|
||||
use pgp::types::{CompressionAlgorithm, KeyDetails, Password, PublicKeyTrait, StringToKey};
|
||||
use rand_old::{Rng as _, thread_rng};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
@@ -33,6 +31,9 @@ pub(crate) const HEADER_SETUPCODE: &str = "passphrase-begin";
|
||||
/// Preferred symmetric encryption algorithm.
|
||||
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
|
||||
|
||||
/// Preferred cryptographic hash.
|
||||
const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::Sha256;
|
||||
|
||||
/// Split data from PGP Armored Data as defined in <https://tools.ietf.org/html/rfc4880#section-6.2>.
|
||||
///
|
||||
/// Returns (type, headers, base64 encoded body).
|
||||
@@ -204,8 +205,7 @@ pub async fn pk_encrypt(
|
||||
}
|
||||
}
|
||||
|
||||
let hash_algorithm = private_key_for_signing.hash_alg();
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), hash_algorithm);
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
@@ -228,8 +228,7 @@ pub async fn pk_encrypt(
|
||||
}
|
||||
}
|
||||
|
||||
let hash_algorithm = private_key_for_signing.hash_alg();
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), hash_algorithm);
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
@@ -454,8 +453,7 @@ pub async fn symm_encrypt_message(
|
||||
);
|
||||
msg.encrypt_with_password(&mut rng, s2k, &shared_secret)?;
|
||||
|
||||
let hash_algorithm = private_key_for_signing.hash_alg();
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), hash_algorithm);
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
|
||||
@@ -59,12 +59,14 @@ pub enum LoginOptions {
|
||||
/// scheme: `dclogin://user@host/?p=password&v=1[&options]`
|
||||
/// read more about the scheme at <https://github.com/deltachat/interface/blob/master/uri-schemes.md#DCLOGIN>
|
||||
pub(super) fn decode_login(qr: &str) -> Result<Qr> {
|
||||
let qr = qr.replacen("://", ":", 1);
|
||||
let url = url::Url::parse(qr).with_context(|| format!("Malformed url: {qr:?}"))?;
|
||||
|
||||
let url = url::Url::parse(&qr).with_context(|| format!("Malformed url: {qr:?}"))?;
|
||||
let payload = qr
|
||||
let url_without_scheme = qr
|
||||
.get(DCLOGIN_SCHEME.len()..)
|
||||
.context("invalid DCLOGIN payload E1")?;
|
||||
let payload = url_without_scheme
|
||||
.strip_prefix("//")
|
||||
.unwrap_or(url_without_scheme);
|
||||
|
||||
let addr = payload
|
||||
.split(['?', '/'])
|
||||
@@ -363,32 +365,4 @@ mod test {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_dclogin_ipv4() -> anyhow::Result<()> {
|
||||
let result = decode_login("dclogin://test@[127.0.0.1]?p=1234&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "test@[127.0.0.1]".to_owned());
|
||||
assert_eq!(options, login_options_just_pw!("1234".to_owned()));
|
||||
} else {
|
||||
unreachable!("wrong type");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_dclogin_ipv6() -> anyhow::Result<()> {
|
||||
let result =
|
||||
decode_login("dclogin://test@[2001:0db8:85a3:0000:0000:8a2e:0370:7334]?p=1234&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(
|
||||
address,
|
||||
"test@[2001:0db8:85a3:0000:0000:8a2e:0370:7334]".to_owned()
|
||||
);
|
||||
assert_eq!(options, login_options_just_pw!("1234".to_owned()));
|
||||
} else {
|
||||
unreachable!("wrong type");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+2
-12
@@ -747,23 +747,13 @@ Content-Disposition: reaction\n\
|
||||
alice_reaction_msg.id.get_state(&alice).await?,
|
||||
MessageState::InSeen
|
||||
);
|
||||
// Reactions don't request MDNs, but an MDN to self is sent.
|
||||
// Reactions don't request MDNs.
|
||||
assert_eq!(
|
||||
alice
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
alice
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
|
||||
(ContactId::SELF,)
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
0
|
||||
);
|
||||
|
||||
// Alice reacts to own message.
|
||||
|
||||
+11
-75
@@ -1,6 +1,5 @@
|
||||
//! Internet Message Format reception pipeline.
|
||||
|
||||
use std::cmp;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::iter;
|
||||
use std::sync::LazyLock;
|
||||
@@ -15,7 +14,7 @@ use mailparse::SingleInfo;
|
||||
use num_traits::FromPrimitive;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ChatVisibility, save_broadcast_secret};
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, save_broadcast_secret};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
|
||||
use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
|
||||
@@ -961,74 +960,6 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
||||
// This is a Delta Chat MDN. Mark as read.
|
||||
markseen_on_imap_table(context, rfc724_mid_orig).await?;
|
||||
}
|
||||
if !mime_parser.incoming && !context.get_config_bool(Config::TeamProfile).await? {
|
||||
let mut updated_chats = BTreeMap::new();
|
||||
let mut archived_chats_maybe_noticed = false;
|
||||
for report in &mime_parser.mdn_reports {
|
||||
for msg_rfc724_mid in report
|
||||
.original_message_id
|
||||
.iter()
|
||||
.chain(&report.additional_message_ids)
|
||||
{
|
||||
let Some(msg_id) = rfc724_mid_exists(context, msg_rfc724_mid).await? else {
|
||||
continue;
|
||||
};
|
||||
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
|
||||
continue;
|
||||
};
|
||||
if msg.state < MessageState::InFresh || msg.state >= MessageState::InSeen {
|
||||
continue;
|
||||
}
|
||||
if !mime_parser.was_encrypted() && msg.get_showpadlock() {
|
||||
warn!(context, "MDN: Not encrypted. Ignoring.");
|
||||
continue;
|
||||
}
|
||||
message::update_msg_state(context, msg_id, MessageState::InSeen).await?;
|
||||
if let Err(e) = msg_id.start_ephemeral_timer(context).await {
|
||||
error!(context, "start_ephemeral_timer for {msg_id}: {e:#}.");
|
||||
}
|
||||
if !mime_parser.has_chat_version() {
|
||||
continue;
|
||||
}
|
||||
archived_chats_maybe_noticed |= msg.state < MessageState::InNoticed
|
||||
&& msg.chat_visibility == ChatVisibility::Archived;
|
||||
updated_chats
|
||||
.entry(msg.chat_id)
|
||||
.and_modify(|ts| *ts = cmp::max(*ts, msg.timestamp_sort))
|
||||
.or_insert(msg.timestamp_sort);
|
||||
}
|
||||
}
|
||||
for (chat_id, timestamp_sort) in updated_chats {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"
|
||||
UPDATE msgs SET state=? WHERE
|
||||
state=? AND
|
||||
hidden=0 AND
|
||||
chat_id=? AND
|
||||
timestamp<?",
|
||||
(
|
||||
MessageState::InNoticed,
|
||||
MessageState::InFresh,
|
||||
chat_id,
|
||||
timestamp_sort,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.context("UPDATE msgs.state")?;
|
||||
// Removes all notifications for the chat in UIs. Ideally should be under
|
||||
// `if chat_id.get_fresh_msg_cnt(context).await? == 0`, but this causes a not
|
||||
// updated profile badge counter in the UIs as of v2.35.0. Removed notifications for
|
||||
// new messages is an already existing and known problem, so let's emit the event
|
||||
// unconditionally for now.
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
}
|
||||
if archived_chats_maybe_noticed {
|
||||
context.on_archived_chats_maybe_noticed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mime_parser.is_call() {
|
||||
@@ -1765,6 +1696,8 @@ async fn add_parts(
|
||||
}
|
||||
|
||||
let is_location_kml = mime_parser.location_kml.is_some();
|
||||
let is_mdn = !mime_parser.mdn_reports.is_empty();
|
||||
|
||||
let mut group_changes = match chat.typ {
|
||||
_ if chat.id.is_special() => GroupChangesInfo::default(),
|
||||
Chattype::Single => GroupChangesInfo::default(),
|
||||
@@ -1800,10 +1733,7 @@ async fn add_parts(
|
||||
|
||||
let state = if !mime_parser.incoming {
|
||||
MessageState::OutDelivered
|
||||
} else if seen
|
||||
|| !mime_parser.mdn_reports.is_empty()
|
||||
|| chat_id_blocked == Blocked::Yes
|
||||
|| group_changes.silent
|
||||
} else if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
|
||||
// No check for `hidden` because only reactions are such and they should be `InFresh`.
|
||||
{
|
||||
MessageState::InSeen
|
||||
@@ -2321,13 +2251,19 @@ RETURNING id
|
||||
}
|
||||
}
|
||||
|
||||
// Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all
|
||||
// outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN,
|
||||
// delete it.
|
||||
let needs_delete_job =
|
||||
!mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes;
|
||||
|
||||
Ok(ReceivedMsg {
|
||||
chat_id,
|
||||
state,
|
||||
hidden,
|
||||
sort_timestamp,
|
||||
msg_ids: created_db_entries,
|
||||
needs_delete_job: false,
|
||||
needs_delete_job,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3878,29 +3878,6 @@ 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(())
|
||||
}
|
||||
|
||||
|
||||
+6
-55
@@ -13,7 +13,7 @@ use crate::config::Config;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
@@ -184,9 +184,6 @@ pub(crate) async fn smtp_send(
|
||||
smtp: &mut Smtp,
|
||||
msg_id: Option<MsgId>,
|
||||
) -> SendResult {
|
||||
if recipients.is_empty() {
|
||||
return SendResult::Success;
|
||||
}
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(context, "SMTP-sending out mime message:\n{message}");
|
||||
}
|
||||
@@ -573,32 +570,17 @@ async fn send_mdn_rfc724_mid(
|
||||
additional_rfc724_mids.clone(),
|
||||
)
|
||||
.await?;
|
||||
let encrypted = mimefactory.will_be_encrypted();
|
||||
let rendered_msg = mimefactory.render(context).await?;
|
||||
let body = rendered_msg.message;
|
||||
|
||||
let mut recipients = Vec::new();
|
||||
if contact_id != ContactId::SELF {
|
||||
recipients.push(contact.get_addr().to_string());
|
||||
}
|
||||
if context.get_config_bool(Config::BccSelf).await? {
|
||||
add_self_recipients(context, &mut recipients, encrypted).await?;
|
||||
}
|
||||
let recipients: Vec<_> = recipients
|
||||
.into_iter()
|
||||
.filter_map(|addr| {
|
||||
async_smtp::EmailAddress::new(addr.clone())
|
||||
.with_context(|| format!("Invalid recipient: {addr}"))
|
||||
.log_err(context)
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
let addr = contact.get_addr();
|
||||
let recipient = async_smtp::EmailAddress::new(addr.to_string())
|
||||
.map_err(|err| format_err!("invalid recipient: {addr} {err:?}"))?;
|
||||
let recipients = vec![recipient];
|
||||
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
}
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
@@ -685,34 +667,3 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds self-addresses to `recipients` as necessary.
|
||||
/// This doesn't check `Config::BccSelf`, it should be checked by the caller if needed.
|
||||
pub(crate) async fn add_self_recipients(
|
||||
context: &Context,
|
||||
recipients: &mut Vec<String>,
|
||||
encrypted: bool,
|
||||
) -> Result<()> {
|
||||
// Previous versions of Delta Chat did not send BCC self
|
||||
// if DeleteServerAfter was set to immediately delete messages
|
||||
// from the server. This is not the case anymore
|
||||
// because BCC-self messages are also used to detect
|
||||
// that message was sent if SMTP server is slow to respond
|
||||
// and connection is frequently lost
|
||||
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
|
||||
// disabled by default is fine.
|
||||
if context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty() {
|
||||
// Avoid sending unencrypted messages to all transports, chatmail relays won't accept
|
||||
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
|
||||
// messages.
|
||||
if encrypted {
|
||||
for addr in context.get_secondary_self_addrs().await? {
|
||||
recipients.push(addr);
|
||||
}
|
||||
}
|
||||
// `from` must be the last addr, see `receive_imf_inner()` why.
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
recipients.push(from);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+2
-5
@@ -9,7 +9,6 @@ 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;
|
||||
|
||||
@@ -22,7 +21,7 @@ use crate::key::load_self_public_keyring;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::securejoin::QrInvite;
|
||||
use crate::tools::time;
|
||||
use crate::tools::{create_id, time};
|
||||
|
||||
pub(crate) const STATISTICS_BOT_EMAIL: &str = "self_reporting@testrun.org";
|
||||
const STATISTICS_BOT_VCARD: &str = include_str!("../assets/statistics-bot.vcf");
|
||||
@@ -391,9 +390,7 @@ pub(crate) async fn stats_id(context: &Context) -> Result<String> {
|
||||
Ok(match context.get_config(Config::StatsId).await? {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let id = rand::distr::Alphabetic
|
||||
.sample_string(&mut rand::rng(), 25)
|
||||
.to_lowercase();
|
||||
let id = create_id();
|
||||
context
|
||||
.set_config_internal(Config::StatsId, Some(&id))
|
||||
.await?;
|
||||
|
||||
@@ -1492,15 +1492,6 @@ impl EventTracker {
|
||||
pub fn clear_events(&self) {
|
||||
while let Ok(_ev) = self.try_recv() {}
|
||||
}
|
||||
|
||||
/// Takes all items from event queue and returns them.
|
||||
pub fn take_events(&self) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
while let Ok(event) = self.try_recv() {
|
||||
events.push(event);
|
||||
}
|
||||
events
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a specific message from a chat and asserts that the chat has a specific length.
|
||||
|
||||
Reference in New Issue
Block a user