Compare commits

..

2 Commits

Author SHA1 Message Date
Hocuri
7ab2994284 Only lower the opt-level for tests 2022-10-27 21:46:01 +02:00
Hocuri
9136f3cda7 Don't use opt_level=1, instead box futures 2022-10-15 09:41:52 +02:00
392 changed files with 1683 additions and 7642 deletions

View File

@@ -1,11 +0,0 @@
[env]
# In unoptimised builds tokio tends to use a lot of stack space when
# creating some complicated futures, tokio has an open issue for this:
# https://github.com/tokio-rs/tokio/issues/2055. Some of our tests
# manage to not fit in the default 2MiB stack anymore due to this, so
# while the issue is not resolved we want to work around this.
# Because compiling optimised builds takes a very long time we prefer
# to avoid that. Setting this environment variable ensures that when
# invoking `cargo test` threads are allowed to have a large enough
# stack size without needing to use an optimised build.
RUST_MIN_STACK = "8388608"

View File

@@ -83,13 +83,13 @@ jobs:
rust: 1.61.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.57.0
# Minimum Supported Rust Version = 1.56.1
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.57.0
rust: 1.56.1
python: 3.7
runs-on: ${{ matrix.os }}
steps:

View File

@@ -8,7 +8,6 @@ on:
env:
CARGO_TERM_COLOR: always
RUST_MIN_STACK: "8388608"
jobs:
build_and_test:

View File

@@ -52,7 +52,6 @@ jobs:
npm install --verbose
- name: Test
timeout-minutes: 10
if: runner.os != 'Windows'
run: |
cd node
@@ -60,7 +59,6 @@ jobs:
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
- name: Run tests on Windows, except lint
timeout-minutes: 10
if: runner.os == 'Windows'
run: |
cd node

View File

@@ -2,105 +2,6 @@
## Unreleased
### Changes
### API-Changes
### Fixes
- fix detection of "All mail", "Trash", "Junk" etc folders. #3760
## 1.101.0
### Changes
- add `configured_inbox_folder` to account info #3748
- `dc_delete_contact()` hides contacts if referenced #3751
- add IMAP UIDs to message info #3755
### Fixes
- improve IMAP logging, in particular fix incorrect "IMAP IDLE protocol
timed out" message on network error during IDLE #3749
- pop Recently Seen Loop event out of the queue when it is in the past
to avoid busy looping #3753
- fix build failures by going back to standard `async_zip` #3747
## 1.100.0
### API-Changes
- jsonrpc: add `miscSaveSticker` method
### Changes
- add JSON-RPC stdio server `deltachat-rpc-server` and use it for JSON-RPC tests #3695
- update rPGP from 0.8 to 0.9 #3737
- jsonrpc: typescript client: use npm released deltachat fork of the tiny emitter package #3741
- jsonrpc: show sticker image in quote #3744
## 1.99.0
### API-Changes
- breaking jsonrpc: changed function naming
- `autocryptInitiateKeyTransfer` -> `initiateAutocryptKeyTransfer`
- `autocryptContinueKeyTransfer` -> `continueAutocryptKeyTransfer`
- `chatlistGetFullChatById` -> `getFullChatById`
- `messageGetMessage` -> `getMessage`
- `messageGetMessages` -> `getMessages`
- `messageGetNotificationInfo` -> `getMessageNotificationInfo`
- `contactsGetContact` -> `getContact`
- `contactsCreateContact` -> `createContact`
- `contactsCreateChatByContactId` -> `createChatByContactId`
- `contactsBlock` -> `blockContact`
- `contactsUnblock` -> `unblockContact`
- `contactsGetBlocked` -> `getBlockedContacts`
- `contactsGetContactIds` -> `getContactIds`
- `contactsGetContacts` -> `getContacts`
- `contactsGetContactsByIds` -> `getContactsByIds`
- `chatGetMedia` -> `getChatMedia`
- `chatGetNeighboringMedia` -> `getNeighboringChatMedia`
- `webxdcSendStatusUpdate` -> `sendWebxdcStatusUpdate`
- `webxdcGetStatusUpdates` -> `getWebxdcStatusUpdates`
- `messageGetWebxdcInfo` -> `getWebxdcInfo`
- jsonrpc: changed method signature
- `miscSendTextMessage(accountId, text, chatId)` -> `miscSendTextMessage(accountId, chatId, text)`
- jsonrpc: add `SystemMessageType` to `Message`
- cffi: add missing `DC_INFO_` constants
- Add DC_EVENT_INCOMING_MSG_BUNCH event #3643
- Python bindings: Make get_matching() only match the
whole event name, e.g. events.get_matching("DC_EVENT_INCOMING_MSG")
won't match DC_EVENT_INCOMING_MSG_BUNCH anymore #3643
- Rust: Introduce a ContextBuilder #3698
### Changes
- allow sender timestamp to be in the future, but not too much
- Disable the new "Authentication-Results/DKIM checking" security feature
until we have tested it a bit #3728
- refactorings #3706
### Fixes
- `dc_search_msgs()` returns unaccepted requests #3694
- emit "contacts changed" event when the contact is no longer "seen recently" #3703
- do not allow peerstate reset if DKIM check failed #3731
## 1.98.0
### API-Changes
- jsonrpc: typescript client: export constants under `C` enum, similar to how its exported from `deltachat-node` #3681
- added reactions support #3644
- jsonrpc: reactions: added reactions to `Message` type and the `sendReaction()` method #3686
### Changes
- simplify `UPSERT` queries #3676
### Fixes
## 1.97.0
### API-Changes
- jsonrpc: add function: #3641, #3645, #3653
- `getChatContacts()`
@@ -132,8 +33,6 @@
- `stopIo()`
- `exportBackup()`
- `importBackup()`
- `getMessageHtml()` #3671
- `miscGetStickerFolder` and `miscGetStickers` #3672
- breaking: jsonrpc: remove function `messageListGetMessageIds()`, it is replaced by `getMessageIds()` and `getMessageListItems()` the latter returns a new `MessageListItem` type, which is the now prefered way of using the message list.
- jsonrpc: add type: #3641, #3645
- `MessageSearchResult`
@@ -142,12 +41,6 @@
### Changes
- Look at Authentication-Results. Don't accept Autocrypt key changes
if they come with negative authentiation results while this contact
sent emails with positive authentication results in the past. #3583
- jsonrpc in cffi also sends events now #3662
- jsonrpc: new format for events and better typescript autocompletion
- Join all "[migration] vXX" log messages into one
### Fixes
- share stock string translations across accounts created by the same account manager #3640

564
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
[package]
name = "deltachat"
version = "1.101.0"
version = "1.96.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
rust-version = "1.57"
rust-version = "1.56"
[profile.dev]
debug = 0
@@ -26,7 +26,7 @@ anyhow = "1"
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
trust-dns-resolver = "0.22"
trust-dns-resolver = "0.21"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
backtrace = "0.3"
@@ -49,9 +49,9 @@ native-tls = "0.2"
num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.16.0"
percent-encoding = "2.2"
pgp = { version = "0.9", default-features = false }
once_cell = "1.15.0"
percent-encoding = "2.0"
pgp = { version = "0.8", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.23"
r2d2 = "0.8"
@@ -77,12 +77,12 @@ fast-socks5 = "0.8"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "4.3.3"
textwrap = "0.16.0"
textwrap = "0.15.1"
async-channel = "1.6.1"
futures-lite = "1.12.0"
tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-stream = { version = "0.1.10", features = ["fs"] }
reqwest = { version = "0.11.12", features = ["json"] }
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
async_zip = { git = "https://github.com/dignifiedquire/rs-async-zip", branch = "main", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"
@@ -98,8 +98,7 @@ tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"]
members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc",
"deltachat-rpc-server"
"deltachat-jsonrpc"
]
[[example]]

View File

@@ -125,12 +125,10 @@ $ cargo test -- --ignored
Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- **Node.js**
- over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
- **Node.js** \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go**[^1] \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
- **Java** and **Swift** (contained in the Android/iOS repos)
The following "frontend" projects make use of the Rust-library
@@ -142,5 +140,3 @@ or its language bindings:
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
- several **Bots**
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.101.0"
version = "1.96.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -25,7 +25,7 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.7"
once_cell = "1.16.0"
once_cell = "1.15.0"
[features]
default = ["vendored"]

View File

@@ -11,17 +11,16 @@ extern "C" {
#endif
typedef struct _dc_context dc_context_t;
typedef struct _dc_accounts dc_accounts_t;
typedef struct _dc_array dc_array_t;
typedef struct _dc_chatlist dc_chatlist_t;
typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_reactions dc_reactions_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_context dc_context_t;
typedef struct _dc_accounts dc_accounts_t;
typedef struct _dc_array dc_array_t;
typedef struct _dc_chatlist dc_chatlist_t;
typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
@@ -992,34 +991,6 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id ID of the message you react to.
* @param reaction A string consisting of emojis separated by spaces.
* @return The ID of the message sent out or 0 for errors.
*/
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
/**
* Get a structure with reactions to the message.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The message ID to get reactions for.
* @return A structure with all reactions to the message.
*/
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
/**
* A webxdc instance sends a status update to its other members.
*
@@ -2056,9 +2027,8 @@ char* dc_get_contact_encrinfo (dc_context_t* context, uint32_t co
/**
* Delete a contact so that it disappears from the corresponding lists.
* Depending on whether there are ongoing chats, deletion is done by physical deletion or hiding.
* The contact is deleted from the local device.
* Delete a contact. The contact is deleted from the local device. It may happen that this is not
* possible as the contact is in use. In this case, the contact can be blocked.
*
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
*
@@ -4098,19 +4068,9 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
// DC_INFO* uses the same values as SystemMessage in rust-land
#define DC_INFO_UNKNOWN 0
#define DC_INFO_GROUP_NAME_CHANGED 2
#define DC_INFO_GROUP_IMAGE_CHANGED 3
#define DC_INFO_MEMBER_ADDED_TO_GROUP 4
#define DC_INFO_MEMBER_REMOVED_FROM_GROUP 5
#define DC_INFO_AUTOCRYPT_SETUP_MESSAGE 6
#define DC_INFO_SECURE_JOIN_MESSAGE 7
#define DC_INFO_LOCATIONSTREAMING_ENABLED 8
#define DC_INFO_LOCATION_ONLY 9
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12
/**
* Check if a message is still in creation. A message is in creation between
@@ -4922,49 +4882,7 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
* @param lot The lot object.
* @return The timestamp as defined by the creator of the object. 0 if there is not timestamp or on errors.
*/
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* @class dc_reactions_t
*
* An object representing all reactions for a single message.
*/
/**
* Returns array of contacts which reacted to the given message.
*
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
*/
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
/**
* Returns a string containing space-separated reactions of a single contact.
*
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @param contact_id ID of the contact.
* @return Space-separated list of emoji sequences, which could be empty.
* Returned string should not be modified and should be freed
* with dc_str_unref() after usage.
*/
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
/**
* Frees an object containing message reactions.
*
* Reactions objects are created by dc_get_msg_reactions().
*
* @memberof dc_reactions_t
* @param reactions The object to free.
* If NULL is given, nothing is done.
*/
void dc_reactions_unref (dc_reactions_t* reactions);
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
@@ -5615,15 +5533,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_MSGS_CHANGED 2000
/**
* Message reactions changed.
*
* @param data1 (int) chat_id ID of the chat affected by the changes.
* @param data2 (int) msg_id ID of the message for which reactions were changed.
*/
#define DC_EVENT_REACTIONS_CHANGED 2001
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
@@ -5635,17 +5544,6 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_INCOMING_MSG 2005
/**
* Downloading a bunch of messages just finished. This is an experimental
* event to allow the UI to only show one notification per message bunch,
* instead of cluttering the user with many notifications.
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
*
* @param data1 0
* @param data2 (char*) msg_ids, a json object with the message ids.
*/
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
/**
* Messages were marked noticed or seen.

View File

@@ -1,6 +1,5 @@
use crate::chat::ChatItem;
use crate::constants::DC_MSG_ID_DAYMARKER;
use crate::contact::ContactId;
use crate::location::Location;
use crate::message::MsgId;
@@ -8,7 +7,6 @@ use crate::message::MsgId;
#[derive(Debug, Clone)]
pub enum dc_array_t {
MsgIds(Vec<MsgId>),
ContactIds(Vec<ContactId>),
Chat(Vec<ChatItem>),
Locations(Vec<Location>),
Uint(Vec<u32>),
@@ -18,7 +16,6 @@ impl dc_array_t {
pub(crate) fn get_id(&self, index: usize) -> u32 {
match self {
Self::MsgIds(array) => array[index].to_u32(),
Self::ContactIds(array) => array[index].to_u32(),
Self::Chat(array) => match array[index] {
ChatItem::Message { msg_id } => msg_id.to_u32(),
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
@@ -31,7 +28,6 @@ impl dc_array_t {
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
match self {
Self::MsgIds(_) => None,
Self::ContactIds(_) => None,
Self::Chat(array) => array.get(index).and_then(|item| match item {
ChatItem::Message { .. } => None,
ChatItem::DayMarker { timestamp } => Some(*timestamp),
@@ -44,7 +40,6 @@ impl dc_array_t {
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
match self {
Self::MsgIds(_) => None,
Self::ContactIds(_) => None,
Self::Chat(_) => None,
Self::Locations(array) => array
.get(index)
@@ -65,7 +60,6 @@ impl dc_array_t {
pub(crate) fn len(&self) -> usize {
match self {
Self::MsgIds(array) => array.len(),
Self::ContactIds(array) => array.len(),
Self::Chat(array) => array.len(),
Self::Locations(array) => array.len(),
Self::Uint(array) => array.len(),
@@ -89,12 +83,6 @@ impl From<Vec<MsgId>> for dc_array_t {
}
}
impl From<Vec<ContactId>> for dc_array_t {
fn from(array: Vec<ContactId>) -> Self {
dc_array_t::ContactIds(array)
}
}
impl From<Vec<ChatItem>> for dc_array_t {
fn from(array: Vec<ChatItem>) -> Self {
dc_array_t::Chat(array)

View File

@@ -37,7 +37,6 @@ use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::StatusUpdateSerial;
@@ -67,8 +66,6 @@ use deltachat::chatlist::Chatlist;
/// Struct representing the deltachat context.
pub type dc_context_t = Context;
pub type dc_reactions_t = Reactions;
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
fn block_on<T>(fut: T) -> T::Output
@@ -501,9 +498,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::Error(_) => 400,
EventType::ErrorSelfNotInGroup(_) => 410,
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::MsgsNoticed { .. } => 2008,
EventType::MsgDelivered { .. } => 2010,
EventType::MsgFailed { .. } => 2012,
@@ -545,10 +540,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::Error(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_) => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
| EventType::MsgsNoticed(chat_id)
| EventType::MsgDelivered { chat_id, .. }
@@ -602,11 +595,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::WebxdcInstanceDeleted { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::SelfavatarChanged => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
@@ -646,7 +637,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
data2.into_raw()
}
EventType::MsgsChanged { .. }
| EventType::ReactionsChanged { .. }
| EventType::IncomingMsg { .. }
| EventType::MsgsNoticed(_)
| EventType::MsgDelivered { .. }
@@ -674,11 +664,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = file.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
.unwrap_or_default()
.to_c_string()
.unwrap_or_default()
.into_raw(),
}
}
@@ -963,48 +948,6 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_reaction(
context: *mut dc_context_t,
msg_id: u32,
reaction: *const libc::c_char,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_reaction()");
return 0;
}
let ctx = &*context;
block_on(async move {
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to send reaction")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_reactions(
context: *mut dc_context_t,
msg_id: u32,
) -> *mut dc_reactions_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_msg_reactions()");
return ptr::null_mut();
}
let ctx = &*context;
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
.log_err(ctx, "failed dc_get_msg_reactions() call")
{
reactions
} else {
return ptr::null_mut();
};
Box::into_raw(Box::new(reactions))
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
@@ -2144,10 +2087,7 @@ pub unsafe extern "C" fn dc_delete_contact(
block_on(async move {
match Contact::delete(ctx, contact_id).await {
Ok(_) => 1,
Err(err) => {
error!(ctx, "cannot delete contact: {}", err);
0
}
Err(_) => 0,
}
})
}
@@ -2863,11 +2803,7 @@ pub unsafe extern "C" fn dc_chat_get_mailinglist_addr(chat: *mut dc_chat_t) -> *
return "".strdup();
}
let ffi_chat = &*chat;
ffi_chat
.chat
.get_mailinglist_addr()
.unwrap_or_default()
.strdup()
ffi_chat.chat.get_mailinglist_addr().strdup()
}
#[no_mangle]
@@ -4052,45 +3988,6 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
lot.get_timestamp()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_contacts(
reactions: *mut dc_reactions_t,
) -> *mut dc_array::dc_array_t {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
return ptr::null_mut();
}
let reactions = &*reactions;
let array: dc_array_t = reactions.contacts().into();
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
reactions: *mut dc_reactions_t,
contact_id: u32,
) -> *mut libc::c_char {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
return ptr::null_mut();
}
let reactions = &*reactions;
reactions.get(ContactId::new(contact_id)).as_str().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_unref()");
return;
}
drop(Box::from_raw(reactions));
}
#[no_mangle]
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
@@ -4555,13 +4452,11 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
mod jsonrpc {
use super::*;
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,
event_thread: JoinHandle<Result<(), anyhow::Error>>,
}
#[no_mangle]
@@ -4577,36 +4472,9 @@ mod jsonrpc {
deltachat_jsonrpc::api::CommandApi::from_arc((*account_manager).inner.clone());
let (request_handle, receiver) = RpcClient::new();
let request_handle2 = request_handle.clone();
let handle = RpcSession::new(request_handle, cmd_api);
let events = block_on({
async {
let am = (*account_manager).inner.clone();
let ev = am.read().await.get_event_emitter();
drop(am);
ev
}
});
let event_thread = spawn({
async move {
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
request_handle2
.send_notification("event", Some(event))
.await?;
}
let res: Result<(), anyhow::Error> = Ok(());
res
}
});
let instance = dc_jsonrpc_instance_t {
receiver,
handle,
event_thread,
};
let instance = dc_jsonrpc_instance_t { receiver, handle };
Box::into_raw(Box::new(instance))
}
@@ -4617,8 +4485,8 @@ mod jsonrpc {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
(*jsonrpc_instance).event_thread.abort();
drop(Box::from_raw(jsonrpc_instance));
Box::from_raw(jsonrpc_instance);
}
#[no_mangle]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.101.0"
version = "1.96.0"
description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
@@ -20,17 +20,16 @@ serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.6.1" }
futures = { version = "0.3.25" }
serde_json = "1.0.87"
futures = { version = "0.3.24" }
serde_json = "1.0.85"
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
tokio = { version = "1.21.2" }
sanitize-filename = "0.4"
walkdir = "2.3.2"
# optional dependencies
axum = { version = "0.5.17", optional = true, features = ["ws"] }
axum = { version = "0.5.16", optional = true, features = ["ws"] }
env_logger = { version = "0.9.1", optional = true }
walkdir = "2.3.2"
[dev-dependencies]
tokio = { version = "1.21.2", features = ["full", "rt-multi-thread"] }

View File

@@ -4,383 +4,142 @@ use serde_json::{json, Value};
use typescript_type_def::TypeDef;
pub fn event_to_json_rpc_notification(event: Event) -> Value {
let id: JSONRPCEventType = event.typ.into();
let (field1, field2): (Value, Value) = match &event.typ {
// events with a single string in field1
EventType::Info(txt)
| EventType::SmtpConnected(txt)
| EventType::ImapConnected(txt)
| EventType::SmtpMessageSent(txt)
| EventType::ImapMessageDeleted(txt)
| EventType::ImapMessageMoved(txt)
| EventType::NewBlobFile(txt)
| EventType::DeletedBlobFile(txt)
| EventType::Warning(txt)
| EventType::Error(txt)
| EventType::ErrorSelfNotInGroup(txt) => (json!(txt), Value::Null),
EventType::ImexFileWritten(path) => (json!(path.to_str()), Value::Null),
// single number
EventType::MsgsNoticed(chat_id) | EventType::ChatModified(chat_id) => {
(json!(chat_id), Value::Null)
}
EventType::ImexProgress(progress) => (json!(progress), Value::Null),
// both fields contain numbers
EventType::MsgsChanged { chat_id, msg_id }
| EventType::IncomingMsg { chat_id, msg_id }
| EventType::MsgDelivered { chat_id, msg_id }
| EventType::MsgFailed { chat_id, msg_id }
| EventType::MsgRead { chat_id, msg_id } => (json!(chat_id), json!(msg_id)),
EventType::ChatEphemeralTimerModified { chat_id, timer } => (json!(chat_id), json!(timer)),
EventType::SecurejoinInviterProgress {
contact_id,
progress,
}
| EventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => (json!(contact_id), json!(progress)),
// field 1 number or null
EventType::ContactsChanged(maybe_number) | EventType::LocationChanged(maybe_number) => (
match maybe_number {
Some(number) => json!(number),
None => Value::Null,
},
Value::Null,
),
// number and maybe string
EventType::ConfigureProgress { progress, comment } => (
json!(progress),
match comment {
Some(content) => json!(content),
None => Value::Null,
},
),
EventType::ConnectivityChanged => (Value::Null, Value::Null),
EventType::SelfavatarChanged => (Value::Null, Value::Null),
EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial,
} => (json!(msg_id), json!(status_update_serial)),
EventType::WebxdcInstanceDeleted { msg_id } => (json!(msg_id), Value::Null),
};
let id: EventTypeName = event.typ.into();
json!({
"event": id,
"id": id,
"contextId": event.id,
"field1": field1,
"field2": field2
})
}
#[derive(Serialize, TypeDef)]
#[serde(tag = "type", rename = "Event")]
pub enum JSONRPCEventType {
/// The library-user may write an informational string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Info {
msg: String,
},
/// Emitted when SMTP connection is established and login was successful.
SmtpConnected {
msg: String,
},
/// Emitted when IMAP connection is established and login was successful.
ImapConnected {
msg: String,
},
/// Emitted when a message was successfully sent to the SMTP server.
SmtpMessageSent {
msg: String,
},
/// Emitted when an IMAP message has been marked as deleted
ImapMessageDeleted {
msg: String,
},
/// Emitted when an IMAP message has been moved
ImapMessageMoved {
msg: String,
},
/// Emitted when an new file in the $BLOBDIR was created
NewBlobFile {
file: String,
},
/// Emitted when an file in the $BLOBDIR was deleted
DeletedBlobFile {
file: String,
},
/// The library-user should write a warning string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Warning {
msg: String,
},
/// The library-user should report an error to the end-user.
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
///
/// However, for ongoing processes (eg. configure())
/// or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then.
Error {
msg: String,
},
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// setChatName(), setChatProfileImage(),
/// addContactToChat(), removeContactFromChat(),
/// and messages sending functions.
ErrorSelfNotInGroup {
msg: String,
},
/// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database:
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
#[serde(rename_all = "camelCase")]
MsgsChanged {
chat_id: u32,
msg_id: u32,
},
/// Reactions for the message changed.
#[serde(rename_all = "camelCase")]
ReactionsChanged {
chat_id: u32,
msg_id: u32,
contact_id: u32,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
#[serde(rename_all = "camelCase")]
IncomingMsg {
chat_id: u32,
msg_id: u32,
},
/// Downloading a bunch of messages just finished. This is an experimental
/// event to allow the UI to only show one notification per message bunch,
/// instead of cluttering the user with many notifications.
///
/// msg_ids contains the message ids.
#[serde(rename_all = "camelCase")]
IncomingMsgBunch {
msg_ids: Vec<u32>,
},
/// Messages were seen or noticed.
/// chat id is always set.
#[serde(rename_all = "camelCase")]
MsgsNoticed {
chat_id: u32,
},
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgDelivered {
chat_id: u32,
msg_id: u32,
},
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgFailed {
chat_id: u32,
msg_id: u32,
},
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgRead {
chat_id: u32,
msg_id: u32,
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See setChatName(), setChatProfileImage(), addContactToChat()
/// and removeContactFromChat().
///
/// This event does not include ephemeral timer modification, which
/// is a separate event.
#[serde(rename_all = "camelCase")]
ChatModified {
chat_id: u32,
},
/// Chat ephemeral timer changed.
#[serde(rename_all = "camelCase")]
ChatEphemeralTimerModified {
chat_id: u32,
timer: u32,
},
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
#[serde(rename_all = "camelCase")]
ContactsChanged {
contact_id: Option<u32>,
},
/// Location of one or more contact has changed.
///
/// @param data1 (u32) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// this parameter is set to `None`.
#[serde(rename_all = "camelCase")]
LocationChanged {
contact_id: Option<u32>,
},
/// Inform about the configuration progress started by configure().
ConfigureProgress {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
},
/// Inform about the import/export progress started by imex().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
#[serde(rename_all = "camelCase")]
ImexProgress {
progress: usize,
},
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
///
/// A typical purpose for a handler of this event may be to make the file public to some system
/// services.
///
/// @param data2 0
#[serde(rename_all = "camelCase")]
ImexFileWritten {
path: String,
},
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
///
/// These events are typically sent after a joiner has scanned the QR code
/// generated by getChatSecurejoinQrCodeSvg().
///
/// @param data1 (int) ID of the contact that wants to join.
/// @param data2 (int) Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
#[serde(rename_all = "camelCase")]
SecurejoinInviterProgress {
contact_id: u32,
progress: usize,
},
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
/// The events are typically sent while secureJoin(), which
/// may take some time, is executed.
/// @param data1 (int) ID of the inviting contact.
/// @param data2 (int) Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
#[serde(rename_all = "camelCase")]
SecurejoinJoinerProgress {
contact_id: u32,
progress: usize,
},
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
/// and possibly the connectivtiy HTML; see getConnectivity() and
/// getConnectivityHtml() for details.
pub enum EventTypeName {
Info,
SmtpConnected,
ImapConnected,
SmtpMessageSent,
ImapMessageDeleted,
ImapMessageMoved,
NewBlobFile,
DeletedBlobFile,
Warning,
Error,
ErrorSelfNotInGroup,
MsgsChanged,
IncomingMsg,
MsgsNoticed,
MsgDelivered,
MsgFailed,
MsgRead,
ChatModified,
ChatEphemeralTimerModified,
ContactsChanged,
LocationChanged,
ConfigureProgress,
ImexProgress,
ImexFileWritten,
SecurejoinInviterProgress,
SecurejoinJoinerProgress,
ConnectivityChanged,
SelfavatarChanged,
#[serde(rename_all = "camelCase")]
WebxdcStatusUpdate {
msg_id: u32,
status_update_serial: u32,
},
/// Inform that a message containing a webxdc instance has been deleted
#[serde(rename_all = "camelCase")]
WebxdcInstanceDeleted {
msg_id: u32,
},
WebxdcStatusUpdate,
WebXdInstanceDeleted,
}
impl From<EventType> for JSONRPCEventType {
impl From<EventType> for EventTypeName {
fn from(event: EventType) -> Self {
use JSONRPCEventType::*;
use EventTypeName::*;
match event {
EventType::Info(msg) => Info { msg },
EventType::SmtpConnected(msg) => SmtpConnected { msg },
EventType::ImapConnected(msg) => ImapConnected { msg },
EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
EventType::NewBlobFile(file) => NewBlobFile { file },
EventType::DeletedBlobFile(file) => DeletedBlobFile { file },
EventType::Warning(msg) => Warning { msg },
EventType::Error(msg) => Error { msg },
EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => ReactionsChanged {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
contact_id: contact_id.to_u32(),
},
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
},
EventType::MsgsNoticed(chat_id) => MsgsNoticed {
chat_id: chat_id.to_u32(),
},
EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::MsgFailed { chat_id, msg_id } => MsgFailed {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::MsgRead { chat_id, msg_id } => MsgRead {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::ChatModified(chat_id) => ChatModified {
chat_id: chat_id.to_u32(),
},
EventType::ChatEphemeralTimerModified { chat_id, timer } => {
ChatEphemeralTimerModified {
chat_id: chat_id.to_u32(),
timer: timer.to_u32(),
}
}
EventType::ContactsChanged(contact) => ContactsChanged {
contact_id: contact.map(|c| c.to_u32()),
},
EventType::LocationChanged(contact) => LocationChanged {
contact_id: contact.map(|c| c.to_u32()),
},
EventType::ConfigureProgress { progress, comment } => {
ConfigureProgress { progress, comment }
}
EventType::ImexProgress(progress) => ImexProgress { progress },
EventType::ImexFileWritten(path) => ImexFileWritten {
path: path.to_str().unwrap_or_default().to_owned(),
},
EventType::SecurejoinInviterProgress {
contact_id,
progress,
} => SecurejoinInviterProgress {
contact_id: contact_id.to_u32(),
progress,
},
EventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => SecurejoinJoinerProgress {
contact_id: contact_id.to_u32(),
progress,
},
EventType::Info(_) => Info,
EventType::SmtpConnected(_) => SmtpConnected,
EventType::ImapConnected(_) => ImapConnected,
EventType::SmtpMessageSent(_) => SmtpMessageSent,
EventType::ImapMessageDeleted(_) => ImapMessageDeleted,
EventType::ImapMessageMoved(_) => ImapMessageMoved,
EventType::NewBlobFile(_) => NewBlobFile,
EventType::DeletedBlobFile(_) => DeletedBlobFile,
EventType::Warning(_) => Warning,
EventType::Error(_) => Error,
EventType::ErrorSelfNotInGroup(_) => ErrorSelfNotInGroup,
EventType::MsgsChanged { .. } => MsgsChanged,
EventType::IncomingMsg { .. } => IncomingMsg,
EventType::MsgsNoticed(_) => MsgsNoticed,
EventType::MsgDelivered { .. } => MsgDelivered,
EventType::MsgFailed { .. } => MsgFailed,
EventType::MsgRead { .. } => MsgRead,
EventType::ChatModified(_) => ChatModified,
EventType::ChatEphemeralTimerModified { .. } => ChatEphemeralTimerModified,
EventType::ContactsChanged(_) => ContactsChanged,
EventType::LocationChanged(_) => LocationChanged,
EventType::ConfigureProgress { .. } => ConfigureProgress,
EventType::ImexProgress(_) => ImexProgress,
EventType::ImexFileWritten(_) => ImexFileWritten,
EventType::SecurejoinInviterProgress { .. } => SecurejoinInviterProgress,
EventType::SecurejoinJoinerProgress { .. } => SecurejoinJoinerProgress,
EventType::ConnectivityChanged => ConnectivityChanged,
EventType::SelfavatarChanged => SelfavatarChanged,
EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial,
} => WebxdcStatusUpdate {
msg_id: msg_id.to_u32(),
status_update_serial: status_update_serial.to_u32(),
},
EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
},
EventType::WebxdcStatusUpdate { .. } => WebxdcStatusUpdate,
EventType::WebxdcInstanceDeleted { .. } => WebXdInstanceDeleted,
}
}
}
@@ -394,8 +153,7 @@ fn generate_events_ts_types_definition() {
root_namespace: None,
..typescript_type_def::DefinitionFileOptions::default()
};
typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&mut buf, options)
.unwrap();
typescript_type_def::write_definition_file::<_, EventTypeName>(&mut buf, options).unwrap();
String::from_utf8(buf).unwrap()
};
std::fs::write("typescript/generated/events.ts", events).unwrap();

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, bail, ensure, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use deltachat::{
chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, marknoticed_chat,
@@ -17,16 +17,14 @@ use deltachat::{
provider::get_provider_info,
qr,
qr_code_generator::get_securejoin_qr_svg,
reaction::send_reaction,
securejoin,
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
};
use sanitize_filename::is_sanitized;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{collections::HashMap, str::FromStr};
use tokio::{fs, sync::RwLock};
use tokio::sync::RwLock;
use walkdir::WalkDir;
use yerpc::rpc;
@@ -36,7 +34,7 @@ pub mod events;
pub mod types;
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::qr::QrObject;
use crate::api::types::QrObject;
use types::account::Account;
use types::chat::FullChat;
@@ -401,12 +399,12 @@ impl CommandApi {
// autocrypt
// ---------------------------------------------
async fn initiate_autocrypt_key_transfer(&self, account_id: u32) -> Result<String> {
async fn autocrypt_initiate_key_transfer(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
deltachat::imex::initiate_key_transfer(&ctx).await
}
async fn continue_autocrypt_key_transfer(
async fn autocrypt_continue_key_transfer(
&self,
account_id: u32,
message_id: u32,
@@ -473,7 +471,11 @@ impl CommandApi {
// chat
// ---------------------------------------------
async fn get_full_chat_by_id(&self, account_id: u32, chat_id: u32) -> Result<FullChat> {
async fn chatlist_get_full_chat_by_id(
&self,
account_id: u32,
chat_id: u32,
) -> Result<FullChat> {
let ctx = self.get_context(account_id).await?;
FullChat::try_from_dc_chat_id(&ctx, chat_id).await
}
@@ -908,17 +910,12 @@ impl CommandApi {
.collect::<Vec<JSONRPCMessageListItem>>())
}
async fn get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
async fn message_get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
MessageObject::from_message_id(&ctx, message_id).await
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
MsgId::new(message_id).get_html(&ctx).await
}
async fn get_messages(
async fn message_get_messages(
&self,
account_id: u32,
message_ids: Vec<u32>,
@@ -935,7 +932,7 @@ impl CommandApi {
}
/// Fetch info desktop needs for creating a notification for a message
async fn get_message_notification_info(
async fn message_get_notification_info(
&self,
account_id: u32,
message_id: u32,
@@ -1026,7 +1023,11 @@ impl CommandApi {
// ---------------------------------------------
/// Get a single contact options by ID.
async fn get_contact(&self, account_id: u32, contact_id: u32) -> Result<ContactObject> {
async fn contacts_get_contact(
&self,
account_id: u32,
contact_id: u32,
) -> Result<ContactObject> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
@@ -1040,7 +1041,7 @@ impl CommandApi {
/// Add a single contact as a result of an explicit user action.
///
/// Returns contact id of the created or existing contact
async fn create_contact(
async fn contacts_create_contact(
&self,
account_id: u32,
email: String,
@@ -1057,7 +1058,11 @@ impl CommandApi {
}
/// Returns contact id of the created or existing DM chat with that contact
async fn create_chat_by_contact_id(&self, account_id: u32, contact_id: u32) -> Result<u32> {
async fn contacts_create_chat_by_contact_id(
&self,
account_id: u32,
contact_id: u32,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let contact = Contact::get_by_id(&ctx, ContactId::new(contact_id)).await?;
ChatId::create_for_contact(&ctx, contact.id)
@@ -1065,17 +1070,17 @@ impl CommandApi {
.map(|id| id.to_u32())
}
async fn block_contact(&self, account_id: u32, contact_id: u32) -> Result<()> {
async fn contacts_block(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
Contact::block(&ctx, ContactId::new(contact_id)).await
}
async fn unblock_contact(&self, account_id: u32, contact_id: u32) -> Result<()> {
async fn contacts_unblock(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
Contact::unblock(&ctx, ContactId::new(contact_id)).await
}
async fn get_blocked_contacts(&self, account_id: u32) -> Result<Vec<ContactObject>> {
async fn contacts_get_blocked(&self, account_id: u32) -> Result<Vec<ContactObject>> {
let ctx = self.get_context(account_id).await?;
let blocked_ids = Contact::get_all_blocked(&ctx).await?;
let mut contacts: Vec<ContactObject> = Vec::with_capacity(blocked_ids.len());
@@ -1091,7 +1096,7 @@ impl CommandApi {
Ok(contacts)
}
async fn get_contact_ids(
async fn contacts_get_contact_ids(
&self,
account_id: u32,
list_flags: u32,
@@ -1104,7 +1109,7 @@ impl CommandApi {
/// Get a list of contacts.
/// (formerly called getContacts2 in desktop)
async fn get_contacts(
async fn contacts_get_contacts(
&self,
account_id: u32,
list_flags: u32,
@@ -1125,7 +1130,7 @@ impl CommandApi {
Ok(contacts)
}
async fn get_contacts_by_ids(
async fn contacts_get_contacts_by_ids(
&self,
account_id: u32,
ids: Vec<u32>,
@@ -1208,7 +1213,7 @@ impl CommandApi {
///
/// Setting `chat_id` to `None` (`null` in typescript) means get messages with media
/// from any chat of the currently used account.
async fn get_chat_media(
async fn chat_get_media(
&self,
account_id: u32,
chat_id: Option<u32>,
@@ -1236,7 +1241,7 @@ impl CommandApi {
///
/// one combined call for getting chat::get_next_media for both directions
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
async fn get_neighboring_chat_media(
async fn chat_get_neighboring_media(
&self,
account_id: u32,
msg_id: u32,
@@ -1388,7 +1393,7 @@ impl CommandApi {
// webxdc
// ---------------------------------------------
async fn send_webxdc_status_update(
async fn webxdc_send_status_update(
&self,
account_id: u32,
instance_msg_id: u32,
@@ -1400,7 +1405,7 @@ impl CommandApi {
.await
}
async fn get_webxdc_status_updates(
async fn webxdc_get_status_updates(
&self,
account_id: u32,
instance_msg_id: u32,
@@ -1415,7 +1420,7 @@ impl CommandApi {
}
/// Get info from a webxdc message
async fn get_webxdc_info(
async fn message_get_webxdc_info(
&self,
account_id: u32,
instance_msg_id: u32,
@@ -1456,23 +1461,6 @@ impl CommandApi {
Ok(message_id.to_u32())
}
/// Send a reaction to message.
///
/// Reaction is a string of emojis separated by spaces. Reaction to a
/// single message can be sent multiple times. The last reaction
/// received overrides all previously received reactions. It is
/// possible to remove all reactions by sending an empty string.
async fn send_reaction(
&self,
account_id: u32,
message_id: u32,
reaction: Vec<String>,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let message_id = send_reaction(&ctx, MsgId::new(message_id), &reaction.join(" ")).await?;
Ok(message_id.to_u32())
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field
@@ -1507,109 +1495,12 @@ impl CommandApi {
// that might get removed later again
// ---------------------------------------------
async fn misc_get_sticker_folder(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let account_folder = ctx
.get_dbfile()
.parent()
.context("account folder not found")?;
let sticker_folder_path = account_folder.join("stickers");
fs::create_dir_all(&sticker_folder_path).await?;
sticker_folder_path
.to_str()
.map(|s| s.to_owned())
.context("path conversion to string failed")
}
/// save a sticker to a collection/folder in the account's sticker folder
async fn misc_save_sticker(
&self,
account_id: u32,
msg_id: u32,
collection: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
ensure!(
message.get_viewtype() == Viewtype::Sticker,
"message {} is not a sticker",
msg_id
);
let account_folder = ctx
.get_dbfile()
.parent()
.context("account folder not found")?;
ensure!(
is_sanitized(&collection),
"illegal characters in collection name"
);
let destination_path = account_folder.join("stickers").join(collection);
fs::create_dir_all(&destination_path).await?;
let file = message.get_file(&ctx).context("no file")?;
fs::copy(
&file,
destination_path.join(format!(
"{}.{}",
msg_id,
file.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
)),
)
.await?;
Ok(())
}
/// for desktop, get stickers from stickers folder,
/// grouped by the collection/folder they are in.
async fn misc_get_stickers(&self, account_id: u32) -> Result<HashMap<String, Vec<String>>> {
let ctx = self.get_context(account_id).await?;
let account_folder = ctx
.get_dbfile()
.parent()
.context("account folder not found")?;
let sticker_folder_path = account_folder.join("stickers");
fs::create_dir_all(&sticker_folder_path).await?;
let mut result = HashMap::new();
let mut packs = tokio::fs::read_dir(sticker_folder_path).await?;
while let Some(entry) = packs.next_entry().await? {
if !entry.file_type().await?.is_dir() {
continue;
}
let pack_name = entry.file_name().into_string().unwrap_or_default();
let mut stickers = tokio::fs::read_dir(entry.path()).await?;
let mut sticker_paths = Vec::new();
while let Some(sticker_entry) = stickers.next_entry().await? {
if !sticker_entry.file_type().await?.is_file() {
continue;
}
let sticker_name = sticker_entry.file_name().into_string().unwrap_or_default();
if sticker_name.ends_with(".png") || sticker_name.ends_with(".webp") {
sticker_paths.push(
sticker_entry
.path()
.to_str()
.map(|s| s.to_owned())
.context("path conversion to string failed")?,
);
}
}
if !sticker_paths.is_empty() {
result.insert(pack_name, sticker_paths);
}
}
Ok(result)
}
/// Returns the messageid of the sent message
async fn misc_send_text_message(
&self,
account_id: u32,
chat_id: u32,
text: String,
chat_id: u32,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
@@ -1715,11 +1606,8 @@ async fn set_config(
if key.starts_with("ui.") {
ctx.set_ui_config(key, value).await?;
} else {
ctx.set_config(
Config::from_str(key).with_context(|| format!("unknown key {:?}", key))?,
value,
)
.await?;
ctx.set_config(Config::from_str(key).context("unknown key")?, value)
.await?;
match key {
"sentbox_watch" | "mvbox_move" | "only_fetch_mvbox" => {
@@ -1738,7 +1626,7 @@ async fn get_config(
if key.starts_with("ui.") {
ctx.get_ui_config(key).await
} else {
ctx.get_config(Config::from_str(key).with_context(|| format!("unknown key {:?}", key))?)
ctx.get_config(Config::from_str(key).context("unknown key")?)
.await
}
}

View File

@@ -37,7 +37,7 @@ pub struct FullChat {
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
can_send: bool,
was_seen_recently: bool,
mailing_list_address: Option<String>,
mailing_list_address: String,
}
impl FullChat {
@@ -81,7 +81,7 @@ impl FullChat {
false
};
let mailing_list_address = chat.get_mailinglist_addr().map(|s| s.to_string());
let mailing_list_address = chat.get_mailinglist_addr().to_string();
Ok(FullChat {
id: chat_id,

View File

@@ -8,7 +8,6 @@ use deltachat::download;
use deltachat::message::Message;
use deltachat::message::MsgId;
use deltachat::message::Viewtype;
use deltachat::reaction::get_msg_reactions;
use num_traits::cast::ToPrimitive;
use serde::Deserialize;
use serde::Serialize;
@@ -16,7 +15,6 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef)]
@@ -45,8 +43,6 @@ pub struct MessageObject {
is_setupmessage: bool,
is_info: bool,
is_forwarded: bool,
/// when is_info is true this describes what type of system message it is
system_message_type: SystemMessageType,
duration: i32,
dimensions_height: i32,
@@ -68,8 +64,6 @@ pub struct MessageObject {
webxdc_info: Option<WebxdcMessageInfo>,
download_state: DownloadState,
reactions: Option<JSONRPCReactions>,
}
#[derive(Serialize, TypeDef)]
@@ -127,7 +121,6 @@ impl MessageObject {
override_sender_name: quote.get_override_sender_name(),
image: if quote.get_viewtype() == Viewtype::Image
|| quote.get_viewtype() == Viewtype::Gif
|| quote.get_viewtype() == Viewtype::Sticker
{
match quote.get_file(context) {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
@@ -146,13 +139,6 @@ impl MessageObject {
None
};
let reactions = get_msg_reactions(context, msg_id).await?;
let reactions = if reactions.is_empty() {
None
} else {
Some(reactions.into())
};
Ok(MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
@@ -178,7 +164,6 @@ impl MessageObject {
is_setupmessage: message.is_setupmessage(),
is_info: message.is_info(),
is_forwarded: message.is_forwarded(),
system_message_type: message.get_info_type().into(),
duration: message.get_duration(),
dimensions_height: message.get_height(),
@@ -208,8 +193,6 @@ impl MessageObject {
webxdc_info,
download_state,
reactions,
})
}
}
@@ -309,61 +292,6 @@ impl From<download::DownloadState> for DownloadState {
}
}
#[derive(Serialize, TypeDef)]
pub enum SystemMessageType {
Unknown,
GroupNameChanged,
GroupImageChanged,
MemberAddedToGroup,
MemberRemovedFromGroup,
AutocryptSetupMessage,
SecurejoinMessage,
LocationStreamingEnabled,
LocationOnly,
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
// Chat protection state changed
ChatProtectionEnabled,
ChatProtectionDisabled,
/// Self-sent-message that contains only json used for multi-device-sync;
/// if possible, we attach that to other messages as for locations.
MultiDeviceSync,
// Sync message that contains a json payload
// sent to the other webxdc instances
// These messages are not shown in the chat.
WebxdcStatusUpdate,
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
WebxdcInfoMessage,
}
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
fn from(system_message_type: deltachat::mimeparser::SystemMessage) -> Self {
use deltachat::mimeparser::SystemMessage;
match system_message_type {
SystemMessage::Unknown => SystemMessageType::Unknown,
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
SystemMessage::AutocryptSetupMessage => SystemMessageType::AutocryptSetupMessage,
SystemMessage::SecurejoinMessage => SystemMessageType::SecurejoinMessage,
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
}
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageNotificationInfo {

View File

@@ -1,3 +1,7 @@
use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
pub mod account;
pub mod chat;
pub mod chat_list;
@@ -5,8 +9,6 @@ pub mod contact;
pub mod location;
pub mod message;
pub mod provider_info;
pub mod qr;
pub mod reactions;
pub mod webxdc;
pub fn color_int_to_hex_string(color: u32) -> String {
@@ -20,3 +22,213 @@ fn maybe_empty_string_to_option(string: String) -> Option<String> {
Some(string)
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
AskVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
FprOk {
contact_id: u32,
},
FprMismatch {
contact_id: Option<u32>,
},
FprWithoutAddr {
fingerprint: String,
},
Account {
domain: String,
},
WebrtcInstance {
domain: String,
instance_pattern: String,
},
Addr {
contact_id: u32,
draft: Option<String>,
},
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
WithdrawVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
Login {
address: String,
},
}
impl From<Qr> for QrObject {
fn from(qr: Qr) -> Self {
match qr {
Qr::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::FprOk { contact_id } => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
}
Qr::FprMismatch { contact_id } => {
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
QrObject::FprMismatch { contact_id }
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::WebrtcInstance {
domain,
instance_pattern,
} => QrObject::WebrtcInstance {
domain,
instance_pattern,
},
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }
}
Qr::Url { url } => QrObject::Url { url },
Qr::Text { text } => QrObject::Text { text },
Qr::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::Login { address, .. } => QrObject::Login { address },
}
}
}

View File

@@ -1,213 +0,0 @@
use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
AskVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
FprOk {
contact_id: u32,
},
FprMismatch {
contact_id: Option<u32>,
},
FprWithoutAddr {
fingerprint: String,
},
Account {
domain: String,
},
WebrtcInstance {
domain: String,
instance_pattern: String,
},
Addr {
contact_id: u32,
draft: Option<String>,
},
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
WithdrawVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
Login {
address: String,
},
}
impl From<Qr> for QrObject {
fn from(qr: Qr) -> Self {
match qr {
Qr::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::FprOk { contact_id } => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
}
Qr::FprMismatch { contact_id } => {
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
QrObject::FprMismatch { contact_id }
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::WebrtcInstance {
domain,
instance_pattern,
} => QrObject::WebrtcInstance {
domain,
instance_pattern,
},
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }
}
Qr::Url { url } => QrObject::Url { url },
Qr::Text { text } => QrObject::Text { text },
Qr::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::Login { address, .. } => QrObject::Login { address },
}
}
}

View File

@@ -1,47 +0,0 @@
use std::collections::BTreeMap;
use deltachat::reaction::Reactions;
use serde::Serialize;
use typescript_type_def::TypeDef;
/// Structure representing all reactions to a particular message.
#[derive(Serialize, TypeDef)]
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JSONRPCReactions {
/// Map from a contact to it's reaction to message.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count
reactions: BTreeMap<String, u32>,
}
impl From<Reactions> for JSONRPCReactions {
fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
for contact_id in reactions.contacts() {
let reaction = reactions.get(contact_id);
if reaction.is_empty() {
continue;
}
let emojis: Vec<String> = reaction
.emojis()
.into_iter()
.map(|emoji| emoji.to_owned())
.collect();
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
for emoji in emojis {
if let Some(x) = unique_reactions.get_mut(&emoji) {
*x += 1;
} else {
unique_reactions.insert(emoji, 1);
}
}
}
JSONRPCReactions {
reactions_by_contact,
reactions: unique_reactions,
}
}
}

View File

@@ -1,4 +1,4 @@
import { DcEvent, DeltaChat } from "../deltachat.js";
import { DeltaChat, DeltaChatEvent } from "../deltachat.js";
var SELECTED_ACCOUNT = 0;
@@ -7,7 +7,7 @@ window.addEventListener("DOMContentLoaded", (_event) => {
SELECTED_ACCOUNT = Number(id);
window.dispatchEvent(new Event("account-changed"));
};
console.log("launch run script...");
console.log('launch run script...')
run().catch((err) => console.error("run failed", err));
});
@@ -16,13 +16,13 @@ async function run() {
const $side = document.getElementById("side")!;
const $head = document.getElementById("header")!;
const client = new DeltaChat("ws://localhost:20808/ws");
const client = new DeltaChat('ws://localhost:20808/ws')
(window as any).client = client.rpc;
;(window as any).client = client.rpc;
client.on("ALL", (accountId, event) => {
onIncomingEvent(accountId, event);
});
client.on("ALL", event => {
onIncomingEvent(event)
})
window.addEventListener("account-changed", async (_event: Event) => {
listChatsForSelectedAccount();
@@ -31,9 +31,9 @@ async function run() {
await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
async function loadAccountsInHeader() {
console.log("load accounts");
console.log('load accounts')
const accounts = await client.rpc.getAllAccounts();
console.log("accounts loaded", accounts);
console.log('accounts loaded', accounts)
for (const account of accounts) {
if (account.type === "Configured") {
write(
@@ -48,14 +48,14 @@ async function run() {
`<a href="#">
${account.id}: (unconfigured)
</a>&nbsp;`
);
)
}
}
}
async function listChatsForSelectedAccount() {
clear($main);
const selectedAccount = SELECTED_ACCOUNT;
const selectedAccount = SELECTED_ACCOUNT
const info = await client.rpc.getAccountInfo(selectedAccount);
if (info.type !== "Configured") {
return write($main, "Account is not configured");
@@ -68,7 +68,7 @@ async function run() {
null
);
for (const [chatId, _messageId] of chats) {
const chat = await client.rpc.getFullChatById(
const chat = await client.rpc.chatlistGetFullChatById(
selectedAccount,
chatId
);
@@ -78,7 +78,7 @@ async function run() {
chatId,
0
);
const messages = await client.rpc.getMessages(
const messages = await client.rpc.messageGetMessages(
selectedAccount,
messageIds
);
@@ -88,15 +88,14 @@ async function run() {
}
}
function onIncomingEvent(accountId: number, event: DcEvent) {
function onIncomingEvent(event: DeltaChatEvent) {
write(
$side,
`
<p class="message">
[<strong>${event.type}</strong> on account ${accountId}]<br>
<em>f1:</em> ${JSON.stringify(
Object.assign({}, event, { type: undefined })
)}
[<strong>${event.id}</strong> on account ${event.contextId}]<br>
<em>f1:</em> ${JSON.stringify(event.field1)}<br>
<em>f2:</em> ${JSON.stringify(event.field2)}
</p>`
);
}

View File

@@ -232,13 +232,13 @@ export class RawClient {
}
public initiateAutocryptKeyTransfer(accountId: T.U32): Promise<string> {
return (this._transport.request('initiate_autocrypt_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
public autocryptInitiateKeyTransfer(accountId: T.U32): Promise<string> {
return (this._transport.request('autocrypt_initiate_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
}
public continueAutocryptKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
return (this._transport.request('continue_autocrypt_key_transfer', [accountId, messageId, setupCode] as RPC.Params)) as Promise<null>;
public autocryptContinueKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
return (this._transport.request('autocrypt_continue_key_transfer', [accountId, messageId, setupCode] as RPC.Params)) as Promise<null>;
}
@@ -252,8 +252,8 @@ export class RawClient {
}
public getFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
return (this._transport.request('get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
public chatlistGetFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
return (this._transport.request('chatlist_get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
}
/**
@@ -593,25 +593,20 @@ export class RawClient {
}
public getMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
return (this._transport.request('get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
public messageGetMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
return (this._transport.request('message_get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
}
public getMessageHtml(accountId: T.U32, messageId: T.U32): Promise<(string|null)> {
return (this._transport.request('get_message_html', [accountId, messageId] as RPC.Params)) as Promise<(string|null)>;
}
public getMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return (this._transport.request('get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
public messageGetMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
}
/**
* Fetch info desktop needs for creating a notification for a message
*/
public getMessageNotificationInfo(accountId: T.U32, messageId: T.U32): Promise<T.MessageNotificationInfo> {
return (this._transport.request('get_message_notification_info', [accountId, messageId] as RPC.Params)) as Promise<T.MessageNotificationInfo>;
public messageGetNotificationInfo(accountId: T.U32, messageId: T.U32): Promise<T.MessageNotificationInfo> {
return (this._transport.request('message_get_notification_info', [accountId, messageId] as RPC.Params)) as Promise<T.MessageNotificationInfo>;
}
/**
@@ -676,8 +671,8 @@ export class RawClient {
/**
* Get a single contact options by ID.
*/
public getContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
return (this._transport.request('get_contact', [accountId, contactId] as RPC.Params)) as Promise<T.Contact>;
public contactsGetContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
return (this._transport.request('contacts_get_contact', [accountId, contactId] as RPC.Params)) as Promise<T.Contact>;
}
/**
@@ -685,48 +680,48 @@ export class RawClient {
*
* Returns contact id of the created or existing contact
*/
public createContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
return (this._transport.request('create_contact', [accountId, email, name] as RPC.Params)) as Promise<T.U32>;
public contactsCreateContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
return (this._transport.request('contacts_create_contact', [accountId, email, name] as RPC.Params)) as Promise<T.U32>;
}
/**
* Returns contact id of the created or existing DM chat with that contact
*/
public createChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
return (this._transport.request('create_chat_by_contact_id', [accountId, contactId] as RPC.Params)) as Promise<T.U32>;
public contactsCreateChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
return (this._transport.request('contacts_create_chat_by_contact_id', [accountId, contactId] as RPC.Params)) as Promise<T.U32>;
}
public blockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('block_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
public contactsBlock(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('contacts_block', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public unblockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('unblock_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
public contactsUnblock(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('contacts_unblock', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public getBlockedContacts(accountId: T.U32): Promise<(T.Contact)[]> {
return (this._transport.request('get_blocked_contacts', [accountId] as RPC.Params)) as Promise<(T.Contact)[]>;
public contactsGetBlocked(accountId: T.U32): Promise<(T.Contact)[]> {
return (this._transport.request('contacts_get_blocked', [accountId] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public getContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
return (this._transport.request('get_contact_ids', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.U32)[]>;
public contactsGetContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
return (this._transport.request('contacts_get_contact_ids', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Get a list of contacts.
* (formerly called getContacts2 in desktop)
*/
public getContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
return (this._transport.request('get_contacts', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.Contact)[]>;
public contactsGetContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
return (this._transport.request('contacts_get_contacts', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public getContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
return (this._transport.request('get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
public contactsGetContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
return (this._transport.request('contacts_get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
}
@@ -770,8 +765,8 @@ export class RawClient {
* Setting `chat_id` to `None` (`null` in typescript) means get messages with media
* from any chat of the currently used account.
*/
public getChatMedia(accountId: T.U32, chatId: (T.U32|null), messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
return (this._transport.request('get_chat_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
public chatGetMedia(accountId: T.U32, chatId: (T.U32|null), messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
return (this._transport.request('chat_get_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
@@ -782,8 +777,8 @@ export class RawClient {
* one combined call for getting chat::get_next_media for both directions
* the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
*/
public getNeighboringChatMedia(accountId: T.U32, msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
return (this._transport.request('get_neighboring_chat_media', [accountId, msgId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<[(T.U32|null),(T.U32|null)]>;
public chatGetNeighboringMedia(accountId: T.U32, msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
return (this._transport.request('chat_get_neighboring_media', [accountId, msgId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<[(T.U32|null),(T.U32|null)]>;
}
@@ -845,20 +840,20 @@ export class RawClient {
}
public sendWebxdcStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return (this._transport.request('send_webxdc_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
public webxdcSendStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return (this._transport.request('webxdc_send_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
}
public getWebxdcStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
return (this._transport.request('get_webxdc_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
public webxdcGetStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
return (this._transport.request('webxdc_get_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
}
/**
* Get info from a webxdc message
*/
public getWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
return (this._transport.request('get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
public messageGetWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
return (this._transport.request('message_get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
}
/**
@@ -878,18 +873,6 @@ export class RawClient {
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.U32>;
}
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*/
public sendReaction(accountId: T.U32, messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
return (this._transport.request('send_reaction', [accountId, messageId, reaction] as RPC.Params)) as Promise<T.U32>;
}
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
@@ -907,31 +890,11 @@ export class RawClient {
return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
}
public miscGetStickerFolder(accountId: T.U32): Promise<string> {
return (this._transport.request('misc_get_sticker_folder', [accountId] as RPC.Params)) as Promise<string>;
}
/**
* save a sticker to a collection/folder in the account's sticker folder
*/
public miscSaveSticker(accountId: T.U32, msgId: T.U32, collection: string): Promise<null> {
return (this._transport.request('misc_save_sticker', [accountId, msgId, collection] as RPC.Params)) as Promise<null>;
}
/**
* for desktop, get stickers from stickers folder,
* grouped by the collection/folder they are in.
*/
public miscGetStickers(accountId: T.U32): Promise<Record<string,(string)[]>> {
return (this._transport.request('misc_get_stickers', [accountId] as RPC.Params)) as Promise<Record<string,(string)[]>>;
}
/**
* Returns the messageid of the sent message
*/
public miscSendTextMessage(accountId: T.U32, chatId: T.U32, text: string): Promise<T.U32> {
return (this._transport.request('misc_send_text_message', [accountId, chatId, text] as RPC.Params)) as Promise<T.U32>;
public miscSendTextMessage(accountId: T.U32, text: string, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('misc_send_text_message', [accountId, text, chatId] as RPC.Params)) as Promise<T.U32>;
}

View File

@@ -1,199 +0,0 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
DC_CHAT_ID_ALLDONE_HINT = 7,
DC_CHAT_ID_ARCHIVED_LINK = 6,
DC_CHAT_ID_LAST_SPECIAL = 9,
DC_CHAT_ID_TRASH = 3,
DC_CHAT_TYPE_BROADCAST = 160,
DC_CHAT_TYPE_GROUP = 120,
DC_CHAT_TYPE_MAILINGLIST = 140,
DC_CHAT_TYPE_SINGLE = 100,
DC_CHAT_TYPE_UNDEFINED = 0,
DC_CONNECTIVITY_CONNECTED = 4000,
DC_CONNECTIVITY_CONNECTING = 2000,
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
DC_CONNECTIVITY_WORKING = 3000,
DC_CONTACT_ID_DEVICE = 5,
DC_CONTACT_ID_INFO = 2,
DC_CONTACT_ID_LAST_SPECIAL = 9,
DC_CONTACT_ID_SELF = 1,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
DC_GCL_ARCHIVED_ONLY = 1,
DC_GCL_FOR_FORWARDING = 8,
DC_GCL_NO_SPECIALS = 2,
DC_GCL_VERIFIED_ONLY = 1,
DC_GCM_ADDDAYMARKER = 1,
DC_GCM_INFO_ONLY = 2,
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,
DC_MEDIA_QUALITY_WORSE = 1,
DC_MSG_ID_DAYMARKER = 9,
DC_MSG_ID_LAST_SPECIAL = 9,
DC_MSG_ID_MARKER1 = 1,
DC_PROVIDER_STATUS_BROKEN = 3,
DC_PROVIDER_STATUS_OK = 1,
DC_PROVIDER_STATUS_PREPARATION = 2,
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
DC_SHOW_EMAILS_ALL = 2,
DC_SHOW_EMAILS_OFF = 0,
DC_SOCKET_AUTO = 0,
DC_SOCKET_PLAIN = 3,
DC_SOCKET_SSL = 1,
DC_SOCKET_STARTTLS = 2,
DC_STATE_IN_FRESH = 10,
DC_STATE_IN_NOTICED = 13,
DC_STATE_IN_SEEN = 16,
DC_STATE_OUT_DELIVERED = 26,
DC_STATE_OUT_DRAFT = 19,
DC_STATE_OUT_FAILED = 24,
DC_STATE_OUT_MDN_RCVD = 28,
DC_STATE_OUT_PENDING = 20,
DC_STATE_OUT_PREPARING = 18,
DC_STATE_UNDEFINED = 0,
DC_STR_AC_SETUP_MSG_BODY = 43,
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
DC_STR_ADD_MEMBER_BY_OTHER = 129,
DC_STR_ADD_MEMBER_BY_YOU = 128,
DC_STR_AEAP_ADDR_CHANGED = 122,
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
DC_STR_CONTACT_NOT_VERIFIED = 36,
DC_STR_CONTACT_SETUP_CHANGED = 37,
DC_STR_CONTACT_VERIFIED = 35,
DC_STR_DEVICE_MESSAGES = 68,
DC_STR_DEVICE_MESSAGES_HINT = 70,
DC_STR_DOWNLOAD_AVAILABILITY = 100,
DC_STR_DRAFT = 3,
DC_STR_E2E_AVAILABLE = 25,
DC_STR_E2E_PREFERRED = 34,
DC_STR_ENCRYPTEDMSG = 24,
DC_STR_ENCR_NONE = 28,
DC_STR_ENCR_TRANSP = 27,
DC_STR_EPHEMERAL_DAY = 79,
DC_STR_EPHEMERAL_DAYS = 95,
DC_STR_EPHEMERAL_DISABLED = 75,
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
DC_STR_EPHEMERAL_HOUR = 78,
DC_STR_EPHEMERAL_HOURS = 94,
DC_STR_EPHEMERAL_MINUTE = 77,
DC_STR_EPHEMERAL_MINUTES = 93,
DC_STR_EPHEMERAL_SECONDS = 76,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
DC_STR_EPHEMERAL_WEEK = 80,
DC_STR_EPHEMERAL_WEEKS = 96,
DC_STR_ERROR = 112,
DC_STR_ERROR_NO_NETWORK = 87,
DC_STR_FAILED_SENDING_TO = 74,
DC_STR_FILE = 12,
DC_STR_FINGERPRINTS = 30,
DC_STR_FORWARDED = 97,
DC_STR_GIF = 23,
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
DC_STR_GROUP_LEFT_BY_OTHER = 133,
DC_STR_GROUP_LEFT_BY_YOU = 132,
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
DC_STR_MESSAGES = 114,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
DC_STR_MSGADDMEMBER = 17,
DC_STR_MSGDELMEMBER = 18,
DC_STR_MSGGROUPLEFT = 19,
DC_STR_MSGGRPIMGCHANGED = 16,
DC_STR_MSGGRPIMGDELETED = 33,
DC_STR_MSGGRPNAME = 15,
DC_STR_MSGLOCATIONDISABLED = 65,
DC_STR_MSGLOCATIONENABLED = 64,
DC_STR_NOMESSAGES = 1,
DC_STR_NOT_CONNECTED = 121,
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
DC_STR_ONE_MOMENT = 106,
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
DC_STR_SECURE_JOIN_REPLIES = 118,
DC_STR_SECURE_JOIN_STARTED = 117,
DC_STR_SELF = 2,
DC_STR_SELF_DELETED_MSG_BODY = 91,
DC_STR_SENDING = 110,
DC_STR_SERVER_TURNED_OFF = 92,
DC_STR_SETUP_CONTACT_QR_DESC = 119,
DC_STR_STICKER = 67,
DC_STR_STORAGE_ON_DOMAIN = 105,
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
DC_STR_SYNC_MSG_BODY = 102,
DC_STR_SYNC_MSG_SUBJECT = 101,
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
DC_STR_UPDATING = 109,
DC_STR_VIDEO = 10,
DC_STR_VIDEOCHAT_INVITATION = 82,
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
DC_STR_VOICEMESSAGE = 7,
DC_STR_WELCOME_MESSAGE = 71,
DC_TEXT1_DRAFT = 1,
DC_TEXT1_SELF = 3,
DC_TEXT1_USERNAME = 2,
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
DC_VIDEOCHATTYPE_JITSI = 2,
DC_VIDEOCHATTYPE_UNKNOWN = 0,
}

View File

@@ -1,214 +1,3 @@
// AUTO-GENERATED by typescript-type-def
export type U32=number;
export type Usize=number;
export type Event=(({
/**
* The library-user may write an informational string to the log.
*
* This event should *not* be reported to the end-user using a popup or something like
* that.
*/
"type":"Info";}&{"msg":string;})|({
/**
* Emitted when SMTP connection is established and login was successful.
*/
"type":"SmtpConnected";}&{"msg":string;})|({
/**
* Emitted when IMAP connection is established and login was successful.
*/
"type":"ImapConnected";}&{"msg":string;})|({
/**
* Emitted when a message was successfully sent to the SMTP server.
*/
"type":"SmtpMessageSent";}&{"msg":string;})|({
/**
* Emitted when an IMAP message has been marked as deleted
*/
"type":"ImapMessageDeleted";}&{"msg":string;})|({
/**
* Emitted when an IMAP message has been moved
*/
"type":"ImapMessageMoved";}&{"msg":string;})|({
/**
* Emitted when an new file in the $BLOBDIR was created
*/
"type":"NewBlobFile";}&{"file":string;})|({
/**
* Emitted when an file in the $BLOBDIR was deleted
*/
"type":"DeletedBlobFile";}&{"file":string;})|({
/**
* The library-user should write a warning string to the log.
*
* This event should *not* be reported to the end-user using a popup or something like
* that.
*/
"type":"Warning";}&{"msg":string;})|({
/**
* The library-user should report an error to the end-user.
*
* As most things are asynchronous, things may go wrong at any time and the user
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
*
* However, for ongoing processes (eg. configure())
* or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
* it might be better to delay showing these events until the function has really
* failed (returned false). It should be sufficient to report only the *last* error
* in a messasge box then.
*/
"type":"Error";}&{"msg":string;})|({
/**
* An action cannot be performed because the user is not in the group.
* Reported eg. after a call to
* setChatName(), setChatProfileImage(),
* addContactToChat(), removeContactFromChat(),
* and messages sending functions.
*/
"type":"ErrorSelfNotInGroup";}&{"msg":string;})|({
/**
* Messages or chats changed. One or more messages or chats changed for various
* reasons in the database:
* - Messages sent, received or removed
* - Chats created, deleted or archived
* - A draft has been set
*
* `chatId` is set if only a single chat is affected by the changes, otherwise 0.
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
*/
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
/**
* Reactions for the message changed.
*/
"type":"ReactionsChanged";}&{"chatId":U32;"msgId":U32;"contactId":U32;})|({
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
*
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
*/
"type":"IncomingMsg";}&{"chatId":U32;"msgId":U32;})|({
/**
* Downloading a bunch of messages just finished. This is an experimental
* event to allow the UI to only show one notification per message bunch,
* instead of cluttering the user with many notifications.
*
* msg_ids contains the message ids.
*/
"type":"IncomingMsgBunch";}&{"msgIds":(U32)[];})|({
/**
* Messages were seen or noticed.
* chat id is always set.
*/
"type":"MsgsNoticed";}&{"chatId":U32;})|({
/**
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
* DC_STATE_OUT_DELIVERED, see `Message.state`.
*/
"type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({
/**
* A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_FAILED, see `Message.state`.
*/
"type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({
/**
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_MDN_RCVD, see `Message.state`.
*/
"type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.
* See setChatName(), setChatProfileImage(), addContactToChat()
* and removeContactFromChat().
*
* This event does not include ephemeral timer modification, which
* is a separate event.
*/
"type":"ChatModified";}&{"chatId":U32;})|({
/**
* Chat ephemeral timer changed.
*/
"type":"ChatEphemeralTimerModified";}&{"chatId":U32;"timer":U32;})|({
/**
* Contact(s) created, renamed, blocked or deleted.
*
* @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
*/
"type":"ContactsChanged";}&{"contactId":(U32|null);})|({
/**
* Location of one or more contact has changed.
*
* @param data1 (u32) contact_id of the contact for which the location has changed.
* If the locations of several contacts have been changed,
* this parameter is set to `None`.
*/
"type":"LocationChanged";}&{"contactId":(U32|null);})|({
/**
* Inform about the configuration progress started by configure().
*/
"type":"ConfigureProgress";}&{
/**
* Progress.
*
* 0=error, 1-999=progress in permille, 1000=success and done
*/
"progress":Usize;
/**
* Progress comment or error, something to display to the user.
*/
"comment":(string|null);})|({
/**
* Inform about the import/export progress started by imex().
*
* @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
* @param data2 0
*/
"type":"ImexProgress";}&{"progress":Usize;})|({
/**
* A file has been exported. A file has been written by imex().
* This event may be sent multiple times by a single call to imex().
*
* A typical purpose for a handler of this event may be to make the file public to some system
* services.
*
* @param data2 0
*/
"type":"ImexFileWritten";}&{"path":string;})|({
/**
* Progress information of a secure-join handshake from the view of the inviter
* (Alice, the person who shows the QR code).
*
* These events are typically sent after a joiner has scanned the QR code
* generated by getChatSecurejoinQrCodeSvg().
*
* @param data1 (int) ID of the contact that wants to join.
* @param data2 (int) Progress as:
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
* 1000=Protocol finished for this contact.
*/
"type":"SecurejoinInviterProgress";}&{"contactId":U32;"progress":Usize;})|({
/**
* Progress information of a secure-join handshake from the view of the joiner
* (Bob, the person who scans the QR code).
* The events are typically sent while secureJoin(), which
* may take some time, is executed.
* @param data1 (int) ID of the inviting contact.
* @param data2 (int) Progress as:
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
* (Bob has verified alice and waits until Alice does the same for him)
*/
"type":"SecurejoinJoinerProgress";}&{"contactId":U32;"progress":Usize;})|{
/**
* The connectivity to the server changed.
* This means that you should refresh the connectivity view
* and possibly the connectivtiy HTML; see getConnectivity() and
* getConnectivityHtml() for details.
*/
"type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({
/**
* Inform that a message containing a webxdc instance has been deleted
*/
"type":"WebxdcInstanceDeleted";}&{"msgId":U32;}));
export type EventTypeName=("Info"|"SmtpConnected"|"ImapConnected"|"SmtpMessageSent"|"ImapMessageDeleted"|"ImapMessageMoved"|"NewBlobFile"|"DeletedBlobFile"|"Warning"|"Error"|"ErrorSelfNotInGroup"|"MsgsChanged"|"IncomingMsg"|"MsgsNoticed"|"MsgDelivered"|"MsgFailed"|"MsgRead"|"ChatModified"|"ChatEphemeralTimerModified"|"ContactsChanged"|"LocationChanged"|"ConfigureProgress"|"ImexProgress"|"ImexFileWritten"|"SecurejoinInviterProgress"|"SecurejoinJoinerProgress"|"ConnectivityChanged"|"SelfavatarChanged"|"WebxdcStatusUpdate"|"WebXdInstanceDeleted");

View File

@@ -22,7 +22,7 @@ export type Contact={"address":string;"color":string;"authName":string;"status":
* the contact's last seen timestamp
*/
"lastSeen":I64;"wasSeenRecently":boolean;};
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":(string|null);};
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":string;};
/**
* cheaper version of fullchat, omits:
@@ -108,20 +108,6 @@ export type Viewtype=("Unknown"|
*/
"Webxdc");
export type MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;"viewType":Viewtype;}));
export type SystemMessageType=("Unknown"|"GroupNameChanged"|"GroupImageChanged"|"MemberAddedToGroup"|"MemberRemovedFromGroup"|"AutocryptSetupMessage"|"SecurejoinMessage"|"LocationStreamingEnabled"|"LocationOnly"|
/**
* Chat ephemeral message timer is changed.
*/
"EphemeralTimerChanged"|"ChatProtectionEnabled"|"ChatProtectionDisabled"|
/**
* Self-sent-message that contains only json used for multi-device-sync;
* if possible, we attach that to other messages as for locations.
*/
"MultiDeviceSync"|"WebxdcStatusUpdate"|
/**
* Webxdc info added with `info` set in `send_webxdc_status_update()`.
*/
"WebxdcInfoMessage");
export type I32=number;
export type WebxdcMessageInfo={
/**
@@ -161,28 +147,7 @@ export type WebxdcMessageInfo={
*/
"internetAccess":boolean;};
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
/**
* Structure representing all reactions to a particular message.
*/
export type Reactions=
/**
* Structure representing all reactions to a particular message.
*/
{
/**
* Map from a contact to it's reaction to message.
*/
"reactionsByContact":Record<U32,(string)[]>;
/**
* Unique reactions and their count
*/
"reactions":Record<string,U32>;};
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;
/**
* when is_info is true this describes what type of system message it is
*/
"systemMessageType":SystemMessageType;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);};
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;};
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
/**
* also known as summary_text1
@@ -195,4 +160,4 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
export type F64=number;
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,string,null,U32,Record<string,(string)[]>,U32,U32,string,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];

View File

@@ -1,8 +1,8 @@
{
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
"dependencies": {
"@deltachat/tiny-emitter": "3.0.0",
"isomorphic-ws": "^4.0.1",
"tiny-emitter": "git+https://github.com/Simon-Laux/tiny-emitter.git",
"yerpc": "^0.3.3"
},
"devDependencies": {
@@ -28,7 +28,7 @@
"main": "dist/deltachat.js",
"name": "@deltachat/jsonrpc-client",
"scripts": {
"build": "run-s generate-bindings extract-constants build:tsc build:bundle",
"build": "run-s generate-bindings build:tsc build:bundle",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"build:tsc": "tsc",
"docs": "typedoc --out docs deltachat.ts",
@@ -36,17 +36,16 @@
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
"example:start": "http-server .",
"extract-constants": "node ./scripts/generate-constants.js",
"generate-bindings": "cargo test",
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
"test:prepare": "cargo build --package deltachat-rpc-server --bin deltachat-rpc-server",
"test:prepare": "cargo build --features webserver --bin deltachat-jsonrpc-server",
"test:report-coverage": "node report_api_coverage.mjs",
"test:run": "mocha dist/test",
"test:run-coverage": "COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include 'dist/*' -r text -r html -r json mocha dist/test"
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.101.0"
"version": "1.96.0"
}

View File

@@ -1,54 +0,0 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from "fs";
import { resolve } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const data = [];
const header = resolve(__dirname, "../../../deltachat-ffi/deltachat.h");
console.log("Generating constants...");
const header_data = readFileSync(header, "UTF-8");
const regex = /^#define\s+(\w+)\s+(\w+)/gm;
let match;
while (null != (match = regex.exec(header_data))) {
const key = match[1];
const value = parseInt(match[2]);
if (!isNaN(value)) {
data.push({ key, value });
}
}
const constants = data
.filter(
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
)
.sort((lhs, rhs) => {
if (lhs.key < rhs.key) return -1;
else if (lhs.key > rhs.key) return 1;
return 0;
})
.filter(({ key }) => {
// filter out what we don't need it
return !(
key.startsWith("DC_EVENT_") ||
key.startsWith("DC_IMEX_") ||
key.startsWith("DC_CHAT_VISIBILITY") ||
key.startsWith("DC_DOWNLOAD") ||
key.startsWith("DC_INFO_") ||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
key.startsWith("DC_QR_")
);
})
.map((row) => {
return ` ${row.key}: ${row.value}`;
})
.join(",\n");
writeFileSync(
resolve(__dirname, "../generated/constants.ts"),
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
);

View File

@@ -1,57 +1,40 @@
import * as T from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { Event } from "../generated/events.js";
import { EventTypeName } from "../generated/events.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
import { TinyEmitter } from "tiny-emitter";
type DCWireEvent<T extends Event> = {
event: T;
export type DeltaChatEvent = {
id: EventTypeName;
contextId: number;
field1: any;
field2: any;
};
// export type Events = Record<
// Event["type"] | "ALL",
// (event: DeltaChatEvent<Event>) => void
// >;
type Events = { ALL: (accountId: number, event: Event) => void } & {
[Property in Event["type"]]: (
accountId: number,
event: Extract<Event, { type: Property }>
) => void;
};
type ContextEvents = { ALL: (event: Event) => void } & {
[Property in Event["type"]]: (
event: Extract<Event, { type: Property }>
) => void;
};
export type DcEvent = Event;
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>;
export type Events = Record<
EventTypeName | "ALL",
(event: DeltaChatEvent) => void
>;
export class BaseDeltaChat<
Transport extends BaseTransport<any>
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
private contextEmitters: TinyEmitter<Events>[] = [];
constructor(public transport: Transport) {
super();
this.rpc = new RawClient(this.transport);
this.transport.on("request", (request: Request) => {
const method = request.method;
if (method === "event") {
const event = request.params! as DCWireEvent<Event>;
this.emit(event.event.type, event.contextId, event.event as any);
this.emit("ALL", event.contextId, event.event as any);
const event = request.params! as DeltaChatEvent;
this.emit(event.id, event);
this.emit("ALL", event);
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(
event.event.type,
event.event as any
);
this.contextEmitters[event.contextId].emit("ALL", event.event);
this.contextEmitters[event.contextId].emit(event.id, event);
this.contextEmitters[event.contextId].emit("ALL", event);
}
}
});
@@ -87,39 +70,8 @@ export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
if (typeof opts === "string") opts = { url: opts };
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
else opts = { ...DEFAULT_OPTS };
const transport = new WebsocketTransport(opts.url);
const transport = new WebsocketTransport(opts.url)
super(transport);
this.opts = opts;
}
}
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
close() {}
constructor(input: any, output: any) {
const transport = new StdioTransport(input, output);
super(transport);
}
}
export class StdioTransport extends BaseTransport {
constructor(public input: any, public output: any) {
super();
var buffer = "";
this.output.on("data", (data: any) => {
buffer += data.toString();
while (buffer.includes("\n")) {
const n = buffer.indexOf("\n");
const line = buffer.substring(0, n);
const message = JSON.parse(line);
this._onmessage(message);
buffer = buffer.substring(n + 1);
}
});
}
_send(message: RPC.Message): void {
const serialized = JSON.stringify(message);
this.input.write(serialized + "\n");
}
}

View File

@@ -4,4 +4,3 @@ export * from "../generated/events.js";
export { RawClient } from "../generated/client.js";
export * from "./client.js";
export * as yerpc from "yerpc";
export { C } from "../generated/constants.js";

View File

@@ -2,7 +2,7 @@ import { strictEqual } from "assert";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import { StdioDeltaChat as DeltaChat } from "../deltachat.js";
import { DeltaChat } from "../deltachat.js";
import {
RpcServerHandle,
@@ -15,7 +15,9 @@ describe("basic tests", () => {
before(async () => {
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout)
// make sure server is up by the time we continue
await new Promise((res) => setTimeout(res, 100));
dc = new DeltaChat(serverHandle.url)
// dc.on("ALL", (event) => {
//console.log("event", event);
// });
@@ -82,21 +84,21 @@ describe("basic tests", () => {
accountId = await dc.rpc.addAccount();
});
it("should block and unblock contact", async function () {
const contactId = await dc.rpc.createContact(
const contactId = await dc.rpc.contactsCreateContact(
accountId,
"example@delta.chat",
null
);
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
.false;
await dc.rpc.blockContact(accountId, contactId);
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
await dc.rpc.contactsBlock(accountId, contactId);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
.true;
expect(await dc.rpc.getBlockedContacts(accountId)).to.have.length(1);
await dc.rpc.unblockContact(accountId, contactId);
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(1);
await dc.rpc.contactsUnblock(accountId, contactId);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
.false;
expect(await dc.rpc.getBlockedContacts(accountId)).to.have.length(0);
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(0);
});
});

View File

@@ -1,8 +1,12 @@
import { assert, expect } from "chai";
import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js";
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
import { DeltaChat, DeltaChatEvent, EventTypeName } from "../deltachat.js";
import {
RpcServerHandle,
createTempUser,
startServer,
} from "./test_base.js";
const EVENT_TIMEOUT = 20000;
const EVENT_TIMEOUT = 20000
describe("online tests", function () {
let serverHandle: RpcServerHandle;
@@ -12,7 +16,7 @@ describe("online tests", function () {
let accountId1: number, accountId2: number;
before(async function () {
this.timeout(12000);
this.timeout(12000)
if (!process.env.DCC_NEW_TMP_EMAIL) {
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
console.error(
@@ -27,10 +31,10 @@ describe("online tests", function () {
this.skip();
}
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
dc = new DeltaChat(serverHandle.url)
dc.on("ALL", (contextId, { type }) => {
if (type !== "Info") console.log(contextId, type);
dc.on("ALL", ({ id, contextId }) => {
if (id !== "Info") console.log(contextId, id);
});
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
@@ -70,7 +74,7 @@ describe("online tests", function () {
addr: account2.email,
mail_pw: account2.password,
});
await dc.rpc.configure(accountId2);
await dc.rpc.configure(accountId2)
accountsConfigured = true;
});
@@ -80,19 +84,19 @@ describe("online tests", function () {
}
this.timeout(15000);
const contactId = await dc.rpc.createContact(
const contactId = await dc.rpc.contactsCreateContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
await dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello");
const { chatId: chatIdOnAccountB } = await eventPromise;
await dc.rpc.miscSendTextMessage(accountId1, "Hello", chatId);
const { field1: chatIdOnAccountB } = await eventPromise;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.getMessageIds(
accountId2,
@@ -101,7 +105,7 @@ describe("online tests", function () {
);
expect(messageList).have.length(1);
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
const message = await dc.rpc.messageGetMessage(accountId2, messageList[0]);
expect(message.text).equal("Hello");
});
@@ -112,22 +116,22 @@ describe("online tests", function () {
this.timeout(10000);
// send message from A to B
const contactId = await dc.rpc.createContact(
const contactId = await dc.rpc.contactsCreateContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
dc.rpc.miscSendTextMessage(accountId1, "Hello2", chatId);
// wait for message from A
console.log("wait for message from A");
const event = await eventPromise;
const { chatId: chatIdOnAccountB } = event;
const { field1: chatIdOnAccountB } = event;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.getMessageIds(
@@ -135,7 +139,7 @@ describe("online tests", function () {
chatIdOnAccountB,
0
);
const message = await dc.rpc.getMessage(
const message = await dc.rpc.messageGetMessage(
accountId2,
messageList.reverse()[0]
);
@@ -145,14 +149,14 @@ describe("online tests", function () {
waitForEvent(dc, "MsgsChanged", accountId1),
waitForEvent(dc, "IncomingMsg", accountId1),
]);
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
dc.rpc.miscSendTextMessage(accountId2, "super secret message", chatId);
// Check if answer arives at A and if it is encrypted
await eventPromise2;
const messageId = (
await dc.rpc.getMessageIds(accountId1, chatId, 0)
).reverse()[0];
const message2 = await dc.rpc.getMessage(accountId1, messageId);
const message2 = await dc.rpc.messageGetMessage(accountId1, messageId);
expect(message2.text).equal("super secret message");
expect(message2.showPadlock).equal(true);
});
@@ -175,22 +179,22 @@ describe("online tests", function () {
});
});
async function waitForEvent<T extends DcEvent["type"]>(
async function waitForEvent(
dc: DeltaChat,
eventType: T,
eventType: EventTypeName,
accountId: number,
timeout: number = EVENT_TIMEOUT
): Promise<Extract<DcEvent, { type: T }>> {
): Promise<DeltaChatEvent> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error("Timeout reached before event came in")),
() => reject(new Error('Timeout reached before event came in')),
timeout
);
const callback = (contextId: number, event: DcEvent) => {
if (contextId == accountId) {
)
const callback = (event: DeltaChatEvent) => {
if (event.contextId == accountId) {
dc.off(eventType, callback);
clearTimeout(rejectTimeout);
resolve(event as any);
clearTimeout(rejectTimeout)
resolve(event);
}
};
dc.on(eventType, callback);

View File

@@ -1,38 +1,38 @@
import { tmpdir } from "os";
import { join, resolve } from "path";
import { mkdtemp, rm } from "fs/promises";
import { existsSync } from "fs";
import { spawn, exec } from "child_process";
import fetch from "node-fetch";
import { Readable, Writable } from "node:stream";
export const RPC_SERVER_PORT = 20808;
export type RpcServerHandle = {
stdin: Writable;
stdout: Readable;
close: () => Promise<void>;
};
url: string,
close: () => Promise<void>
}
export async function startServer(): Promise<RpcServerHandle> {
export async function startServer(port: number = RPC_SERVER_PORT): Promise<RpcServerHandle> {
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
const pathToServerBinary = resolve(
join(await getTargetDir(), "debug/deltachat-rpc-server")
);
const pathToServerBinary = resolve(join(await getTargetDir(), "debug/deltachat-jsonrpc-server"));
console.log('using server binary: ' + pathToServerBinary);
if (!existsSync(pathToServerBinary)) {
throw new Error(
"server executable does not exist, you need to build it first" +
"\nserver executable not found at " +
pathToServerBinary
);
}
const server = spawn(pathToServerBinary, {
cwd: tmpDir,
env: {
RUST_LOG: process.env.RUST_LOG || "info",
RUST_MIN_STACK: "8388608",
DC_PORT: '' + port
},
});
server.on("error", (err) => {
throw new Error(
"Failed to start server executable " +
pathToServerBinary +
", make sure you built it first."
);
});
let shouldClose = false;
server.on("exit", () => {
@@ -43,10 +43,12 @@ export async function startServer(): Promise<RpcServerHandle> {
});
server.stderr.pipe(process.stderr);
server.stdout.pipe(process.stdout)
const url = `ws://localhost:${port}/ws`
return {
stdin: server.stdin,
stdout: server.stdout,
url,
close: async () => {
shouldClose = true;
if (!server.kill()) {

View File

@@ -1,26 +0,0 @@
[package]
name = "deltachat-rpc-server"
version = "1.101.0"
description = "DeltaChat JSON-RPC server"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
readme = "README.md"
license = "MPL-2.0"
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
categories = ["cryptography", "std", "email"]
[[bin]]
name = "deltachat-rpc-server"
[dependencies]
deltachat-jsonrpc = { path = "../deltachat-jsonrpc" }
anyhow = "1"
env_logger = { version = "0.9.1" }
futures-lite = "1.12.0"
log = "0.4"
serde_json = "1.0.85"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.21.2", features = ["io-std"] }
yerpc = { version = "0.3.1", features = ["anyhow_expose"] }

View File

@@ -1,68 +0,0 @@
///! Delta Chat core RPC server.
///!
///! It speaks JSON Lines over stdio.
use std::path::PathBuf;
use anyhow::Result;
use deltachat_jsonrpc::api::events::event_to_json_rpc_notification;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use tokio::task::JoinHandle;
use yerpc::{RpcClient, RpcSession};
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
let accounts = Accounts::new(PathBuf::from(&path)).await?;
let events = accounts.get_event_emitter();
log::info!("Creating JSON-RPC API.");
let state = CommandApi::new(accounts);
let (client, mut out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), state);
// Events task converts core events to JSON-RPC notifications.
let events_task: JoinHandle<Result<()>> = tokio::spawn(async move {
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
client.send_notification("event", Some(event)).await?;
}
Ok(())
});
// Send task prints JSON responses to stdout.
let send_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
while let Some(message) = out_receiver.next().await {
let message = serde_json::to_string(&message)?;
log::trace!("RPC send {}", message);
println!("{}", message);
}
Ok(())
});
// Receiver task reads JSON requests from stdin.
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
let stdin = io::stdin();
let mut lines = BufReader::new(stdin).lines();
while let Some(message) = lines.next_line().await? {
log::trace!("RPC recv {}", message);
session.handle_incoming(&message).await;
}
log::info!("EOF reached on stdin");
Ok(())
});
// Wait for the end of stdin.
recv_task.await??;
// Shutdown the server.
send_task.abort();
events_task.abort();
Ok(())
}

View File

@@ -20,7 +20,6 @@ use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
use deltachat::tools::*;
@@ -408,7 +407,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
resend <msg-id>\n\
markseen <msg-id>\n\
delmsg <msg-id>\n\
react <msg-id> [<reaction>]\n\
===========================Contact commands==\n\
listcontacts [<query>]\n\
listverified [<query>]\n\
@@ -554,11 +552,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
sql::housekeeping(&context).await.ok_or_log(&context);
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" {
DC_GCL_ARCHIVED_ONLY
} else {
0
};
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
let time_start = std::time::SystemTime::now();
let chatlist = Chatlist::try_load(
&context,
@@ -1127,12 +1121,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ids[0] = MsgId::new(arg1.parse()?);
message::delete_msgs(&context, &ids).await?;
}
"react" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let msg_id = MsgId::new(arg1.parse()?);
let reaction = arg2;
send_reaction(&context, msg_id, reaction).await?;
}
"listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all(
&context,

View File

@@ -72,19 +72,6 @@ fn receive_event(event: EventType) {
))
);
}
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => {
info!(
"{}",
yellow.paint(format!(
"Received REACTIONS_CHANGED(chat_id={}, msg_id={}, contact_id={})",
chat_id, msg_id, contact_id
))
);
}
EventType::ContactsChanged(_) => {
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
}
@@ -221,7 +208,7 @@ const CHAT_COMMANDS: [&str; 36] = [
"accept",
"blockchat",
];
const MESSAGE_COMMANDS: [&str; 9] = [
const MESSAGE_COMMANDS: [&str; 8] = [
"listmsgs",
"msginfo",
"listfresh",
@@ -230,7 +217,6 @@ const MESSAGE_COMMANDS: [&str; 9] = [
"markseen",
"delmsg",
"download",
"react",
];
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",
@@ -446,7 +432,7 @@ async fn handle_cmd(
}
println!("{}", qr);
let output = Command::new("qrencode")
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.output()
.expect("failed to execute process");
io::stdout().write_all(&output.stdout).unwrap();

View File

@@ -42,7 +42,6 @@ module.exports = {
DC_EVENT_IMEX_FILE_WRITTEN: 2052,
DC_EVENT_IMEX_PROGRESS: 2051,
DC_EVENT_INCOMING_MSG: 2005,
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
DC_EVENT_INFO: 100,
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,
@@ -51,7 +50,6 @@ module.exports = {
DC_EVENT_MSG_FAILED: 2012,
DC_EVENT_MSG_READ: 2015,
DC_EVENT_NEW_BLOB_FILE: 150,
DC_EVENT_REACTIONS_CHANGED: 2001,
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
DC_EVENT_SELFAVATAR_CHANGED: 2110,
@@ -72,19 +70,8 @@ module.exports = {
DC_IMEX_EXPORT_SELF_KEYS: 1,
DC_IMEX_IMPORT_BACKUP: 12,
DC_IMEX_IMPORT_SELF_KEYS: 2,
DC_INFO_AUTOCRYPT_SETUP_MESSAGE: 6,
DC_INFO_EPHEMERAL_TIMER_CHANGED: 10,
DC_INFO_GROUP_IMAGE_CHANGED: 3,
DC_INFO_GROUP_NAME_CHANGED: 2,
DC_INFO_LOCATIONSTREAMING_ENABLED: 8,
DC_INFO_LOCATION_ONLY: 9,
DC_INFO_MEMBER_ADDED_TO_GROUP: 4,
DC_INFO_MEMBER_REMOVED_FROM_GROUP: 5,
DC_INFO_PROTECTION_DISABLED: 12,
DC_INFO_PROTECTION_ENABLED: 11,
DC_INFO_SECURE_JOIN_MESSAGE: 7,
DC_INFO_UNKNOWN: 0,
DC_INFO_WEBXDC_INFO_MESSAGE: 32,
DC_KEY_GEN_DEFAULT: 0,
DC_KEY_GEN_ED25519: 2,
DC_KEY_GEN_RSA2048: 1,

View File

@@ -14,9 +14,7 @@ module.exports = {
400: 'DC_EVENT_ERROR',
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',

View File

@@ -42,7 +42,6 @@ export enum C {
DC_EVENT_IMEX_FILE_WRITTEN = 2052,
DC_EVENT_IMEX_PROGRESS = 2051,
DC_EVENT_INCOMING_MSG = 2005,
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
DC_EVENT_INFO = 100,
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
@@ -51,7 +50,6 @@ export enum C {
DC_EVENT_MSG_FAILED = 2012,
DC_EVENT_MSG_READ = 2015,
DC_EVENT_NEW_BLOB_FILE = 150,
DC_EVENT_REACTIONS_CHANGED = 2001,
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
DC_EVENT_SELFAVATAR_CHANGED = 2110,
@@ -72,19 +70,8 @@ export enum C {
DC_IMEX_EXPORT_SELF_KEYS = 1,
DC_IMEX_IMPORT_BACKUP = 12,
DC_IMEX_IMPORT_SELF_KEYS = 2,
DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6,
DC_INFO_EPHEMERAL_TIMER_CHANGED = 10,
DC_INFO_GROUP_IMAGE_CHANGED = 3,
DC_INFO_GROUP_NAME_CHANGED = 2,
DC_INFO_LOCATIONSTREAMING_ENABLED = 8,
DC_INFO_LOCATION_ONLY = 9,
DC_INFO_MEMBER_ADDED_TO_GROUP = 4,
DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5,
DC_INFO_PROTECTION_DISABLED = 12,
DC_INFO_PROTECTION_ENABLED = 11,
DC_INFO_SECURE_JOIN_MESSAGE = 7,
DC_INFO_UNKNOWN = 0,
DC_INFO_WEBXDC_INFO_MESSAGE = 32,
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
@@ -295,9 +282,7 @@ export const EventId2EventName: { [key: number]: string } = {
400: 'DC_EVENT_ERROR',
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',

View File

@@ -89,11 +89,7 @@ describe('JSON RPC', function () {
const { dc } = DeltaChat.newTemporary()
let promise_resolve
const promise = new Promise((res, _rej) => {
promise_resolve = (response) => {
// ignore events
const answer = JSON.parse(response)
if (answer['method'] !== 'event') res(answer)
}
promise_resolve = res
})
dc.startJsonRpcHandler(promise_resolve)
dc.jsonRpcRequest(
@@ -110,7 +106,7 @@ describe('JSON RPC', function () {
id: 2,
result: [1],
},
await promise
JSON.parse(await promise)
)
dc.close()
})

View File

@@ -57,8 +57,8 @@
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
"test": "npm run test:lint && npm run test:mocha",
"test:lint": "npm run lint",
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail"
},
"types": "node/dist/index.d.ts",
"version": "1.101.0"
"version": "1.96.0"
}

View File

@@ -105,7 +105,7 @@ class FFIEventTracker:
yield self.get(timeout=timeout, check_error=check_error)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
rex = re.compile("^(?:{})$".format(event_name_regex))
rex = re.compile("(?:{}).*".format(event_name_regex))
for ev in self.iter_events(timeout=timeout, check_error=check_error):
if rex.match(ev.name):
return ev

View File

@@ -200,11 +200,11 @@ class TestOfflineContact:
assert ac1.delete_contact(contact1)
assert contact1 not in ac1.get_contacts()
def test_delete_referenced_contact_hides_contact(self, acfactory):
def test_get_contacts_and_delete_fails(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.com", name="some1")
msg = contact1.create_chat().send_text("one message")
assert ac1.delete_contact(contact1)
assert not ac1.delete_contact(contact1)
assert not msg.filemime
def test_create_chat_flexibility(self, acfactory):

View File

@@ -10,9 +10,6 @@ envlist =
commands =
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pip wheel . -w {toxworkdir}/wheelhouse --no-deps
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
passenv =
DCC_RS_DEV
DCC_RS_TARGET

View File

@@ -63,13 +63,8 @@ def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml", "deltachat-jsonrpc/Cargo.toml"]
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
toml_list = [
"Cargo.toml",
"deltachat-ffi/Cargo.toml",
"deltachat-jsonrpc/Cargo.toml",
"deltachat-rpc-server/Cargo.toml",
]
try:
opts = parser.parse_args()
except SystemExit:

10
spec.md
View File

@@ -450,16 +450,6 @@ This allows the receiver to show the time without knowing the file format.
Chat-Duration: 10000
# Reactions
Messengers MAY implement [RFC 9078](https://tools.ietf.org/html/rfc9078) reactions.
Received reaction should be interpreted as overwriting all previous reactions
received from the same contact.
This semantics is compatible to [XEP-0444](https://xmpp.org/extensions/xep-0444.html).
As an extension to RFC 9078, it is allowed to send empty reaction message,
in which case all previously sent reactions are retracted.
# Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual.

View File

@@ -1,753 +0,0 @@
//! Parsing and handling of the Authentication-Results header.
//! See the comment on [`handle_authres`] for more.
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fmt;
use anyhow::Result;
use mailparse::MailHeaderMap;
use mailparse::ParsedMail;
use once_cell::sync::Lazy;
use crate::config::Config;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::tools::time;
use crate::tools::EmailAddress;
/// `authres` is short for the Authentication-Results header, defined in
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
/// about whether DKIM and SPF passed.
///
/// To mitigate From forgery, we remember for each sending domain whether it is known
/// to have valid DKIM. If an email from such a domain comes with invalid DKIM,
/// we don't allow changing the autocrypt key.
///
/// See <https://github.com/deltachat/deltachat-core-rust/issues/3507>.
pub(crate) async fn handle_authres(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
message_time: i64,
) -> Result<DkimResults> {
let from_domain = match EmailAddress::new(from) {
Ok(email) => email.domain,
Err(e) => {
warn!(context, "invalid email {:#}", e);
// This email is invalid, but don't return an error, we still want to
// add a stub to the database so that it's not downloaded again
return Ok(DkimResults::default());
}
};
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
update_authservid_candidates(context, &authres).await?;
compute_dkim_results(context, authres, &from_domain, message_time).await
}
#[derive(Default, Debug)]
pub(crate) struct DkimResults {
/// Whether DKIM passed for this particular e-mail.
pub dkim_passed: bool,
/// Whether DKIM is known to work for e-mails coming from the sender's domain,
/// i.e. whether we expect DKIM to work.
pub dkim_should_work: bool,
/// Whether changing the public Autocrypt key should be allowed.
/// This is false if we expected DKIM to work (dkim_works=true),
/// but it failed now (dkim_passed=false).
pub allow_keychange: bool,
}
impl fmt::Display for DkimResults {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"DKIM Results: Passed={}, Works={}, Allow_Keychange={}",
self.dkim_passed, self.dkim_should_work, self.allow_keychange
)?;
if !self.allow_keychange {
write!(fmt, " KEYCHANGES NOT ALLOWED!!!!")?;
}
Ok(())
}
}
type AuthservId = String;
#[derive(Debug, PartialEq)]
enum DkimResult {
/// The header explicitly said that DKIM passed
Passed,
/// The header explicitly said that DKIM failed
Failed,
/// The header didn't say anything about DKIM; this might mean that it wasn't
/// checked, but it might also mean that it failed. This is because some providers
/// (e.g. ik.me, mail.ru, posteo.de) don't add `dkim=none` to their
/// Authentication-Results if there was no DKIM.
Nothing,
}
type ParsedAuthresHeaders = Vec<(AuthservId, DkimResult)>;
fn parse_authres_headers(
headers: &mailparse::headers::Headers<'_>,
from_domain: &str,
) -> ParsedAuthresHeaders {
let mut res = Vec::new();
for header_value in headers.get_all_values(HeaderDef::AuthenticationResults.into()) {
let header_value = remove_comments(&header_value);
if let Some(mut authserv_id) = header_value.split(';').next() {
if authserv_id.contains(char::is_whitespace) || authserv_id.is_empty() {
// Outlook violates the RFC by not adding an authserv-id at all, which we notice
// because there is whitespace in the first identifier before the ';'.
// Authentication-Results-parsing still works securely because they remove incoming
// Authentication-Results headers.
// We just use an arbitrary authserv-id, it will work for Outlook, and in general,
// with providers not implementing the RFC correctly, someone can trick us
// into thinking that an incoming email is DKIM-correct, anyway.
// The most important thing here is that we have some valid `authserv_id`.
authserv_id = "invalidAuthservId";
}
let dkim_passed = parse_one_authres_header(&header_value, from_domain);
res.push((authserv_id.to_string(), dkim_passed));
}
}
res
}
/// The headers can contain comments that look like this:
/// ```text
/// Authentication-Results: (this is a comment) gmx.net; (another; comment) dkim=pass;
/// ```
fn remove_comments(header: &str) -> Cow<'_, str> {
// In Pomsky, this is:
// "(" Codepoint* lazy ")"
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
static RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
RE.replace_all(header, " ")
}
/// Parses a single Authentication-Results header, like:
///
/// ```text
/// Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
/// ```
fn parse_one_authres_header(header_value: &str, from_domain: &str) -> DkimResult {
if let Some((before_dkim_part, dkim_to_end)) = header_value.split_once("dkim=") {
// Check that the character right before `dkim=` is a space or a tab
// so that we wouldn't e.g. mistake `notdkim=pass` for `dkim=pass`
if before_dkim_part.ends_with(' ') || before_dkim_part.ends_with('\t') {
let dkim_part = dkim_to_end.split(';').next().unwrap_or_default();
let dkim_parts: Vec<_> = dkim_part.split_whitespace().collect();
if let Some(&"pass") = dkim_parts.first() {
// DKIM headers contain a header.d or header.i field
// that says which domain signed. We have to check ourselves
// that this is the same domain as in the From header.
let header_d: &str = &format!("header.d={}", &from_domain);
let header_i: &str = &format!("header.i=@{}", &from_domain);
if dkim_parts.contains(&header_d) || dkim_parts.contains(&header_i) {
// We have found a `dkim=pass` header!
return DkimResult::Passed;
}
} else {
// dkim=fail, dkim=none, ...
return DkimResult::Failed;
}
}
}
DkimResult::Nothing
}
/// ## About authserv-ids
///
/// After having checked DKIM, our email server adds an Authentication-Results header.
///
/// Now, an attacker could just add an Authentication-Results header that says dkim=pass
/// in order to make us think that DKIM was correct in their From-forged email.
///
/// In order to prevent this, each email server adds its authserv-id to the
/// Authentication-Results header, e.g. Testrun's authserv-id is `testrun.org`, Gmail's
/// is `mx.google.com`. When Testrun gets a mail delivered from outside, it will then
/// remove any Authentication-Results headers whose authserv-id is also `testrun.org`.
///
/// We need to somehow find out the authserv-id(s) of our email server, so that
/// we can use the Authentication-Results with the right authserv-id.
///
/// ## What this function does
///
/// When receiving an email, this function is called and updates the candidates for
/// our server's authserv-id, i.e. what we think our server's authserv-id is.
///
/// Usually, every incoming email has Authentication-Results with our server's
/// authserv-id, so, the intersection of the existing authserv-ids and the incoming
/// authserv-ids for our server's authserv-id is a good guess for our server's
/// authserv-id. When this intersection is empty, we assume that the authserv-id has
/// changed and start over with the new authserv-ids.
///
/// See [`handle_authres`].
async fn update_authservid_candidates(
context: &Context,
authres: &ParsedAuthresHeaders,
) -> Result<()> {
let mut new_ids: BTreeSet<&str> = authres
.iter()
.map(|(authserv_id, _dkim_passed)| authserv_id.as_str())
.collect();
if new_ids.is_empty() {
// The incoming message doesn't contain any authentication results, maybe it's a
// self-sent or a mailer-daemon message
return Ok(());
}
let old_config = context.get_config(Config::AuthservIdCandidates).await?;
let old_ids = parse_authservid_candidates_config(&old_config);
let intersection: BTreeSet<&str> = old_ids.intersection(&new_ids).copied().collect();
if !intersection.is_empty() {
new_ids = intersection;
}
// If there were no AuthservIdCandidates previously, just start with
// the ones from the incoming email
if old_ids != new_ids {
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
context
.set_config(Config::AuthservIdCandidates, Some(&new_config))
.await?;
// Updating the authservid candidates may mean that we now consider
// emails as "failed" which "passed" previously, so we need to
// reset our expectation which DKIMs work.
clear_dkim_works(context).await?
}
Ok(())
}
/// Use the parsed authres and the authservid candidates to compute whether DKIM passed
/// and whether a keychange should be allowed.
///
/// We track in the `sending_domains` table whether we get positive Authentication-Results
/// for mails from a contact (meaning that their provider properly authenticates against
/// our provider).
///
/// Once a contact is known to come with positive Authentication-Resutls (dkim: pass),
/// we don't accept Autocrypt key changes if they come with negative Authentication-Results.
async fn compute_dkim_results(
context: &Context,
mut authres: ParsedAuthresHeaders,
from_domain: &str,
message_time: i64,
) -> Result<DkimResults> {
let mut dkim_passed = false;
let ids_config = context.get_config(Config::AuthservIdCandidates).await?;
let ids = parse_authservid_candidates_config(&ids_config);
// Remove all foreign authentication results
authres.retain(|(authserv_id, _dkim_passed)| ids.contains(authserv_id.as_str()));
if authres.is_empty() {
// If the authentication results are empty, then our provider doesn't add them
// and an attacker could just add their own Authentication-Results, making us
// think that DKIM passed. So, in this case, we can as well assume that DKIM passed.
dkim_passed = true;
} else {
for (_authserv_id, current_dkim_passed) in authres {
match current_dkim_passed {
DkimResult::Passed => {
dkim_passed = true;
break;
}
DkimResult::Failed => {
dkim_passed = false;
break;
}
DkimResult::Nothing => {
// Continue looking for an Authentication-Results header
}
}
}
}
let last_working_timestamp = dkim_works_timestamp(context, from_domain).await?;
let mut dkim_should_work = dkim_should_work(last_working_timestamp)?;
if message_time > last_working_timestamp && dkim_passed {
set_dkim_works_timestamp(context, from_domain, message_time).await?;
dkim_should_work = true;
}
Ok(DkimResults {
dkim_passed,
dkim_should_work,
allow_keychange: dkim_passed || !dkim_should_work,
})
}
/// Whether DKIM in emails from this domain should be considered to work.
fn dkim_should_work(last_working_timestamp: i64) -> Result<bool> {
// When we get an email with valid DKIM-Authentication-Results,
// then we assume that DKIM works for 30 days from this time on.
let should_work_until = last_working_timestamp + 3600 * 24 * 30;
let dkim_ever_worked = last_working_timestamp > 0;
// We're using time() here and not the time when the message
// claims to have been sent (passed around as `message_time`)
// because otherwise an attacker could just put a time way
// in the future into the `Date` header and then we would
// assume that DKIM doesn't have to be valid anymore.
let dkim_should_work_now = should_work_until > time();
Ok(dkim_ever_worked && dkim_should_work_now)
}
async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i64, anyhow::Error> {
let last_working_timestamp: i64 = context
.sql
.query_get_value(
"SELECT dkim_works FROM sending_domains WHERE domain=?",
paramsv![from_domain],
)
.await?
.unwrap_or(0);
Ok(last_working_timestamp)
}
async fn set_dkim_works_timestamp(
context: &Context,
from_domain: &str,
timestamp: i64,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
paramsv![from_domain, timestamp],
)
.await?;
Ok(())
}
async fn clear_dkim_works(context: &Context) -> Result<()> {
context
.sql
.execute("DELETE FROM sending_domains", paramsv![])
.await?;
Ok(())
}
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
config
.as_deref()
.map(|c| c.split_whitespace().collect())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use std::time::Duration;
use tokio::fs;
use tokio::io::AsyncReadExt;
use super::*;
use crate::aheader::EncryptPreference;
use crate::e2ee;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::securejoin::get_securejoin_qr;
use crate::securejoin::join_securejoin;
use crate::test_utils;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools;
#[test]
fn test_remove_comments() {
let header = "Authentication-Results: mx3.messagingengine.com;
dkim=pass (1024-bit rsa key sha256) header.d=riseup.net;"
.to_string();
assert_eq!(
remove_comments(&header),
"Authentication-Results: mx3.messagingengine.com;
dkim=pass header.d=riseup.net;"
);
let header = ") aaa (".to_string();
assert_eq!(remove_comments(&header), ") aaa (");
let header = "((something weird) no comment".to_string();
assert_eq!(remove_comments(&header), " no comment");
let header = "🎉(🎉(🎉))🎉(".to_string();
assert_eq!(remove_comments(&header), "🎉 )🎉(");
// Comments are allowed to include whitespace
let header = "(com\n\t\r\nment) no comment (comment)".to_string();
assert_eq!(remove_comments(&header), " no comment ");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_authentication_results() -> Result<()> {
let t = TestContext::new().await;
t.configure_addr("alice@gmx.net").await;
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Passed),
("gmx.net".to_string(), DkimResult::Nothing)
]
);
let bytes = b"Authentication-Results: gmx.net; notdkim=pass header.i=@slack.com
Authentication-Results: gmx.net; notdkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Nothing),
("gmx.net".to_string(), DkimResult::Nothing)
]
);
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(actual, vec![("gmx.net".to_string(), DkimResult::Nothing)],);
// Weird Authentication-Results from Outlook without an authserv-id
let bytes = b"Authentication-Results: spf=pass (sender IP is 40.92.73.85)
smtp.mailfrom=hotmail.com; dkim=pass (signature was verified)
header.d=hotmail.com;dmarc=pass action=none
header.from=hotmail.com;compauth=pass reason=100";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "hotmail.com");
// At this point, the most important thing to test is that there are no
// authserv-ids with whitespace in them.
assert_eq!(
actual,
vec![("invalidAuthservId".to_string(), DkimResult::Passed)]
);
let bytes = b"Authentication-Results: gmx.net; dkim=none header.i=@slack.com
Authentication-Results: gmx.net; dkim=pass header.i=@slack.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Failed),
("gmx.net".to_string(), DkimResult::Passed)
]
);
// ';' in comments
let bytes = b"Authentication-Results: mx1.riseup.net;
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.a=rsa-sha256 header.s=mail header.b=avNJu6sw;
dkim-atps=neutral";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "yandex.ru");
assert_eq!(
actual,
vec![("mx1.riseup.net".to_string(), DkimResult::Passed)]
);
let bytes = br#"Authentication-Results: box.hispanilandia.net;
dkim=fail reason="signature verification failed" (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="kqh3WUKq";
dkim-atps=neutral
Authentication-Results: box.hispanilandia.net; dmarc=pass (p=quarantine dis=none) header.from=disroot.org
Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@disroot.org"#;
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "disroot.org");
assert_eq!(
actual,
vec![
("box.hispanilandia.net".to_string(), DkimResult::Failed),
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
]
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_update_authservid_candidates() -> Result<()> {
let t = TestContext::new_alice().await;
update_authservid_candidates_test(&t, &["mx3.messagingengine.com"]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx3.messagingengine.com");
// "mx4.messagingengine.com" seems to be the new authserv-id, DC should accept it
update_authservid_candidates_test(&t, &["mx4.messagingengine.com"]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
// A message without any Authentication-Results headers shouldn't remove all
// candidates since it could be a mailer-daemon message or so
update_authservid_candidates_test(&t, &[]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
update_authservid_candidates_test(&t, &["mx4.messagingengine.com", "someotherdomain.com"])
.await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
Ok(())
}
/// Calls update_authservid_candidates(), meant for using in a test.
///
/// update_authservid_candidates() only looks at the keys of its
/// `authentication_results` parameter. So, this function takes `incoming_ids`
/// and adds some AuthenticationResults to get the HashMap we need.
async fn update_authservid_candidates_test(context: &Context, incoming_ids: &[&str]) {
let v = incoming_ids
.iter()
.map(|id| (id.to_string(), DkimResult::Passed))
.collect();
update_authservid_candidates(context, &v).await.unwrap()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_realworld_authentication_results() -> Result<()> {
let mut test_failed = false;
let dir = tools::read_dir("test-data/message/dkimchecks-2022-09-28/".as_ref())
.await
.unwrap();
let mut bytes = Vec::new();
for entry in dir {
if !entry.file_type().await.unwrap().is_dir() {
continue;
}
let self_addr = entry.file_name().into_string().unwrap();
let self_domain = EmailAddress::new(&self_addr).unwrap().domain;
let authres_parsing_works = [
"ik.me",
"web.de",
"posteo.de",
"gmail.com",
"hotmail.com",
"mail.ru",
"aol.com",
"yahoo.com",
"icloud.com",
"fastmail.com",
"mail.de",
"outlook.com",
"gmx.de",
"testrun.org",
]
.contains(&self_domain.as_str());
let t = TestContext::new().await;
t.configure_addr(&self_addr).await;
if !authres_parsing_works {
println!("========= Receiving as {} =========", &self_addr);
}
// Simulate receiving all emails once, so that we have the correct authserv-ids
let mut dir = tools::read_dir(&entry.path()).await.unwrap();
// The ordering in which the emails are received can matter;
// the test _should_ pass for every ordering.
dir.sort_by_key(|d| d.file_name());
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::thread_rng());
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;
bytes.clear();
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers)[0].addr;
let res = handle_authres(&t, &mail, from, time()).await?;
assert!(res.allow_keychange);
}
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;
bytes.clear();
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers)[0].addr;
let res = handle_authres(&t, &mail, from, time()).await?;
if !res.allow_keychange {
println!(
"!!!!!! FAILURE Receiving {:?}, keychange is not allowed !!!!!!",
entry.path()
);
test_failed = true;
}
let from_domain = EmailAddress::new(from).unwrap().domain;
assert_eq!(
res.dkim_should_work,
dkim_should_work(dkim_works_timestamp(&t, &from_domain).await?)?
);
assert_eq!(res.dkim_passed, res.dkim_should_work);
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
// These are (fictional) forged emails where the attacker added a fake
// Authentication-Results before sending the email
&& from != "forged-authres-added@example.com"
// Other forged emails
&& !from.starts_with("forged");
if res.dkim_passed != expected_result {
if authres_parsing_works {
println!(
"!!!!!! FAILURE Receiving {:?}, order {:#?} wrong result: !!!!!!",
entry.path(),
dir.iter().map(|e| e.file_name()).collect::<Vec<_>>()
);
test_failed = true;
}
println!("From {}: {}", from_domain, res.dkim_passed);
}
}
}
assert!(!test_failed);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_handle_authres() {
let t = TestContext::new().await;
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
// return an Err because this would prevent the message from being added
// to the database and downloaded again and again
let bytes = b"Authentication-Results: dkim=";
let mail = mailparse::parse_mail(bytes).unwrap();
handle_authres(&t, &mail, "invalidfrom.com", time())
.await
.unwrap();
}
#[ignore = "Disallowing keychanges is disabled for now"]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_handle_authres_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Bob sends Alice a message, so she gets his key
tcm.send_recv_accept(&bob, &alice, "Hi").await;
// We don't need bob anymore, let's make sure it's not accidentally used
drop(bob);
// Assume Alice receives an email from bob@example.net with
// correct DKIM -> `set_dkim_works()` was called
set_dkim_works_timestamp(&alice, "example.net", time()).await?;
// And Alice knows her server's authserv-id
alice
.set_config(Config::AuthservIdCandidates, Some("example.org"))
.await?;
tcm.section("An attacker, bob2, sends a from-forged email to Alice!");
// Sleep to make sure key reset is ignored because of DKIM failure
// and not because reordering is suspected.
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let bob2 = tcm.unconfigured().await;
bob2.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob2).await?;
let chat = bob2.create_chat(&alice).await;
let mut sent = bob2
.send_text(chat.id, "Please send me lots of money")
.await;
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
let received = alice.recv_msg(&sent).await;
// Assert that the error tells the user about the problem
assert!(received.error.unwrap().contains("DKIM failed"));
let bob_state = Peerstate::from_addr(&alice, "bob@example.net")
.await?
.unwrap();
// Encryption preference is still mutual.
assert_eq!(bob_state.prefer_encrypt, EncryptPreference::Mutual);
// Also check that the keypair was not changed
assert_eq!(
bob_state.public_key.unwrap(),
test_utils::bob_keypair().public
);
// Since Alice didn't change the key, Bob can't read her message
let received = tcm
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
.await;
assert!(!received.text.as_ref().unwrap().contains("1234"));
assert!(received.error.is_some());
tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working.");
tcm.section("To fix the key problems, Bob scans Alice's QR code.");
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
join_securejoin(&bob2.ctx, &qr).await.unwrap();
loop {
if let Some(mut sent) = bob2.pop_sent_msg_opt(Duration::ZERO).await {
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
alice.recv_msg(&sent).await;
} else if let Some(sent) = alice.pop_sent_msg_opt(Duration::ZERO).await {
bob2.recv_msg(&sent).await;
} else {
break;
}
}
// Unfortunately, securejoin currently doesn't work with authres-checking,
// so these checks would fail:
// let contact_bob = alice.add_or_lookup_contact(&bob2).await;
// assert_eq!(
// contact_bob.is_verified(&alice.ctx).await.unwrap(),
// VerifiedStatus::BidirectVerified
// );
// let contact_alice = bob2.add_or_lookup_contact(&alice).await;
// assert_eq!(
// contact_alice.is_verified(&bob2.ctx).await.unwrap(),
// VerifiedStatus::BidirectVerified
// );
// // Bob can read Alice's messages again
// let received = tcm
// .try_send_recv(&alice, &bob2, "Can you read this again?")
// .await;
// assert_eq!(received.text.as_ref().unwrap(), "Can you read this again?");
// assert!(received.error.is_none());
Ok(())
}
}

View File

@@ -1130,8 +1130,8 @@ impl Chat {
}
/// Returns mailing list address where messages are sent to.
pub fn get_mailinglist_addr(&self) -> Option<&str> {
self.param.get(Param::ListPost)
pub fn get_mailinglist_addr(&self) -> &str {
self.param.get(Param::ListPost).unwrap_or_default()
}
/// Returns profile image path for the chat.
@@ -2439,7 +2439,7 @@ pub async fn get_chat_media(
AND hidden=0
ORDER BY timestamp, id;",
paramsv![
chat_id.is_none(),
if chat_id.is_none() { 1i32 } else { 0i32 },
chat_id.unwrap_or_else(|| ChatId::new(0)),
msg_type,
if msg_type2 != Viewtype::Unknown {
@@ -3702,11 +3702,11 @@ mod tests {
// create group and sync it to the second device
let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?;
let sent = a1.send_text(a1_chat_id, "ho!").await;
a1.send_text(a1_chat_id, "ho!").await;
let a1_msg = a1.get_last_msg().await;
let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?;
let a2_msg = a2.recv_msg(&sent).await;
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
let a2_chat_id = a2_msg.chat_id;
let a2_chat = Chat::load_from_db(&a2, a2_chat_id).await?;

View File

@@ -184,12 +184,6 @@ pub enum Config {
/// In a future versions, this switch may be removed.
#[strum(props(default = "0"))]
SendSyncMsgs,
/// Space-separated list of all the authserv-ids which we believe
/// may be the one of our email server.
///
/// See `crate::authres::update_authservid_candidates`.
AuthservIdCandidates,
}
impl Context {

View File

@@ -1,20 +1,14 @@
//! Contacts module
use std::cmp::Reverse;
use std::collections::BinaryHeap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::task;
use tokio::time::{timeout, Duration};
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
@@ -30,7 +24,7 @@ use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::sql::{self, params_iter};
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, EmailAddress};
use crate::tools::{get_abs_path, improve_single_line_input, time, EmailAddress};
use crate::{chat, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -40,9 +34,7 @@ const SEEN_RECENTLY_SECONDS: i64 = 600;
///
/// Some contact IDs are reserved to identify special contacts. This
/// type can represent both the special as well as normal contacts.
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ContactId(u32);
impl ContactId {
@@ -229,7 +221,7 @@ pub enum Origin {
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
SecurejoinJoined = 0x0200_0000,
/// contact added manually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
/// contact added mannually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
ManuallyCreated = 0x0400_0000,
}
@@ -455,7 +447,7 @@ impl Contact {
/// - "row_authname": name as authorized from a contact, set only through a From-header
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
pub(crate) async fn add_or_lookup(
context: &Context,
name: &str,
@@ -944,36 +936,43 @@ impl Contact {
Ok(ret)
}
/// Delete a contact so that it disappears from the corresponding lists.
/// Depending on whether there are ongoing chats, deletion is done by physical deletion or hiding.
/// The contact is deleted from the local device.
/// Delete a contact. The contact is deleted from the local device. It may happen that this is not
/// possible as the contact is in use. In this case, the contact can be blocked.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn delete(context: &Context, contact_id: ContactId) -> Result<()> {
ensure!(!contact_id.is_special(), "Can not delete special contact");
context
let count_chats = context
.sql
.transaction(move |transaction| {
// make sure, the transaction starts with a write command and becomes EXCLUSIVE by that -
// upgrading later may be impossible by races.
let deleted_contacts = transaction.execute(
"DELETE FROM contacts WHERE id=?
AND (SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?)=0;",
paramsv![contact_id, contact_id],
)?;
if deleted_contacts == 0 {
transaction.execute(
"UPDATE contacts SET origin=? WHERE id=?;",
paramsv![Origin::Hidden, contact_id],
)?;
}
Ok(())
})
.count(
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
paramsv![contact_id],
)
.await?;
context.emit_event(EventType::ContactsChanged(None));
Ok(())
if count_chats == 0 {
match context
.sql
.execute("DELETE FROM contacts WHERE id=?;", paramsv![contact_id])
.await
{
Ok(_) => {
context.emit_event(EventType::ContactsChanged(None));
return Ok(());
}
Err(err) => {
error!(context, "delete_contact {} failed ({})", contact_id, err);
return Err(err);
}
}
}
info!(
context,
"could not delete contact {}, there are {} chats with it", contact_id, count_chats
);
bail!("Could not delete contact with ongoing chats");
}
/// Get a single contact object. For a list, see eg. get_contacts().
@@ -1369,17 +1368,13 @@ pub(crate) async fn update_last_seen(
"Can not update special contact last seen timestamp"
);
if context
context
.sql
.execute(
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
paramsv![timestamp, contact_id],
)
.await?
> 0
{
context.interrupt_recently_seen(contact_id, timestamp).await;
}
.await?;
Ok(())
}
@@ -1446,132 +1441,6 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
.collect()
}
#[derive(Debug)]
pub(crate) struct RecentlySeenInterrupt {
contact_id: ContactId,
timestamp: i64,
}
#[derive(Debug)]
pub(crate) struct RecentlySeenLoop {
/// Task running "recently seen" loop.
handle: task::JoinHandle<()>,
interrupt_send: Sender<RecentlySeenInterrupt>,
}
impl RecentlySeenLoop {
pub(crate) fn new(context: Context) -> Self {
let (interrupt_send, interrupt_recv) = channel::bounded(1);
let handle = task::spawn(async move { Self::run(context, interrupt_recv).await });
Self {
handle,
interrupt_send,
}
}
async fn run(context: Context, interrupt: Receiver<RecentlySeenInterrupt>) {
type MyHeapElem = (Reverse<i64>, ContactId);
// Priority contains all recently seen sorted by the timestamp
// when they become not recently seen.
//
// Initialize with contacts which are currently seen, but will
// become unseen in the future.
let mut unseen_queue: BinaryHeap<MyHeapElem> = context
.sql
.query_map(
"SELECT id, last_seen FROM contacts
WHERE last_seen > ?",
paramsv![time() - SEEN_RECENTLY_SECONDS],
|row| {
let contact_id: ContactId = row.get("id")?;
let last_seen: i64 = row.get("last_seen")?;
Ok((Reverse(last_seen + SEEN_RECENTLY_SECONDS), contact_id))
},
|rows| {
rows.collect::<std::result::Result<BinaryHeap<MyHeapElem>, _>>()
.map_err(Into::into)
},
)
.await
.unwrap_or_default();
loop {
let now = SystemTime::now();
let (until, contact_id) =
if let Some((Reverse(timestamp), contact_id)) = unseen_queue.peek() {
(
UNIX_EPOCH
+ Duration::from_secs((*timestamp).try_into().unwrap_or(u64::MAX))
+ Duration::from_secs(1),
Some(contact_id),
)
} else {
// Sleep for 24 hours.
(now + Duration::from_secs(86400), None)
};
if let Ok(duration) = until.duration_since(now) {
info!(
context,
"Recently seen loop waiting for {} or interrupt",
duration_to_str(duration)
);
match timeout(duration, interrupt.recv()).await {
Err(_) => {
// Timeout, notify about contact.
if let Some(contact_id) = contact_id {
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
unseen_queue.pop();
}
}
Ok(Err(err)) => {
warn!(
context,
"Error receiving an interruption in recently seen loop: {}", err
);
}
Ok(Ok(RecentlySeenInterrupt {
contact_id,
timestamp,
})) => {
// Received an interrupt.
unseen_queue.push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id));
}
}
} else {
info!(
context,
"Recently seen loop is not waiting, event is already due."
);
// Event is already in the past.
if let Some(contact_id) = contact_id {
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
}
unseen_queue.pop();
}
}
}
pub(crate) fn interrupt(&self, contact_id: ContactId, timestamp: i64) {
self.interrupt_send
.try_send(RecentlySeenInterrupt {
contact_id,
timestamp,
})
.ok();
}
pub(crate) fn abort(self) {
self.handle.abort();
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1731,7 +1600,7 @@ mod tests {
);
assert_eq!(Contact::add_address_book(&t, book).await.unwrap(), 4);
// check first added contact, this modifies authname because it is empty
// check first added contact, this modifies authname beacuse it is empty
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "bla foo", "one@eins.org", Origin::IncomingUnknownTo)
.await
@@ -1939,70 +1808,21 @@ mod tests {
// Create Bob contact
let (contact_id, _) =
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
.await?;
.await
.unwrap();
let chat = alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
.len(),
1
);
// If a contact has ongoing chats, contact is only hidden on deletion
Contact::delete(&alice, contact_id).await?;
let contact = Contact::load_from_db(&alice, contact_id).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
.len(),
0
);
// Can't delete a contact with ongoing chats.
assert!(Contact::delete(&alice, contact_id).await.is_err());
// Delete chat.
chat.get_id().delete(&alice).await?;
// Can delete contact physically now
// Can delete contact now.
Contact::delete(&alice, contact_id).await?;
assert!(Contact::load_from_db(&alice, contact_id).await.is_err());
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
.len(),
0
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_and_recreate_contact() -> Result<()> {
let t = TestContext::new_alice().await;
// test recreation after physical deletion
let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
Contact::delete(&t, contact_id1).await?;
assert!(Contact::load_from_db(&t, contact_id1).await.is_err());
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_ne!(contact_id2, contact_id1);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
// test recreation after hiding
t.create_chat_with_contact("Foo", "foo@bar.de").await;
Contact::delete(&t, contact_id2).await?;
let contact = Contact::load_from_db(&t, contact_id2).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?;
let contact = Contact::load_from_db(&t, contact_id3).await?;
assert_eq!(contact.origin, Origin::ManuallyCreated);
assert_eq!(contact_id3, contact_id2);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
Ok(())
}

View File

@@ -26,156 +26,6 @@ use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::tools::{duration_to_str, time};
/// Builder for the [`Context`].
///
/// Many arguments to the [`Context`] are kind of optional and only needed to handle
/// multiple contexts, for which the [account manager](crate::accounts::Accounts) should be
/// used. This builder makes creating a new context simpler, especially for the
/// standalone-context case.
///
/// # Examples
///
/// Creating a new unecrypted database:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
/// # rt.block_on(async move {
/// use deltachat::context::ContextBuilder;
///
/// let dir = tempfile::tempdir().unwrap();
/// let context = ContextBuilder::new(dir.path().join("db"))
/// .open()
/// .await
/// .unwrap();
/// drop(context);
/// # });
/// ```
///
/// To use an encrypted database provide a password. If the database does not yet exist it
/// will be created:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
/// # rt.block_on(async move {
/// use deltachat::context::ContextBuilder;
///
/// let dir = tempfile::tempdir().unwrap();
/// let context = ContextBuilder::new(dir.path().join("db"))
/// .with_password("secret".into())
/// .open()
/// .await
/// .unwrap();
/// drop(context);
/// # });
/// ```
#[derive(Clone, Debug)]
pub struct ContextBuilder {
dbfile: PathBuf,
id: u32,
events: Events,
stock_strings: StockStrings,
password: Option<String>,
}
impl ContextBuilder {
/// Create the builder using the given database file.
///
/// The *dbfile* should be in a dedicated directory and this directory must exist. The
/// [`Context`] will create other files and folders in the same directory as the
/// database file used.
pub fn new(dbfile: PathBuf) -> Self {
ContextBuilder {
dbfile,
id: rand::random(),
events: Events::new(),
stock_strings: StockStrings::new(),
password: None,
}
}
/// Sets the context ID.
///
/// This identifier is used e.g. in [`Event`]s to identify which [`Context`] an event
/// belongs to. The only real limit on it is that it should not conflict with any other
/// [`Context`]s you currently have open. So if you handle multiple [`Context`]s you
/// may want to use this.
///
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
/// common case for using multiple [`Context`] instances.
pub fn with_id(mut self, id: u32) -> Self {
self.id = id;
self
}
/// Sets the event channel for this [`Context`].
///
/// Mostly useful when using multiple [`Context`]s, this allows creating one [`Events`]
/// channel and passing it to all [`Context`]s so all events are recieved on the same
/// channel.
///
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
/// common case for using multiple [`Context`] instances.
pub fn with_events(mut self, events: Events) -> Self {
self.events = events;
self
}
/// Sets the [`StockStrings`] map to use for this [`Context`].
///
/// This is useful in order to share the same translation strings in all [`Context`]s.
/// The mapping may be empty when set, it will be populated by
/// [`Context::set_stock-translation`] or [`Accounts::set_stock_translation`] calls.
///
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
/// common case for using multiple [`Context`] instances.
///
/// [`Accounts::set_stock_translation`]: crate::accounts::Accounts::set_stock_translation
pub fn with_stock_strings(mut self, stock_strings: StockStrings) -> Self {
self.stock_strings = stock_strings;
self
}
/// Sets the password to unlock the database.
///
/// If an encrypted database is used it must be opened with a password. Setting a
/// password on a new database will enable encryption.
pub fn with_password(mut self, password: String) -> Self {
self.password = Some(password);
self
}
/// Opens the [`Context`].
pub async fn open(self) -> Result<Context, ContextError> {
let context =
Context::new_closed(&self.dbfile, self.id, self.events, self.stock_strings).await?;
let password = self.password.unwrap_or_default();
match context.open(password).await? {
true => Ok(context),
false => Err(ContextError::DatabaseEncrypted),
}
}
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum ContextError {
#[error("database could not be decrypted, incorrect or missing password")]
DatabaseEncrypted,
#[error("failed to open context")]
Other(#[from] anyhow::Error),
}
/// The context for a single DeltaChat account.
///
/// This contains all the state for a single DeltaChat account, including background tasks
/// running in Tokio to operate the account. The [`Context`] can be cheaply cloned.
///
/// Each context, and thus each account, must be associated with an directory where all the
/// state is kept. This state is also preserved between restarts.
///
/// To use multiple accounts it is best to look at the [accounts
/// manager][crate::accounts::Accounts] which handles storing multiple accounts in a single
/// directory structure and handles loading them all concurrently.
#[derive(Clone, Debug)]
pub struct Context {
pub(crate) inner: Arc<InnerContext>,
@@ -570,10 +420,6 @@ impl Context {
.await?
.unwrap_or_default();
let configured_inbox_folder = self
.get_config(Config::ConfiguredInboxFolder)
.await?
.unwrap_or_else(|| "<unset>".to_string());
let configured_sentbox_folder = self
.get_config(Config::ConfiguredSentboxFolder)
.await?
@@ -645,7 +491,6 @@ impl Context {
res.insert("mvbox_move", mvbox_move.to_string());
res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
res.insert("folders_configured", folders_configured.to_string());
res.insert("configured_inbox_folder", configured_inbox_folder);
res.insert("configured_sentbox_folder", configured_sentbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
@@ -699,12 +544,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"authserv_id_candidates",
self.get_config(Config::AuthservIdCandidates)
.await?
.unwrap_or_default(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
@@ -811,7 +650,7 @@ impl Context {
ON m.chat_id=c.id
WHERE m.chat_id>9
AND m.hidden=0
AND c.blocked!=1
AND c.blocked=0
AND ct.blocked=0
AND m.txt LIKE ?
ORDER BY m.id DESC LIMIT 1000",
@@ -864,8 +703,6 @@ mod tests {
use crate::chat::{
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::message::{Message, Viewtype};
use crate::receive_imf::receive_imf;
@@ -1201,59 +1038,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_search_unaccepted_requests() -> Result<()> {
let t = TestContext::new_alice().await;
receive_imf(
&t,
b"From: BobBar <bob@example.org>\n\
To: alice@example.org\n\
Subject: foo\n\
Message-ID: <msg1234@example.org>\n\
Chat-Version: 1.0\n\
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
\n\
hello bob, foobar test!\n",
false,
)
.await?;
let chat_id = t.get_last_msg().await.get_chat_id();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_type(), Chattype::Single);
assert!(chat.is_contact_request());
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
assert_eq!(
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
1
);
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
chat_id.block(&t).await?;
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
assert_eq!(
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
0
);
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
let contact_ids = get_chat_contacts(&t, chat_id).await?;
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
assert_eq!(
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
1
);
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_limit_search_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;

View File

@@ -4,13 +4,12 @@ use std::collections::HashSet;
use anyhow::{Context as _, Result};
use mailparse::ParsedMail;
use mailparse::SingleInfo;
use crate::aheader::Aheader;
use crate::authres;
use crate::authres::handle_authres;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::log::LogExt;
@@ -56,44 +55,35 @@ pub async fn try_decrypt(
.await
}
pub async fn prepare_decryption(
pub async fn create_decryption_info(
context: &Context,
mail: &ParsedMail<'_>,
from: &[SingleInfo],
message_time: i64,
) -> Result<DecryptionInfo> {
let from = if let Some(f) = from.first() {
&f.addr
} else {
return Ok(DecryptionInfo::default());
};
let from = mail
.headers
.get_header(HeaderDef::From_)
.and_then(|from_addr| mailparse::addrparse_header(from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
.unwrap_or_default();
let autocrypt_header = Aheader::from_headers(from, &mail.headers)
let autocrypt_header = Aheader::from_headers(&from, &mail.headers)
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
.flatten();
let dkim_results = handle_authres(context, mail, from, message_time).await?;
let peerstate = get_autocrypt_peerstate(
context,
from,
autocrypt_header.as_ref(),
message_time,
// Disallowing keychanges is disabled for now:
true, // dkim_results.allow_keychange,
)
.await?;
let peerstate =
get_autocrypt_peerstate(context, &from, autocrypt_header.as_ref(), message_time).await?;
Ok(DecryptionInfo {
from: from.to_string(),
from,
autocrypt_header,
peerstate,
message_time,
dkim_results,
})
}
#[derive(Default, Debug)]
#[derive(Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
@@ -106,7 +96,6 @@ pub struct DecryptionInfo {
/// means out-of-order message arrival, We don't modify the
/// peerstate in this case.
pub message_time: i64,
pub(crate) dkim_results: authres::DkimResults,
}
/// Returns a reference to the encrypted payload of a ["Mixed
@@ -274,16 +263,12 @@ fn keyring_from_peerstate(peerstate: &Option<Peerstate>) -> Keyring<SignedPublic
/// If we already know this fingerprint from another contact's peerstate, return that
/// peerstate in order to make AEAP work, but don't save it into the db yet.
///
/// The param `allow_change` is used to prevent the autocrypt key from being changed
/// if we suspect that the message may be forged and have a spoofed sender identity.
///
/// Returns updated peerstate.
pub(crate) async fn get_autocrypt_peerstate(
context: &Context,
from: &str,
autocrypt_header: Option<&Aheader>,
message_time: i64,
allow_change: bool,
) -> Result<Option<Peerstate>> {
let mut peerstate;
@@ -304,15 +289,8 @@ pub(crate) async fn get_autocrypt_peerstate(
if let Some(ref mut peerstate) = peerstate {
if addr_cmp(&peerstate.addr, from) {
if allow_change {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
info!(
context,
"Refusing to update existing peerstate of {}", &peerstate.addr
);
}
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
// If `peerstate.addr` and `from` differ, this means that
// someone is using the same key but a different addr, probably

View File

@@ -11,7 +11,7 @@ use crate::imap::{Imap, ImapActionResult};
use crate::job::{self, Action, Job, Status};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::param::Params;
use crate::param::{Param, Params};
use crate::tools::time;
use crate::{job_try, stock_str, EventType};
use std::cmp::max;
@@ -69,6 +69,42 @@ impl Context {
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
}
}
// Merges the two messages to `placeholder_msg_id`;
// `full_msg_id` is no longer used afterwards.
pub(crate) async fn merge_messages(
&self,
full_msg_id: MsgId,
placeholder_msg_id: MsgId,
) -> Result<()> {
let placeholder = Message::load_from_db(self, placeholder_msg_id).await?;
self.sql
.transaction(move |transaction| {
transaction
.execute("DELETE FROM msgs WHERE id=?;", paramsv![placeholder_msg_id])?;
transaction.execute(
"UPDATE msgs SET id=? WHERE id=?",
paramsv![placeholder_msg_id, full_msg_id],
)?;
Ok(())
})
.await?;
let mut full = Message::load_from_db(self, placeholder_msg_id).await?;
for key in [
Param::WebxdcSummary,
Param::WebxdcSummaryTimestamp,
Param::WebxdcDocument,
Param::WebxdcDocumentTimestamp,
] {
if let Some(value) = placeholder.param.get(key) {
full.param.set(key, value);
}
}
full.update_param(self).await?;
Ok(())
}
}
impl MsgId {
@@ -428,14 +464,14 @@ mod tests {
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
let sent2_rfc724_mid = Message::load_from_db(&alice, sent2.sender_msg_id)
let sent2_rfc742_mid = Message::load_from_db(&alice, sent2.sender_msg_id)
.await?
.rfc724_mid;
// not downloading the status update results in an placeholder
receive_imf_inner(
&bob,
&sent2_rfc724_mid,
&sent2_rfc742_mid,
sent2.payload().as_bytes(),
false,
Some(sent2.payload().len() as u32),
@@ -451,7 +487,7 @@ mod tests {
// (usually status updates are too small for not being downloaded directly)
receive_imf_inner(
&bob,
&sent2_rfc724_mid,
&sent2_rfc742_mid,
sent2.payload().as_bytes(),
false,
None,

View File

@@ -173,13 +173,6 @@ pub enum EventType {
msg_id: MsgId,
},
/// Reactions for the message changed.
ReactionsChanged {
chat_id: ChatId,
msg_id: MsgId,
contact_id: ContactId,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -189,10 +182,6 @@ pub enum EventType {
msg_id: MsgId,
},
IncomingMsgBunch {
msg_ids: Vec<MsgId>,
},
/// Messages were seen or noticed.
/// chat id is always set.
MsgsNoticed(ChatId),

View File

@@ -63,11 +63,6 @@ pub enum HeaderDef {
Sender,
EphemeralTimer,
Received,
/// A header that includes the results of the DKIM, SPF and DMARC checks.
/// See <https://datatracker.ietf.org/doc/html/rfc8601>
AuthenticationResults,
_TestHeader,
}

View File

@@ -94,7 +94,7 @@ impl HtmlMsgParser {
let parsedmail = mailparse::parse_mail(rawmime)?;
parser.collect_texts_recursive(&parsedmail).await?;
parser.collect_texts_recursive(context, &parsedmail).await?;
if parser.html.is_empty() {
if let Some(plain) = &parser.plain {
@@ -117,6 +117,7 @@ impl HtmlMsgParser {
/// therefore we use the first one.
fn collect_texts_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
// Boxed future to deal with recursion
@@ -124,7 +125,7 @@ impl HtmlMsgParser {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in mail.subparts.iter() {
self.collect_texts_recursive(cur_data).await?
self.collect_texts_recursive(context, cur_data).await?
}
Ok(())
}
@@ -134,7 +135,7 @@ impl HtmlMsgParser {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
self.collect_texts_recursive(&mail).await
self.collect_texts_recursive(context, &mail).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
@@ -206,7 +207,7 @@ impl HtmlMsgParser {
Ok(re) => {
self.html = re
.replace_all(
&self.html,
&*self.html,
format!("${{1}}{}${{3}}", replacement).as_str(),
)
.as_ref()

View File

@@ -494,8 +494,6 @@ impl Imap {
}
async fn disconnect(&mut self, context: &Context) {
info!(context, "disconnecting");
// Close folder if messages should be expunged
if let Err(err) = self.close_folder(context).await {
warn!(context, "failed to close folder: {:?}", err);
@@ -904,12 +902,6 @@ impl Imap {
info!(context, "{} mails read from \"{}\".", read_cnt, folder);
let msg_ids = received_msgs
.iter()
.flat_map(|m| m.msg_ids.clone())
.collect();
context.emit_event(EventType::IncomingMsgBunch { msg_ids });
chat::mark_old_messages_as_noticed(context, received_msgs).await?;
Ok(read_cnt > 0)
@@ -1949,20 +1941,15 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
fn get_folder_meaning(folder_name: &Name) -> FolderMeaning {
for attr in folder_name.attributes() {
match attr {
NameAttribute::Trash => return FolderMeaning::Other,
NameAttribute::Sent => return FolderMeaning::Sent,
NameAttribute::Junk => return FolderMeaning::Spam,
NameAttribute::Drafts => return FolderMeaning::Drafts,
NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
NameAttribute::Extension(ref label) => {
match label.as_ref() {
"\\Spam" => return FolderMeaning::Spam,
"\\Important" => return FolderMeaning::Virtual,
_ => {}
};
}
_ => {}
if let NameAttribute::Extension(ref label) = attr {
match label.as_ref() {
"\\Trash" => return FolderMeaning::Other,
"\\Sent" => return FolderMeaning::Sent,
"\\Spam" | "\\Junk" => return FolderMeaning::Spam,
"\\Drafts" => return FolderMeaning::Drafts,
"\\All" | "\\Important" | "\\Flagged" => return FolderMeaning::Virtual,
_ => {}
};
}
}
FolderMeaning::Unknown
@@ -2170,8 +2157,8 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
.sql
.execute(
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
paramsv![folder, uid_next],
ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;",
paramsv![folder, uid_next, uid_next, folder],
)
.await?;
Ok(())
@@ -2202,8 +2189,8 @@ pub(crate) async fn set_uidvalidity(
.sql
.execute(
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
paramsv![folder, uidvalidity],
ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;",
paramsv![folder, uidvalidity, uidvalidity, folder],
)
.await?;
Ok(())
@@ -2225,8 +2212,8 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
.sql
.execute(
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
paramsv![folder, modseq],
ON CONFLICT(folder) DO UPDATE SET modseq=? WHERE folder=?;",
paramsv![folder, modseq, modseq, folder],
)
.await?;
Ok(())

View File

@@ -54,10 +54,10 @@ impl Imap {
Interrupt(InterruptInfo),
}
let folder_name = watch_folder.as_deref().unwrap_or("None");
info!(
context,
"{}: Idle entering wait-on-remote state", folder_name
"{}: Idle entering wait-on-remote state",
watch_folder.as_deref().unwrap_or("None")
);
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let info = self.idle_interrupt.recv().await;
@@ -70,36 +70,26 @@ impl Imap {
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
info!(context, "Idle has NewData {:?}", x);
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(
context,
"{}: Idle-wait timeout or interruption", folder_name
);
info!(context, "Idle-wait timeout or interruption");
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(
context,
"{}: Idle wait was interrupted manually", folder_name
);
info!(context, "Idle wait was interrupted");
}
Ok(Event::Interrupt(i)) => {
info!(
context,
"{}: Idle wait was interrupted: {:?}", folder_name, &i
);
info = i;
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
warn!(context, "Idle wait errored: {:?}", err);
}
}
let session = tokio::time::timeout(Duration::from_secs(15), handle.done())
.await
.with_context(|| format!("{}: IMAP IDLE protocol timed out", folder_name))?
.with_context(|| format!("{}: IMAP IDLE failed", folder_name))?;
.await?
.context("IMAP IDLE protocol timed out")?;
self.session = Some(Session { inner: session });
} else {
warn!(context, "Attempted to idle without a session");
@@ -183,7 +173,6 @@ impl Imap {
}
Event::Interrupt(info) => {
// Interrupt
info!(context, "Fake IDLE interrupted");
break info;
}
}

View File

@@ -19,31 +19,35 @@ pub enum Error {
#[error("Got a NO response when trying to select {0}, usually this means that it doesn't exist: {1}")]
NoFolder(String, String),
#[error("IMAP close/expunge failed")]
CloseExpungeFailed(#[from] async_imap::error::Error),
#[error("IMAP other error: {0}")]
Other(String),
}
impl From<anyhow::Error> for Error {
fn from(err: anyhow::Error) -> Error {
Error::Other(format!("{:#}", err))
}
}
impl Imap {
/// Issues a CLOSE command to expunge selected folder.
///
/// CLOSE is considerably faster than an EXPUNGE, see
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
pub(super) async fn close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
pub(super) async fn close_folder(&mut self, context: &Context) -> Result<()> {
if let Some(ref folder) = self.config.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
let session = self.session.as_mut().context("no session")?;
if let Err(err) = session.close().await.context("IMAP close/expunge failed") {
self.trigger_reconnect(context).await;
return Err(err);
if let Some(ref mut session) = self.session {
match session.close().await {
Ok(_) => {
info!(context, "close/expunge succeeded");
}
Err(err) => {
self.trigger_reconnect(context).await;
return Err(Error::CloseExpungeFailed(err));
}
}
} else {
return Err(Error::NoSession);
}
info!(context, "close/expunge succeeded");
}
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
@@ -52,7 +56,7 @@ impl Imap {
}
/// Issues a CLOSE command if selected folder needs expunge.
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> Result<()> {
if self.config.selected_folder_needs_expunge {
self.close_folder(context).await?;
}

View File

@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
use ::pgp::types::KeyTrait;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use futures::StreamExt;
use futures::{StreamExt, TryStreamExt};
use futures_lite::FutureExt;
use rand::{thread_rng, Rng};
use tokio::fs::{self, File};
@@ -17,6 +17,7 @@ use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
use crate::config::Config;
use crate::contact::ContactId;
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::log::LogExt;
@@ -30,7 +31,6 @@ use crate::tools::{
create_folder, delete_file, get_filesuffix_lc, open_file_std, read_file, time, write_file,
EmailAddress,
};
use crate::{e2ee, tools};
// Name of the database file in the backup.
const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
@@ -576,7 +576,10 @@ async fn export_backup_inner(
.append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
.await?;
let read_dir = tools::read_dir(context.get_blobdir()).await?;
let read_dir: Vec<_> =
tokio_stream::wrappers::ReadDirStream::new(fs::read_dir(context.get_blobdir()).await?)
.try_collect()
.await?;
let count = read_dir.len();
let mut written_files = 0;

View File

@@ -18,11 +18,7 @@
clippy::mixed_read_write_in_expression,
clippy::bool_assert_comparison,
clippy::manual_split_once,
clippy::format_push_string,
clippy::bool_to_int_with_if,
// This lint can be re-enabled once we don't target
// Rust 1.56 anymore:
clippy::collapsible_str_replace
clippy::format_push_string
)]
#[macro_use]
@@ -97,7 +93,6 @@ mod update_helper;
pub mod webxdc;
#[macro_use]
mod dehtml;
mod authres;
mod color;
pub mod html;
pub mod plaintext;
@@ -108,7 +103,6 @@ pub mod receive_imf;
pub mod tools;
pub mod accounts;
pub mod reaction;
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";

View File

@@ -22,7 +22,6 @@ use crate::imap::markseen_on_imap_table;
use crate::mimeparser::{parse_message_id, DeliveryReport, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::reaction::get_msg_reactions;
use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::stock_str;
@@ -726,7 +725,7 @@ impl Message {
self.text = text;
}
pub fn set_file(&mut self, file: impl ToString, filemime: Option<&str>) {
pub fn set_file(&mut self, file: impl AsRef<str>, filemime: Option<&str>) {
self.param.set(Param::File, file);
if let Some(filemime) = filemime {
self.param.set(Param::MimeType, filemime);
@@ -752,11 +751,6 @@ impl Message {
self.param.set_int(Param::Duration, duration);
}
/// Marks the message as reaction.
pub(crate) fn set_reaction(&mut self) {
self.param.set_int(Param::Reaction, 1);
}
pub async fn latefiling_mediasize(
&mut self,
context: &Context,
@@ -1088,11 +1082,6 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
ret += "\n";
let reactions = get_msg_reactions(context, msg_id).await?;
if !reactions.is_empty() {
ret += &format!("Reactions: {}\n", reactions);
}
if let Some(error) = msg.error.as_ref() {
ret += &format!("Error: {}", error);
}
@@ -1122,28 +1111,6 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
}
if !msg.rfc724_mid.is_empty() {
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
let server_uids = context
.sql
.query_map(
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
paramsv![msg.rfc724_mid],
|row| {
let folder: String = row.get("folder")?;
let uid: u32 = row.get("uid")?;
Ok((folder, uid))
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for (folder, uid) in server_uids {
// Format as RFC 5092 relative IMAP URL.
ret += &format!("\n</{}/;UID={}>", folder, uid);
}
}
let hop_info: Option<String> = context
.sql

View File

@@ -183,10 +183,7 @@ impl<'a> MimeFactory<'a> {
)
.await?;
if !msg.is_system_message()
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
&& context.get_config_bool(Config::MdnsEnabled).await?
{
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await? {
req_mdn = true;
}
}
@@ -1125,11 +1122,6 @@ impl<'a> MimeFactory<'a> {
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
))
.body(message_text);
if self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0 {
main_part = main_part.header(("Content-Disposition", "reaction"));
}
let mut parts = Vec::new();
// add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded;
@@ -1311,7 +1303,7 @@ impl<'a> MimeFactory<'a> {
/// This line length limit is an
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
fn wrapped_base64_encode(buf: &[u8]) -> String {
let base64 = base64::encode(buf);
let base64 = base64::encode(&buf);
let mut chars = base64.chars();
std::iter::repeat_with(|| chars.by_ref().take(78).collect::<String>())
.take_while(|s| !s.is_empty())

View File

@@ -15,7 +15,7 @@ use crate::blob::BlobObject;
use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
use crate::contact::{addr_cmp, addr_normalize, ContactId};
use crate::context::Context;
use crate::decrypt::{prepare_decryption, try_decrypt};
use crate::decrypt::{create_decryption_info, try_decrypt};
use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::format_flowed::unformat_flowed;
@@ -178,7 +178,7 @@ impl MimeMessage {
.get_header_value(HeaderDef::Date)
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
let mut hop_info = parse_receive_headers(&mail.get_headers());
let hop_info = parse_receive_headers(&mail.get_headers());
let mut headers = Default::default();
let mut recipients = Default::default();
@@ -220,9 +220,7 @@ impl MimeMessage {
let mut mail_raw = Vec::new();
let mut gossiped_addr = Default::default();
let mut from_is_signed = false;
let mut decryption_info = prepare_decryption(context, &mail, &from, message_time).await?;
hop_info += "\n\n";
hop_info += &decryption_info.dkim_results.to_string();
let mut decryption_info = create_decryption_info(context, &mail, message_time).await?;
// `signatures` is non-empty exactly if the message was encrypted and correctly signed.
let (mail, signatures, warn_empty_signature) =
@@ -298,8 +296,6 @@ impl MimeMessage {
if let Some(peerstate) = &mut decryption_info.peerstate {
if message_time > peerstate.last_seen_autocrypt
&& mail.ctype.mimetype != "multipart/report"
// Disallowing keychanges is disabled for now:
// && decryption_info.dkim_results.allow_keychange
{
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
@@ -373,12 +369,6 @@ impl MimeMessage {
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context).await?;
// Disallowing keychanges is disabled for now
// if !decryption_info.dkim_results.allow_keychange {
// for part in parser.parts.iter_mut() {
// part.error = Some("Seems like DKIM failed, this either is an attack or (more likely) a bug in Authentication-Results checking. Please tell us about this at https://support.delta.chat.".to_string());
// }
// }
if warn_empty_signature && parser.signatures.is_empty() {
for part in parser.parts.iter_mut() {
part.error = Some("No valid signature".to_string());
@@ -561,10 +551,7 @@ impl MimeMessage {
}
if prepend_subject && !subject.is_empty() {
let part_with_text = self
.parts
.iter_mut()
.find(|part| !part.msg.is_empty() && !part.is_reaction);
let part_with_text = self.parts.iter_mut().find(|part| !part.msg.is_empty());
if let Some(mut part) = part_with_text {
part.msg = format!("{} {}", subject, part.msg);
}
@@ -926,7 +913,6 @@ impl MimeMessage {
Ok(any_part_added)
}
/// Returns true if any part was added, false otherwise.
async fn add_single_part_if_known(
&mut self,
context: &Context,
@@ -960,30 +946,6 @@ impl MimeMessage {
warn!(context, "Missing attachment");
return Ok(false);
}
mime::TEXT
if mail.get_content_disposition().disposition
== DispositionType::Extension("reaction".to_string()) =>
{
// Reaction.
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
Err(err) => {
warn!(context, "Invalid body parsed {:?}", err);
// Note that it's not always an error - might be no data
return Ok(false);
}
};
let part = Part {
typ: Viewtype::Text,
mimetype: Some(mime_type),
msg: decoded_data,
is_reaction: true,
..Default::default()
};
self.do_add_single_part(part);
return Ok(true);
}
mime::TEXT | mime::HTML => {
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
@@ -1682,9 +1644,6 @@ pub struct Part {
/// note that multipart/related may contain further multipart nestings
/// and all of them needs to be marked with `is_related`.
pub(crate) is_related: bool,
/// Part is an RFC 9078 reaction.
pub(crate) is_reaction: bool,
}
/// return mimetype and viewtype for a parsed mail
@@ -3370,39 +3329,4 @@ Message.
Ok(())
}
/// Tests parsing of MIME message containing RFC 9078 reaction.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let mime_message = MimeMessage::from_bytes(
&alice,
"To: alice@example.org\n\
From: bob@example.net\n\
Date: Today, 29 February 2021 00:00:10 -800\n\
Message-ID: 56789@example.net\n\
In-Reply-To: 12345@example.org\n\
Subject: Meeting\n\
Mime-Version: 1.0 (1.0)\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: reaction\n\
\n\
\u{1F44D}"
.as_bytes(),
)
.await?;
assert_eq!(mime_message.parts.len(), 1);
assert_eq!(mime_message.parts[0].is_reaction, true);
assert_eq!(
mime_message
.get_header(HeaderDef::InReplyTo)
.and_then(|msgid| parse_message_id(msgid).ok())
.unwrap(),
"12345@example.org"
);
Ok(())
}
}

View File

@@ -59,9 +59,6 @@ pub enum Param {
/// For Messages
WantsMdn = b'r',
/// For Messages: the message is a reaction.
Reaction = b'x',
/// For Messages: a message with Auto-Submitted header ("bot").
Bot = b'b',
@@ -266,8 +263,8 @@ impl Params {
}
/// Set the given key to the passed in value.
pub fn set(&mut self, key: Param, value: impl ToString) -> &mut Self {
self.inner.insert(key, value.to_string());
pub fn set(&mut self, key: Param, value: impl AsRef<str>) -> &mut Self {
self.inner.insert(key, value.as_ref().to_string());
self
}

View File

@@ -196,36 +196,35 @@ impl Peerstate {
// gossip_key_fingerprint, verified_key, verified_key_fingerprint
let res = Peerstate {
addr: row.get("addr")?,
last_seen: row.get("last_seen")?,
last_seen_autocrypt: row.get("last_seen_autocrypt")?,
prefer_encrypt: EncryptPreference::from_i32(row.get("prefer_encrypted")?)
.unwrap_or_default(),
addr: row.get(0)?,
last_seen: row.get(1)?,
last_seen_autocrypt: row.get(2)?,
prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(),
public_key: row
.get("public_key")
.get(4)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
public_key_fingerprint: row
.get::<_, Option<String>>("public_key_fingerprint")?
.get::<_, Option<String>>(7)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_key: row
.get("gossip_key")
.get(6)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
gossip_key_fingerprint: row
.get::<_, Option<String>>("gossip_key_fingerprint")?
.get::<_, Option<String>>(8)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_timestamp: row.get("gossip_timestamp")?,
gossip_timestamp: row.get(5)?,
verified_key: row
.get("verified_key")
.get(9)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
verified_key_fingerprint: row
.get::<_, Option<String>>("verified_key_fingerprint")?
.get::<_, Option<String>>(10)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),

View File

@@ -44,12 +44,12 @@ impl PlainText {
let line = line.to_string().replace('\r', "");
let mut line = LINKIFY_MAIL_RE
.replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
.replace_all(&*line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
.as_ref()
.to_string();
line = LINKIFY_URL_RE
.replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
.replace_all(&*line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
.as_ref()
.to_string();

View File

@@ -230,7 +230,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
.await
.with_context(|| format!("can't check if address {:?} is our address", addr))?
{
if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await {
if token::exists(context, token::Namespace::InviteNumber, &*invitenumber).await {
Ok(Qr::WithdrawVerifyGroup {
grpname,
grpid,
@@ -260,7 +260,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
})
}
} else if context.is_self_addr(addr).await? {
if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await {
if token::exists(context, token::Namespace::InviteNumber, &*invitenumber).await {
Ok(Qr::WithdrawVerifyContact {
contact_id,
fingerprint,

View File

@@ -1,551 +0,0 @@
//! # Reactions.
//!
//! Reactions are short messages consisting of emojis sent in reply to
//! messages. Unlike normal messages which are added to the end of the chat,
//! reactions are supposed to be displayed near the original messages.
//!
//! RFC 9078 specifies how reactions are transmitted in MIME messages.
//!
//! Reaction update semantics is not well-defined in RFC 9078, so
//! Delta Chat uses the same semantics as in
//! [XEP-0444](https://xmpp.org/extensions/xep-0444.html) section
//! "3.2 Updating reactions to a message". Received reactions override
//! all previously received reactions from the same user and it is
//! possible to remove all reactions by sending an empty string as a reaction,
//! even though RFC 9078 requires at least one emoji to be sent.
use std::collections::BTreeMap;
use std::fmt;
use anyhow::Result;
use crate::chat::{send_msg, ChatId};
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
/// A single reaction consisting of multiple emoji sequences.
///
/// It is guaranteed to have all emojis sorted and deduplicated inside.
#[derive(Debug, Default, Clone)]
pub struct Reaction {
/// Canonical represntation of reaction as a string of space-separated emojis.
reaction: String,
}
// We implement From<&str> instead of std::str::FromStr, because
// FromStr requires error type and reaction parsing never returns an
// error.
impl From<&str> for Reaction {
/// Parses a string containing a reaction.
///
/// Reaction string is separated by spaces or tabs (`WSP` in ABNF),
/// but this function accepts any ASCII whitespace, so even a CRLF at
/// the end of string is acceptable.
///
/// Any short enough string is accepted as a reaction to avoid the
/// complexity of validating emoji sequences as required by RFC
/// 9078. On the sender side UI is responsible to provide only
/// valid emoji sequences via reaction picker. On the receiver
/// side, abuse of the possibility to use arbitrary strings as
/// reactions is not different from other kinds of spam attacks
/// such as sending large numbers of large messages, and should be
/// dealt with the same way, e.g. by blocking the user.
fn from(reaction: &str) -> Self {
let mut emojis: Vec<&str> = reaction
.split_ascii_whitespace()
.filter(|&emoji| emoji.len() < 30)
.collect();
emojis.sort_unstable();
emojis.dedup();
let reaction = emojis.join(" ");
Self { reaction }
}
}
impl Reaction {
/// Returns true if reaction contains no emojis.
pub fn is_empty(&self) -> bool {
self.reaction.is_empty()
}
/// Returns a vector of emojis composing a reaction.
pub fn emojis(&self) -> Vec<&str> {
self.reaction.split(' ').collect()
}
/// Returns space-separated string of emojis
pub fn as_str(&self) -> &str {
&self.reaction
}
/// Appends emojis from another reaction to this reaction.
pub fn add(&self, other: Self) -> Self {
let mut emojis: Vec<&str> = self.emojis();
emojis.append(&mut other.emojis());
emojis.sort_unstable();
emojis.dedup();
let reaction = emojis.join(" ");
Self { reaction }
}
}
/// Structure representing all reactions to a particular message.
#[derive(Debug)]
pub struct Reactions {
/// Map from a contact to its reaction to message.
reactions: BTreeMap<ContactId, Reaction>,
}
impl Reactions {
/// Returns vector of contacts that reacted to the message.
pub fn contacts(&self) -> Vec<ContactId> {
self.reactions.keys().copied().collect()
}
/// Returns reaction of a given contact to message.
///
/// If contact did not react to message or removed the reaction,
/// this method returns an empty reaction.
pub fn get(&self, contact_id: ContactId) -> Reaction {
self.reactions.get(&contact_id).cloned().unwrap_or_default()
}
/// Returns true if the message has no reactions.
pub fn is_empty(&self) -> bool {
self.reactions.is_empty()
}
}
impl fmt::Display for Reactions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut emoji_frequencies: BTreeMap<String, usize> = BTreeMap::new();
for reaction in self.reactions.values() {
for emoji in reaction.emojis() {
emoji_frequencies
.entry(emoji.to_string())
.and_modify(|x| *x += 1)
.or_insert(1);
}
}
let mut first = true;
for (emoji, frequency) in emoji_frequencies {
if !first {
write!(f, " ")?;
}
first = false;
write!(f, "{}{}", emoji, frequency)?;
}
Ok(())
}
}
async fn set_msg_id_reaction(
context: &Context,
msg_id: MsgId,
chat_id: ChatId,
contact_id: ContactId,
reaction: Reaction,
) -> Result<()> {
if reaction.is_empty() {
// Simply remove the record instead of setting it to empty string.
context
.sql
.execute(
"DELETE FROM reactions
WHERE msg_id = ?1
AND contact_id = ?2",
paramsv![msg_id, contact_id],
)
.await?;
} else {
context
.sql
.execute(
"INSERT INTO reactions (msg_id, contact_id, reaction)
VALUES (?1, ?2, ?3)
ON CONFLICT(msg_id, contact_id)
DO UPDATE SET reaction=excluded.reaction",
paramsv![msg_id, contact_id, reaction.as_str()],
)
.await?;
}
context.emit_event(EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
});
Ok(())
}
/// Sends a reaction to message `msg_id`, overriding previously sent reactions.
///
/// `reaction` is a string consisting of space-separated emoji. Use
/// empty string to retract a reaction.
pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
let msg = Message::load_from_db(context, msg_id).await?;
let chat_id = msg.chat_id;
let reaction: Reaction = reaction.into();
let mut reaction_msg = Message::new(Viewtype::Text);
reaction_msg.text = Some(reaction.as_str().to_string());
reaction_msg.set_reaction();
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
reaction_msg.hidden = true;
// Send messsage first.
let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
// Only set reaction if we successfully sent the message.
set_msg_id_reaction(context, msg_id, msg.chat_id, ContactId::SELF, reaction).await?;
Ok(reaction_msg_id)
}
/// Adds given reaction to message `msg_id` and sends an update.
///
/// This can be used to implement advanced clients that allow reacting
/// with multiple emojis. For a simple messenger UI, you probably want
/// to use [`send_reaction()`] instead so reacting with a new emoji
/// removes previous emoji at the same time.
pub async fn add_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
let self_reaction = get_self_reaction(context, msg_id).await?;
let reaction = self_reaction.add(Reaction::from(reaction));
send_reaction(context, msg_id, reaction.as_str()).await
}
/// Updates reaction of `contact_id` on the message with `in_reply_to`
/// Message-ID. If no such message is found in the database, reaction
/// is ignored.
///
/// `reaction` is a space-separated string of emojis. It can be empty
/// if contact wants to remove all reactions.
pub(crate) async fn set_msg_reaction(
context: &Context,
in_reply_to: &str,
chat_id: ChatId,
contact_id: ContactId,
reaction: Reaction,
) -> Result<()> {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
set_msg_id_reaction(context, msg_id, chat_id, contact_id, reaction).await
} else {
info!(
context,
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
);
Ok(())
}
}
/// Get our own reaction for a given message.
async fn get_self_reaction(context: &Context, msg_id: MsgId) -> Result<Reaction> {
let reaction_str: Option<String> = context
.sql
.query_get_value(
"SELECT reaction
FROM reactions
WHERE msg_id=? AND contact_id=?",
paramsv![msg_id, ContactId::SELF],
)
.await?;
Ok(reaction_str
.as_deref()
.map(Reaction::from)
.unwrap_or_default())
}
/// Returns a structure containing all reactions to the message.
pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<Reactions> {
let reactions = context
.sql
.query_map(
"SELECT contact_id, reaction FROM reactions WHERE msg_id=?",
paramsv![msg_id],
|row| {
let contact_id: ContactId = row.get(0)?;
let reaction: String = row.get(1)?;
Ok((contact_id, reaction))
},
|rows| {
let mut reactions = Vec::new();
for row in rows {
let (contact_id, reaction) = row?;
reactions.push((contact_id, Reaction::from(reaction.as_str())));
}
Ok(reactions)
},
)
.await?
.into_iter()
.collect();
Ok(Reactions { reactions })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::get_chat_msgs;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::contact::{Contact, Origin};
use crate::download::DownloadState;
use crate::message::MessageState;
use crate::receive_imf::{receive_imf, receive_imf_inner};
use crate::test_utils::TestContext;
#[test]
fn test_parse_reaction() {
// Check that basic set of emojis from RFC 9078 is supported.
assert_eq!(Reaction::from("👍").emojis(), vec!["👍"]);
assert_eq!(Reaction::from("👎").emojis(), vec!["👎"]);
assert_eq!(Reaction::from("😀").emojis(), vec!["😀"]);
assert_eq!(Reaction::from("").emojis(), vec![""]);
assert_eq!(Reaction::from("😢").emojis(), vec!["😢"]);
// Empty string can be used to remove all reactions.
assert!(Reaction::from("").is_empty());
// Short strings can be used as emojis, could be used to add
// support for custom emojis via emoji shortcodes.
assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
// Check that long strings are not valid emojis.
assert!(
Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
);
// Multiple reactions separated by spaces or tabs are supported.
assert_eq!(Reaction::from("👍 ❤").emojis(), vec!["", "👍"]);
assert_eq!(Reaction::from("👍\t").emojis(), vec!["", "👍"]);
// Invalid emojis are removed, but valid emojis are retained.
assert_eq!(
Reaction::from("👍\t:foo: ❤").emojis(),
vec![":foo:", "", "👍"]
);
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), ":foo: ❤ 👍");
// Duplicates are removed.
assert_eq!(Reaction::from("👍 👍").emojis(), vec!["👍"]);
}
#[test]
fn test_add_reaction() {
let reaction1 = Reaction::from("👍 😀");
let reaction2 = Reaction::from("");
let reaction_sum = reaction1.add(reaction2);
assert_eq!(reaction_sum.emojis(), vec!["", "👍", "😀"]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receive_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config(Config::ShowEmails, Some("2")).await?;
// Alice receives BCC-self copy of a message sent to Bob.
receive_imf(
&alice,
"To: bob@example.net\n\
From: alice@example.org\n\
Date: Today, 29 February 2021 00:00:00 -800\n\
Message-ID: 12345@example.org\n\
Subject: Meeting\n\
\n\
Can we chat at 1pm pacific, today?"
.as_bytes(),
false,
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.state, MessageState::OutDelivered);
let reactions = get_msg_reactions(&alice, msg.id).await?;
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 0);
let bob_id = Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated)
.await?
.0;
let bob_reaction = reactions.get(bob_id);
assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet.
// Alice receives reaction to her message from Bob.
receive_imf(
&alice,
"To: alice@example.org\n\
From: bob@example.net\n\
Date: Today, 29 February 2021 00:00:10 -800\n\
Message-ID: 56789@example.net\n\
In-Reply-To: 12345@example.org\n\
Subject: Meeting\n\
Mime-Version: 1.0 (1.0)\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: reaction\n\
\n\
\u{1F44D}"
.as_bytes(),
false,
)
.await?;
let reactions = get_msg_reactions(&alice, msg.id).await?;
assert_eq!(reactions.to_string(), "👍1");
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 1);
assert_eq!(contacts.get(0), Some(&bob_id));
let bob_reaction = reactions.get(bob_id);
assert_eq!(bob_reaction.is_empty(), false);
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
assert_eq!(bob_reaction.as_str(), "👍");
Ok(())
}
async fn expect_reactions_changed_event(
t: &TestContext,
expected_chat_id: ChatId,
expected_msg_id: MsgId,
expected_contact_id: ContactId,
) -> Result<()> {
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::ReactionsChanged { .. }))
.await;
match event {
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => {
assert_eq!(chat_id, expected_chat_id);
assert_eq!(msg_id, expected_msg_id);
assert_eq!(contact_id, expected_contact_id);
}
_ => unreachable!(),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(&bob).await;
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
let bob_msg = bob.recv_msg(&alice_msg).await;
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 1);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 1);
let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
bob.recv_msg(&alice_msg2).await;
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 2);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 2);
bob_msg.chat_id.accept(&bob).await?;
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 2);
let bob_reaction_msg = bob.pop_sent_msg().await;
let alice_reaction_msg = alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
assert_eq!(alice_reaction_msg.chat_id, DC_CHAT_ID_TRASH);
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 2);
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 1);
let bob_id = contacts.get(0).unwrap();
let bob_reaction = reactions.get(*bob_id);
assert_eq!(bob_reaction.is_empty(), false);
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
assert_eq!(bob_reaction.as_str(), "👍");
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
.await?;
// Alice reacts to own message.
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
.await
.unwrap();
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
assert_eq!(reactions.to_string(), "👍2 😀1");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_and_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
let msg_header = "From: Bob <bob@example.net>\n\
To: Alice <alice@example.org>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain";
let msg_full = format!("{}\n\n100k text...", msg_header);
// Alice downloads message from Bob partially.
let alice_received_message = receive_imf_inner(
&alice,
"first@example.org",
msg_header.as_bytes(),
false,
Some(100000),
false,
)
.await?
.unwrap();
let alice_msg_id = *alice_received_message.msg_ids.get(0).unwrap();
// Bob downloads own message on the other device.
let bob_received_message = receive_imf(&bob, msg_full.as_bytes(), false)
.await?
.unwrap();
let bob_msg_id = *bob_received_message.msg_ids.get(0).unwrap();
// Bob reacts to own message.
send_reaction(&bob, bob_msg_id, "👍").await.unwrap();
let bob_reaction_msg = bob.pop_sent_msg().await;
// Alice receives a reaction.
alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Available);
// Alice downloads full message.
receive_imf_inner(
&alice,
"first@example.org",
msg_full.as_bytes(),
false,
None,
false,
)
.await?;
// Check that reaction is still on the message after full download.
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Done);
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
Ok(())
}
}

View File

@@ -33,7 +33,6 @@ use crate::mimeparser::{
};
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::reaction::{set_msg_reaction, Reaction};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::sql;
use crate::stock_str;
@@ -173,13 +172,10 @@ pub(crate) async fn receive_imf_inner(
.await?;
let rcvd_timestamp = smeared_time(context).await;
// Sender timestamp is allowed to be a bit in the future due to
// unsynchronized clocks, but not too much.
let sent_timestamp = mime_parser
.get_header(HeaderDef::Date)
.and_then(|value| mailparse::dateparse(value).ok())
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp + 60));
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp));
// Add parts
let received_msg = add_parts(
@@ -408,7 +404,7 @@ async fn add_parts(
from_id: ContactId,
seen: bool,
is_partial_download: Option<u32>,
mut replace_msg_id: Option<MsgId>,
replace_msg_id: Option<MsgId>,
fetching_existing_messages: bool,
prevent_rename: bool,
) -> Result<ReceivedMsg> {
@@ -434,9 +430,8 @@ async fn add_parts(
};
// incoming non-chat messages may be discarded
let is_location_kml = mime_parser.location_kml.is_some();
let location_kml_is = mime_parser.location_kml.is_some();
let is_mdn = !mime_parser.mdn_reports.is_empty();
let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
@@ -455,7 +450,7 @@ async fn add_parts(
ShowEmails::All => allow_creation = !is_mdn,
}
} else {
allow_creation = !is_mdn && !is_reaction;
allow_creation = !is_mdn;
}
// check if the message introduces a new chat:
@@ -694,8 +689,7 @@ async fn add_parts(
state = if seen
|| fetching_existing_messages
|| is_mdn
|| is_reaction
|| is_location_kml
|| location_kml_is
|| securejoin_seen
|| chat_id_blocked == Blocked::Yes
{
@@ -847,15 +841,14 @@ async fn add_parts(
}
}
let orig_chat_id = chat_id;
let chat_id = if is_mdn || is_reaction {
if is_mdn {
chat_id = Some(DC_CHAT_ID_TRASH);
}
let chat_id = chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH)");
DC_CHAT_ID_TRASH
} else {
chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH)");
DC_CHAT_ID_TRASH
})
};
});
// Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
let mut ephemeral_timer = if is_partial_download.is_some() {
@@ -1060,41 +1053,11 @@ async fn add_parts(
let conn = context.sql.get_conn().await?;
for part in &mime_parser.parts {
if part.is_reaction {
set_msg_reaction(
context,
&mime_in_reply_to,
orig_chat_id.unwrap_or_default(),
from_id,
Reaction::from(part.msg.as_str()),
)
.await?;
}
let mut param = part.param.clone();
if is_system_message != SystemMessage::Unknown {
param.set_int(Param::Cmd, is_system_message as i32);
}
if let Some(replace_msg_id) = replace_msg_id {
let placeholder = Message::load_from_db(context, replace_msg_id).await?;
for key in [
Param::WebxdcSummary,
Param::WebxdcSummaryTimestamp,
Param::WebxdcDocument,
Param::WebxdcDocumentTimestamp,
] {
if let Some(value) = placeholder.param.get(key) {
param.set(key, value);
}
}
}
let mut txt_raw = "".to_string();
let mut stmt = conn.prepare_cached(
r#"
INSERT INTO msgs
(
id,
rfc724_mid, chat_id,
from_id, to_id, timestamp, timestamp_sent,
timestamp_rcvd, type, state, msgrmsg,
@@ -1104,22 +1067,13 @@ INSERT INTO msgs
ephemeral_timestamp, download_state, hop_info
)
VALUES (
?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?
)
ON CONFLICT (id) DO UPDATE
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
from_id=excluded.from_id, to_id=excluded.to_id, timestamp=excluded.timestamp, timestamp_sent=excluded.timestamp_sent,
timestamp_rcvd=excluded.timestamp_rcvd, type=excluded.type, state=excluded.state, msgrmsg=excluded.msgrmsg,
txt=excluded.txt, subject=excluded.subject, txt_raw=excluded.txt_raw, param=excluded.param,
bytes=excluded.bytes, mime_headers=excluded.mime_headers, mime_in_reply_to=excluded.mime_in_reply_to,
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
);
"#,
)?;
@@ -1141,6 +1095,11 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
txt_raw = format!("{}\n\n{}", subject, msg_raw);
}
let mut param = part.param.clone();
if is_system_message != SystemMessage::Unknown {
param.set_int(Param::Cmd, is_system_message as i32);
}
let ephemeral_timestamp = if in_fresh {
0
} else {
@@ -1154,10 +1113,9 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
// If you change which information is skipped if the message is trashed,
// also change `MsgId::trash()` and `delete_expired_messages()`
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
let trash = chat_id.is_trash() || (location_kml_is && msg.is_empty());
stmt.execute(paramsv![
replace_msg_id,
rfc724_mid,
if trash { DC_CHAT_ID_TRASH } else { chat_id },
if trash { ContactId::UNDEFINED } else { from_id },
@@ -1196,10 +1154,6 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
},
mime_parser.hop_info
])?;
// We only replace placeholder with a first part,
// afterwards insert additional parts.
replace_msg_id = None;
let row_id = conn.last_insert_rowid();
drop(stmt);
@@ -1208,8 +1162,14 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
drop(conn);
if let Some(replace_msg_id) = replace_msg_id {
// "Replace" placeholder with a message that has no parts.
replace_msg_id.delete_from_db(context).await?;
if let Some(created_msg_id) = created_db_entries.pop() {
context
.merge_messages(created_msg_id, replace_msg_id)
.await?;
created_db_entries.push(replace_msg_id);
} else {
replace_msg_id.delete_from_db(context).await?;
}
}
chat_id.unarchive_if_not_muted(context).await?;
@@ -1904,7 +1864,7 @@ async fn apply_mailinglist_changes(
Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
let mut contact = Contact::load_from_db(context, contact_id).await?;
if contact.param.get(Param::ListId) != Some(listid) {
contact.param.set(Param::ListId, listid);
contact.param.set(Param::ListId, &listid);
contact.update_param(context).await?;
}
@@ -1912,7 +1872,7 @@ async fn apply_mailinglist_changes(
if list_post != old_list_post {
// Apparently the mailing list is using a different List-Post header in each message.
// Make the mailing list read-only because we would't know which message the user wants to reply to.
chat.param.remove(Param::ListPost);
chat.param.set(Param::ListPost, "");
chat.update_param(context).await?;
}
} else {
@@ -2989,7 +2949,7 @@ mod tests {
assert!(chat.can_send(&t.ctx).await?);
assert_eq!(
chat.get_mailinglist_addr(),
Some("reply+elernshsetushoyseshetihseusaferuhsedtisneu@reply.github.com")
"reply+elernshsetushoyseshetihseusaferuhsedtisneu@reply.github.com"
);
assert_eq!(chat.name, "deltachat/deltachat-core-rust");
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1);
@@ -2998,7 +2958,7 @@ mod tests {
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?;
assert!(!chat.can_send(&t.ctx).await?);
assert_eq!(chat.get_mailinglist_addr(), None);
assert_eq!(chat.get_mailinglist_addr(), "");
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?;
assert_eq!(chats.len(), 1);
@@ -3057,7 +3017,7 @@ mod tests {
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
assert_eq!(chat.name, "delta-dev");
assert!(chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), Some("delta@codespeak.net"));
assert_eq!(chat.get_mailinglist_addr(), "delta@codespeak.net");
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
let contact1 = Contact::load_from_db(&t.ctx, msg.from_id).await.unwrap();
@@ -3149,7 +3109,6 @@ Hello mailinglist!\r\n"
.unwrap();
receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
t.evtracker.wait_next_incoming_message().await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0).unwrap();
@@ -3162,6 +3121,7 @@ Hello mailinglist!\r\n"
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); // Test that the message disappeared
t.evtracker.consume_events();
receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap();
// Check that no notification is displayed for blocked mailing list message.
@@ -3320,7 +3280,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.name, "ola");
assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1);
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), None);
assert_eq!(chat.get_mailinglist_addr(), "");
// receive another message with no sender name but the same address,
// make sure this lands in the same chat
@@ -3373,7 +3333,7 @@ Hello mailinglist!\r\n"
);
assert_eq!(chat.name, "Atlas Obscura");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), None);
assert_eq!(chat.get_mailinglist_addr(), "");
Ok(())
}
@@ -3402,7 +3362,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "1234ABCD-123LMNO.mailing.dhl.de");
assert_eq!(chat.name, "DHL Paket");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), None);
assert_eq!(chat.get_mailinglist_addr(), "");
Ok(())
}
@@ -3431,7 +3391,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "dpdde.mxmail.service.dpd.de");
assert_eq!(chat.name, "DPD");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), None);
assert_eq!(chat.get_mailinglist_addr(), "");
Ok(())
}
@@ -3452,7 +3412,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "96540.xt.local");
assert_eq!(chat.name, "Microsoft Store");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), None);
assert_eq!(chat.get_mailinglist_addr(), "");
receive_imf(
&t,
@@ -3465,7 +3425,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "121231234.xt.local");
assert_eq!(chat.name, "DER SPIEGEL Kundenservice");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), None);
assert_eq!(chat.get_mailinglist_addr(), "");
Ok(())
}
@@ -3488,7 +3448,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "51231231231231231231231232869f58.xing.com");
assert_eq!(chat.name, "xing.com");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), None);
assert_eq!(chat.get_mailinglist_addr(), "");
Ok(())
}

View File

@@ -5,7 +5,6 @@ use futures_lite::FutureExt;
use tokio::task;
use crate::config::Config;
use crate::contact::{ContactId, RecentlySeenLoop};
use crate::context::Context;
use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::imap::Imap;
@@ -36,8 +35,6 @@ pub(crate) struct Scheduler {
ephemeral_interrupt_send: Sender<()>,
location_handle: task::JoinHandle<()>,
location_interrupt_send: Sender<()>,
recently_seen_loop: RecentlySeenLoop,
}
impl Context {
@@ -82,12 +79,6 @@ impl Context {
scheduler.interrupt_location();
}
}
pub(crate) async fn interrupt_recently_seen(&self, contact_id: ContactId, timestamp: i64) {
if let Some(scheduler) = &*self.scheduler.read().await {
scheduler.interrupt_recently_seen(contact_id, timestamp);
}
}
}
async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConnectionHandlers) {
@@ -166,117 +157,112 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
.await;
}
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config) -> InterruptInfo {
let folder = match ctx.get_config(folder_config).await {
Ok(folder) => folder,
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo {
match ctx.get_config(folder).await {
Ok(Some(watch_folder)) => {
// connect and fake idle if unable to connect
if let Err(err) = connection.prepare(ctx).await {
warn!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
if folder == Config::ConfiguredInboxFolder {
if let Err(err) = connection
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap failed")
{
warn!(ctx, "{:#}", err);
}
}
// Fetch the watched folder.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
// Mark expired messages for deletion. Marked messages will be deleted from the server
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
// called right before `fetch_move_delete` because it is not well optimized and would
// otherwise slow down message fetching.
if let Err(err) = delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages failed")
{
warn!(ctx, "{:#}", err);
}
// Scan additional folders only after finishing fetching the watched folder.
//
// On iOS the application has strictly limited time to work in background, so we may not
// be able to scan all folders before time is up if there are many of them.
if folder == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
match connection.scan_folders(ctx).await {
Err(err) => {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{}", err);
}
Ok(true) => {
// Fetch the watched folder again in case scanning other folder moved messages
// there.
//
// In most cases this will select the watched folder and return because there are
// no new messages. We want to select the watched folder anyway before going IDLE
// there, so this does not take additional protocol round-trip.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
}
Ok(false) => {}
}
}
// Synchronize Seen flags.
connection
.sync_seen_flags(ctx, &watch_folder)
.await
.context("sync_seen_flags")
.ok_or_log(ctx);
connection.connectivity.set_connected(ctx).await;
// idle
if connection.can_idle() {
match connection.idle(ctx, Some(watch_folder)).await {
Ok(v) => v,
Err(err) => {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{}", err);
InterruptInfo::new(false)
}
}
} else {
connection.fake_idle(ctx, Some(watch_folder)).await
}
}
Ok(None) => {
connection.connectivity.set_not_configured(ctx).await;
info!(ctx, "Can not watch {} folder, not set", folder);
connection.fake_idle(ctx, None).await
}
Err(err) => {
warn!(
ctx,
"Can not watch {} folder, failed to retrieve config: {:#}", folder_config, err
"Can not watch {} folder, failed to retrieve config: {:?}", folder, err
);
return connection.fake_idle(ctx, None).await;
}
};
let watch_folder = if let Some(watch_folder) = folder {
watch_folder
} else {
connection.connectivity.set_not_configured(ctx).await;
info!(ctx, "Can not watch {} folder, not set", folder_config);
return connection.fake_idle(ctx, None).await;
};
// connect and fake idle if unable to connect
if let Err(err) = connection.prepare(ctx).await {
warn!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
if folder_config == Config::ConfiguredInboxFolder {
if let Err(err) = connection
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap failed")
{
warn!(ctx, "{:#}", err);
}
}
// Fetch the watched folder.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
.context("fetch_move_delete")
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
// Mark expired messages for deletion. Marked messages will be deleted from the server
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
// called right before `fetch_move_delete` because it is not well optimized and would
// otherwise slow down message fetching.
if let Err(err) = delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages")
{
warn!(ctx, "{:#}", err);
}
// Scan additional folders only after finishing fetching the watched folder.
//
// On iOS the application has strictly limited time to work in background, so we may not
// be able to scan all folders before time is up if there are many of them.
if folder_config == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
match connection.scan_folders(ctx).await.context("scan_folders") {
Err(err) => {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{:#}", err);
}
Ok(true) => {
// Fetch the watched folder again in case scanning other folder moved messages
// there.
//
// In most cases this will select the watched folder and return because there are
// no new messages. We want to select the watched folder anyway before going IDLE
// there, so this does not take additional protocol round-trip.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
.context("fetch_move_delete after scan_folders")
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
}
Ok(false) => {}
}
}
// Synchronize Seen flags.
connection
.sync_seen_flags(ctx, &watch_folder)
.await
.context("sync_seen_flags")
.ok_or_log(ctx);
connection.connectivity.set_connected(ctx).await;
// idle
if !connection.can_idle() {
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
match connection.idle(ctx, Some(watch_folder)).await {
Ok(v) => v,
Err(err) => {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
InterruptInfo::new(false)
connection.fake_idle(ctx, None).await
}
}
}
@@ -285,11 +271,11 @@ async fn simple_imap_loop(
ctx: Context,
started: Sender<()>,
inbox_handlers: ImapConnectionHandlers,
folder_config: Config,
folder: Config,
) {
use futures::future::FutureExt;
info!(ctx, "starting simple loop for {}", folder_config);
info!(ctx, "starting simple loop for {}", folder.as_ref());
let ImapConnectionHandlers {
mut connection,
stop_receiver,
@@ -305,7 +291,7 @@ async fn simple_imap_loop(
}
loop {
fetch_idle(&ctx, &mut connection, folder_config).await;
fetch_idle(&ctx, &mut connection, folder).await;
}
};
@@ -486,8 +472,6 @@ impl Scheduler {
})
};
let recently_seen_loop = RecentlySeenLoop::new(ctx.clone());
let res = Self {
inbox,
mvbox,
@@ -501,7 +485,6 @@ impl Scheduler {
ephemeral_interrupt_send,
location_handle,
location_interrupt_send,
recently_seen_loop,
};
// wait for all loops to be started
@@ -556,10 +539,6 @@ impl Scheduler {
self.location_interrupt_send.try_send(()).ok();
}
fn interrupt_recently_seen(&self, contact_id: ContactId, timestamp: i64) {
self.recently_seen_loop.interrupt(contact_id, timestamp);
}
/// Halt the scheduler.
///
/// It consumes the scheduler and never fails to stop it. In the worst case, long-running tasks
@@ -595,7 +574,6 @@ impl Scheduler {
.ok_or_log(context);
self.ephemeral_handle.abort();
self.location_handle.abort();
self.recently_seen_loop.abort();
}
}

View File

@@ -405,7 +405,7 @@ impl Context {
ret += " <b>";
ret += &*escaper::encode_minimal(&foldername);
ret += ":</b> ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
ret += &*escaper::encode_minimal(&*detailed.to_string_imap(self).await);
ret += "</li>";
folder_added = true;

View File

@@ -695,214 +695,222 @@ mod tests {
use crate::test_utils::{TestContext, TestContextManager};
use crate::tools::EmailAddress;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact() {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
0
);
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
0
);
#[test]
fn test_setup_contact() {
let body = Box::pin(async {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
0
);
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
0
);
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
// Step 2: Bob scans QR-code, sends vc-request
join_securejoin(&bob.ctx, &qr).await.unwrap();
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
1
);
// Step 2: Bob scans QR-code, sends vc-request
join_securejoin(&bob.ctx, &qr).await.unwrap();
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
1
);
let sent = bob.pop_sent_msg().await;
assert_eq!(
sent.recipient(),
EmailAddress::new("alice@example.org").unwrap()
);
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
let sent = bob.pop_sent_msg().await;
assert_eq!(
sent.recipient(),
EmailAddress::new("alice@example.org").unwrap()
);
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
// Step 3: Alice receives vc-request, sends vc-auth-required
alice.recv_msg(&sent).await;
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
1
);
// Step 3: Alice receives vc-request, sends vc-auth-required
alice.recv_msg(&sent).await;
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
1
);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-auth-required"
);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-auth-required"
);
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
bob.recv_msg(&sent).await;
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
bob.recv_msg(&sent).await;
// Check Bob emitted the JoinerProgress event.
let event = bob
.evtracker
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
.await;
match event {
EventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => {
let alice_contact_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
assert_eq!(contact_id, alice_contact_id);
assert_eq!(progress, 400);
// Check Bob emitted the JoinerProgress event.
let event = bob
.evtracker
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
.await;
match event {
EventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => {
let alice_contact_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
assert_eq!(contact_id, alice_contact_id);
assert_eq!(progress, 400);
}
_ => unreachable!(),
}
_ => unreachable!(),
}
// Check Bob sent the right message.
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
// Alice should not yet have Bob verified
let contact_bob_id =
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::Unverified
);
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// exactly one one-to-one chat should be visible for both now
// (check this before calling alice.create_chat() explicitly below)
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
// Check Bob sent the right message.
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.len(),
1
);
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
1
);
.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
// Check Alice got the verified message in her 1:1 chat.
{
let chat = alice.create_chat(&bob).await;
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id(), DC_GCM_ADDDAYMARKER)
// Alice should not yet have Bob verified
let contact_bob_id =
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.max()
.expect("No messages in Alice's 1:1 chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("bob@example.net verified"));
}
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::Unverified
);
// Check Alice sent the right message to Bob.
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm"
);
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
// exactly one one-to-one chat should be visible for both now
// (check this before calling alice.create_chat() explicitly below)
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
1
);
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
1
);
// Check Alice got the verified message in her 1:1 chat.
{
let chat = alice.create_chat(&bob).await;
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id(), DC_GCM_ADDDAYMARKER)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.max()
.expect("No messages in Alice's 1:1 chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("bob@example.net verified"));
}
// Check Alice sent the right message to Bob.
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm"
);
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await.unwrap(),
VerifiedStatus::Unverified
);
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await.unwrap(),
VerifiedStatus::Unverified
);
// Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// Check Bob got the verified message in his 1:1 chat.
{
let chat = bob.create_chat(&alice).await;
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id(), DC_GCM_ADDDAYMARKER)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.max()
.expect("No messages in Bob's 1:1 chat");
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("alice@example.org verified"));
}
// Check Bob got the verified message in his 1:1 chat.
{
let chat = bob.create_chat(&alice).await;
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id(), DC_GCM_ADDDAYMARKER)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.max()
.expect("No messages in Bob's 1:1 chat");
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("alice@example.org verified"));
}
// Check Bob sent the final message
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
// Check Bob sent the final message
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
});
return tokio::runtime::Builder::new_multi_thread()
.worker_threads(2usize)
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1068,231 +1076,240 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_secure_join() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
#[test]
fn test_secure_join() -> Result<()> {
let body = Box::pin(async {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// We start with empty chatlists.
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
// We start with empty chatlists.
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
let alice_chatid =
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
let alice_chatid =
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat")
.await?;
// Step 1: Generate QR-code, secure-join implied by chatid
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid))
.await
.unwrap();
// Step 2: Bob scans QR-code, sends vg-request
let bob_chatid = join_securejoin(&bob.ctx, &qr).await?;
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
let sent = bob.pop_sent_msg().await;
assert_eq!(
sent.recipient(),
EmailAddress::new("alice@example.org").unwrap()
);
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
// Step 3: Alice receives vg-request, sends vg-auth-required
alice.recv_msg(&sent).await;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-auth-required"
);
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
bob.recv_msg(&sent).await;
let sent = bob.pop_sent_msg().await;
// Check Bob emitted the JoinerProgress event.
let event = bob
.evtracker
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
.await;
match event {
EventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => {
let alice_contact_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
assert_eq!(contact_id, alice_contact_id);
assert_eq!(progress, 400);
}
_ => unreachable!(),
}
// Check Bob sent the right handshake message.
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx).await?.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
// Alice should not yet have Bob verified
let contact_bob_id =
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
.await?
.expect("Contact not found");
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id).await?;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::Unverified
);
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::BidirectVerified
);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-member-added"
);
{
// Now Alice's chat with Bob should still be hidden, the verified message should
// appear in the group chat.
let chat = alice
.get_chat(&bob)
// Step 1: Generate QR-code, secure-join implied by chatid
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid))
.await
.expect("Alice has no 1:1 chat with bob");
.unwrap();
// Step 2: Bob scans QR-code, sends vg-request
let bob_chatid = join_securejoin(&bob.ctx, &qr).await?;
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
let sent = bob.pop_sent_msg().await;
assert_eq!(
chat.blocked,
Blocked::Yes,
"Alice's 1:1 chat with Bob is not hidden"
sent.recipient(),
EmailAddress::new("alice@example.org").unwrap()
);
let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid, DC_GCM_ADDDAYMARKER)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.min()
.expect("No messages in Alice's group chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("bob@example.net verified"));
}
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id).await?;
assert_eq!(
contact_bob.is_verified(&bob.ctx).await?,
VerifiedStatus::Unverified
);
// Step 3: Alice receives vg-request, sends vg-auth-required
alice.recv_msg(&sent).await;
// Step 7: Bob receives vg-member-added, sends vg-member-added-received
bob.recv_msg(&sent).await;
{
// Bob has Alice verified, message shows up in the group chat.
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
contact_alice.is_verified(&bob.ctx).await?,
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-auth-required"
);
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
bob.recv_msg(&sent).await;
let sent = bob.pop_sent_msg().await;
// Check Bob emitted the JoinerProgress event.
let event = bob
.evtracker
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
.await;
match event {
EventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => {
let alice_contact_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
assert_eq!(contact_id, alice_contact_id);
assert_eq!(progress, 400);
}
_ => unreachable!(),
}
// Check Bob sent the right handshake message.
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx).await?.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
// Alice should not yet have Bob verified
let contact_bob_id =
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
.await?
.expect("Contact not found");
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id).await?;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::Unverified
);
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::BidirectVerified
);
let chat = bob
.get_chat(&alice)
.await
.expect("Bob has no 1:1 chat with Alice");
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
chat.blocked,
Blocked::Yes,
"Bob's 1:1 chat with Alice is not hidden"
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-member-added"
);
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid, DC_GCM_ADDDAYMARKER)
.await
.unwrap()
{
if let chat::ChatItem::Message { msg_id } = item {
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
let text = msg.get_text().unwrap();
println!("msg {} text: {}", msg_id, text);
}
// Now Alice's chat with Bob should still be hidden, the verified message should
// appear in the group chat.
let chat = alice
.get_chat(&bob)
.await
.expect("Alice has no 1:1 chat with bob");
assert_eq!(
chat.blocked,
Blocked::Yes,
"Alice's 1:1 chat with Bob is not hidden"
);
let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid, DC_GCM_ADDDAYMARKER)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.min()
.expect("No messages in Alice's group chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("bob@example.net verified"));
}
let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid, DC_GCM_ADDDAYMARKER)
.await
.unwrap()
.into_iter();
loop {
match msg_iter.next() {
Some(chat::ChatItem::Message { msg_id }) => {
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id).await?;
assert_eq!(
contact_bob.is_verified(&bob.ctx).await?,
VerifiedStatus::Unverified
);
// Step 7: Bob receives vg-member-added, sends vg-member-added-received
bob.recv_msg(&sent).await;
{
// Bob has Alice verified, message shows up in the group chat.
assert_eq!(
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
let chat = bob
.get_chat(&alice)
.await
.expect("Bob has no 1:1 chat with Alice");
assert_eq!(
chat.blocked,
Blocked::Yes,
"Bob's 1:1 chat with Alice is not hidden"
);
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid, DC_GCM_ADDDAYMARKER)
.await
.unwrap()
{
if let chat::ChatItem::Message { msg_id } = item {
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
let text = msg.get_text().unwrap();
match text.contains("alice@example.org verified") {
true => {
assert!(msg.is_info());
break;
}
false => continue,
}
println!("msg {} text: {}", msg_id, text);
}
}
let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid, DC_GCM_ADDDAYMARKER)
.await
.unwrap()
.into_iter();
loop {
match msg_iter.next() {
Some(chat::ChatItem::Message { msg_id }) => {
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
let text = msg.get_text().unwrap();
match text.contains("alice@example.org verified") {
true => {
assert!(msg.is_info());
break;
}
false => continue,
}
}
Some(_) => continue,
None => panic!("Verified message not found in Bob's group chat"),
}
Some(_) => continue,
None => panic!("Verified message not found in Bob's group chat"),
}
}
}
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-member-added-received"
);
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-member-added-received"
);
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
assert!(bob_chat.is_protected());
assert!(bob_chat.typ == Chattype::Group);
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
assert!(bob_chat.is_protected());
assert!(bob_chat.typ == Chattype::Group);
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
// The one-to-one chats are used internally for the hidden handshake messages,
// however, should not be visible in the UIs.
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
// The one-to-one chats are used internally for the hidden handshake messages,
// however, should not be visible in the UIs.
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
let bobs_chat_with_alice = bob.create_chat(&alice).await;
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
alice.recv_msg(&sent).await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
let bobs_chat_with_alice = bob.create_chat(&alice).await;
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
alice.recv_msg(&sent).await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
Ok(())
Ok(())
});
return tokio::runtime::Builder::new_multi_thread()
.worker_threads(2usize)
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -396,7 +396,7 @@ impl Sql {
}
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
pub async fn count(&self, query: &str, params: impl rusqlite::Params) -> Result<usize> {
pub async fn count(&self, query: &str, params: impl rusqlite::Params) -> anyhow::Result<usize> {
let count: isize = self.query_row(query, params, |row| row.get(0)).await?;
Ok(usize::try_from(count)?)
}
@@ -429,10 +429,10 @@ impl Sql {
///
/// If the function returns an error, the transaction will be rolled back. If it does not return an
/// error, the transaction will be committed.
pub async fn transaction<G, H>(&self, callback: G) -> Result<H>
pub async fn transaction<G, H>(&self, callback: G) -> anyhow::Result<H>
where
H: Send + 'static,
G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> anyhow::Result<H>,
{
let mut conn = self.get_conn().await?;
tokio::task::block_in_place(move || {
@@ -453,7 +453,7 @@ impl Sql {
}
/// Query the database if the requested table already exists.
pub async fn table_exists(&self, name: &str) -> Result<bool> {
pub async fn table_exists(&self, name: &str) -> anyhow::Result<bool> {
let conn = self.get_conn().await?;
tokio::task::block_in_place(move || {
let mut exists = false;
@@ -468,7 +468,7 @@ impl Sql {
}
/// Check if a column exists in a given table.
pub async fn col_exists(&self, table_name: &str, col_name: &str) -> Result<bool> {
pub async fn col_exists(&self, table_name: &str, col_name: &str) -> anyhow::Result<bool> {
let conn = self.get_conn().await?;
tokio::task::block_in_place(move || {
let mut exists = false;
@@ -492,7 +492,7 @@ impl Sql {
sql: &str,
params: impl rusqlite::Params,
f: F,
) -> Result<Option<T>>
) -> anyhow::Result<Option<T>>
where
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
{
@@ -516,7 +516,7 @@ impl Sql {
&self,
query: &str,
params: impl rusqlite::Params,
) -> Result<Option<T>>
) -> anyhow::Result<Option<T>>
where
T: rusqlite::types::FromSql,
{

View File

@@ -24,6 +24,7 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
.await
.context("failed to check if config table exists")?
{
info!(context, "First time init: creating tables",);
sql.transaction(move |transaction| {
transaction.execute_batch(TABLES)?;
@@ -34,8 +35,7 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
)?;
Ok(())
})
.await
.context("Creating tables failed")?;
.await?;
let mut lock = context.sql.config_cache.write().await;
lock.insert(
@@ -57,6 +57,7 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
let mut recode_avatar = false;
if dbversion < 1 {
info!(context, "[migration] v1");
sql.execute_migration(
r#"
CREATE TABLE leftgrps ( id INTEGER PRIMARY KEY, grpid TEXT DEFAULT '');
@@ -66,6 +67,7 @@ CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
.await?;
}
if dbversion < 2 {
info!(context, "[migration] v2");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';",
2,
@@ -73,6 +75,7 @@ CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
.await?;
}
if dbversion < 7 {
info!(context, "[migration] v7");
sql.execute_migration(
"CREATE TABLE keypairs (\
id INTEGER PRIMARY KEY, \
@@ -86,6 +89,7 @@ CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
.await?;
}
if dbversion < 10 {
info!(context, "[migration] v10");
sql.execute_migration(
"CREATE TABLE acpeerstates (\
id INTEGER PRIMARY KEY, \
@@ -100,6 +104,7 @@ CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
.await?;
}
if dbversion < 12 {
info!(context, "[migration] v12");
sql.execute_migration(
r#"
CREATE TABLE msgs_mdns ( msg_id INTEGER, contact_id INTEGER);
@@ -109,6 +114,7 @@ CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);"#,
.await?;
}
if dbversion < 17 {
info!(context, "[migration] v17");
sql.execute_migration(
r#"
ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;
@@ -122,6 +128,7 @@ CREATE INDEX msgs_index5 ON msgs (starred);"#,
.await?;
}
if dbversion < 18 {
info!(context, "[migration] v18");
sql.execute_migration(
r#"
ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;
@@ -131,6 +138,7 @@ ALTER TABLE acpeerstates ADD COLUMN gossip_key;"#,
.await?;
}
if dbversion < 27 {
info!(context, "[migration] v27");
// chat.id=1 and chat.id=2 are the old deaddrops,
// the current ones are defined by chats.blocked=2
sql.execute_migration(
@@ -144,6 +152,7 @@ ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;"#,
.await?;
}
if dbversion < 34 {
info!(context, "[migration] v34");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0;
@@ -158,6 +167,7 @@ CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);"#,
recalc_fingerprints = true;
}
if dbversion < 39 {
info!(context, "[migration] v39");
sql.execute_migration(
r#"
CREATE TABLE tokens (
@@ -175,14 +185,17 @@ CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);"#,
.await?;
}
if dbversion < 40 {
info!(context, "[migration] v40");
sql.execute_migration("ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;", 40)
.await?;
}
if dbversion < 44 {
info!(context, "[migration] v44");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN mime_headers TEXT;", 44)
.await?;
}
if dbversion < 46 {
info!(context, "[migration] v46");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;
@@ -192,10 +205,12 @@ ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
.await?;
}
if dbversion < 47 {
info!(context, "[migration] v47");
sql.execute_migration("ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;", 47)
.await?;
}
if dbversion < 48 {
info!(context, "[migration] v48");
// NOTE: move_state is not used anymore
sql.execute_migration(
"ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;",
@@ -204,6 +219,7 @@ ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
.await?;
}
if dbversion < 49 {
info!(context, "[migration] v49");
sql.execute_migration(
"ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;",
49,
@@ -211,6 +227,7 @@ ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
.await?;
}
if dbversion < 50 {
info!(context, "[migration] v50");
// installations <= 0.100.1 used DC_SHOW_EMAILS_ALL implicitly;
// keep this default and use DC_SHOW_EMAILS_NO
// only for new installations
@@ -221,6 +238,7 @@ ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
sql.set_db_version(50).await?;
}
if dbversion < 53 {
info!(context, "[migration] v53");
// the messages containing _only_ locations
// are also added to the database as _hidden_.
sql.execute_migration(
@@ -245,6 +263,7 @@ CREATE INDEX chats_index3 ON chats (locations_send_until);"#,
.await?;
}
if dbversion < 54 {
info!(context, "[migration] v54");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;
@@ -254,6 +273,7 @@ CREATE INDEX msgs_index6 ON msgs (location_id);"#,
.await?;
}
if dbversion < 55 {
info!(context, "[migration] v55");
sql.execute_migration(
"ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;",
55,
@@ -261,6 +281,7 @@ CREATE INDEX msgs_index6 ON msgs (location_id);"#,
.await?;
}
if dbversion < 59 {
info!(context, "[migration] v59");
// records in the devmsglabels are kept when the message is deleted.
// so, msg_id may or may not exist.
sql.execute_migration(
@@ -274,6 +295,7 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
}
if dbversion < 60 {
info!(context, "[migration] v60");
sql.execute_migration(
"ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;",
60,
@@ -281,6 +303,7 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
.await?;
}
if dbversion < 61 {
info!(context, "[migration] v61");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;",
61,
@@ -289,6 +312,7 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
update_icons = true;
}
if dbversion < 62 {
info!(context, "[migration] v62");
sql.execute_migration(
"ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
62,
@@ -296,14 +320,17 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
.await?;
}
if dbversion < 63 {
info!(context, "[migration] v63");
sql.execute_migration("UPDATE chats SET grpid='' WHERE type=100", 63)
.await?;
}
if dbversion < 64 {
info!(context, "[migration] v64");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';", 64)
.await?;
}
if dbversion < 65 {
info!(context, "[migration] v65");
sql.execute_migration(
r#"
ALTER TABLE chats ADD COLUMN ephemeral_timer INTEGER;
@@ -314,10 +341,12 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
.await?;
}
if dbversion < 66 {
info!(context, "[migration] v66");
update_icons = true;
sql.set_db_version(66).await?;
}
if dbversion < 67 {
info!(context, "[migration] v67");
for prefix in &["", "configured_"] {
if let Some(server_flags) = sql
.get_raw_config_int(format!("{}server_flags", prefix))
@@ -344,6 +373,7 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
sql.set_db_version(67).await?;
}
if dbversion < 68 {
info!(context, "[migration] v68");
// the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat()
sql.execute_migration(
"CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);",
@@ -352,6 +382,7 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
.await?;
}
if dbversion < 69 {
info!(context, "[migration] v69");
sql.execute_migration(
r#"
ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0;
@@ -363,6 +394,7 @@ UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
}
if dbversion < 71 {
info!(context, "[migration] v71");
if let Ok(addr) = context.get_primary_self_addr().await {
if let Ok(domain) = EmailAddress::new(&addr).map(|email| email.domain) {
context
@@ -378,16 +410,20 @@ UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
sql.set_db_version(71).await?;
}
if dbversion < 72 && !sql.col_exists("msgs", "mime_modified").await? {
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#,
72,
)
.await?;
if dbversion < 72 {
info!(context, "[migration] v72");
if !sql.col_exists("msgs", "mime_modified").await? {
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#,
72,
)
.await?;
}
}
if dbversion < 73 {
use Config::*;
info!(context, "[migration] v73");
sql.execute(
r#"
CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#,
@@ -423,10 +459,12 @@ paramsv![]
sql.set_db_version(73).await?;
}
if dbversion < 74 {
info!(context, "[migration] v74");
sql.execute_migration("UPDATE contacts SET name='' WHERE name=authname", 74)
.await?;
}
if dbversion < 75 {
info!(context, "[migration] v75");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';",
75,
@@ -434,20 +472,24 @@ paramsv![]
.await?;
}
if dbversion < 76 {
info!(context, "[migration] v76");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", 76)
.await?;
}
if dbversion < 77 {
info!(context, "[migration] v77");
recode_avatar = true;
sql.set_db_version(77).await?;
}
if dbversion < 78 {
// move requests to "Archived Chats",
// this way, the app looks familiar after the contact request upgrade.
info!(context, "[migration] v78");
sql.execute_migration("UPDATE chats SET archived=1 WHERE blocked=2;", 78)
.await?;
}
if dbversion < 79 {
info!(context, "[migration] v79");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN download_state INTEGER DEFAULT 0;
@@ -457,6 +499,7 @@ paramsv![]
.await?;
}
if dbversion < 80 {
info!(context, "[migration] v80");
sql.execute_migration(
r#"CREATE TABLE multi_device_sync (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -466,10 +509,12 @@ item TEXT DEFAULT '');"#,
.await?;
}
if dbversion < 81 {
info!(context, "[migration] v81");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN hop_info TEXT;", 81)
.await?;
}
if dbversion < 82 {
info!(context, "[migration] v82");
sql.execute_migration(
r#"CREATE TABLE imap (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -502,6 +547,7 @@ DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
.await?;
}
if dbversion < 83 {
info!(context, "[migration] v83");
sql.execute_migration(
"ALTER TABLE imap_sync
ADD COLUMN modseq -- Highest modification sequence
@@ -511,6 +557,7 @@ DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
.await?;
}
if dbversion < 84 {
info!(context, "[migration] v84");
sql.execute_migration(
r#"CREATE TABLE msgs_status_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -523,6 +570,7 @@ CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);"#,
.await?;
}
if dbversion < 85 {
info!(context, "[migration] v85");
sql.execute_migration(
r#"CREATE TABLE smtp (
id INTEGER PRIMARY KEY,
@@ -539,6 +587,7 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 86 {
info!(context, "[migration] v86");
sql.execute_migration(
r#"CREATE TABLE bobstate (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -551,6 +600,7 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 87 {
info!(context, "[migration] v87");
// the index is used to speed up delete_expired_messages()
sql.execute_migration(
"CREATE INDEX IF NOT EXISTS msgs_index8 ON msgs (ephemeral_timestamp);",
@@ -559,10 +609,12 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 88 {
info!(context, "[migration] v88");
sql.execute_migration("DROP TABLE IF EXISTS backup_blobs;", 88)
.await?;
}
if dbversion < 89 {
info!(context, "[migration] v89");
sql.execute_migration(
r#"CREATE TABLE imap_markseen (
id INTEGER,
@@ -573,6 +625,7 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 90 {
info!(context, "[migration] v90");
sql.execute_migration(
r#"CREATE TABLE smtp_mdns (
msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN
@@ -585,6 +638,7 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 91 {
info!(context, "[migration] v91");
sql.execute_migration(
r#"CREATE TABLE smtp_status_updates (
msg_id INTEGER NOT NULL UNIQUE, -- msg_id of the webxdc instance with pending updates
@@ -596,42 +650,6 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
)
.await?;
}
if dbversion < 92 {
sql.execute_migration(
r#"CREATE TABLE reactions (
msg_id INTEGER NOT NULL, -- id of the message reacted to
contact_id INTEGER NOT NULL, -- id of the contact reacting to the message
reaction TEXT DEFAULT '' NOT NULL, -- a sequence of emojis separated by spaces
PRIMARY KEY(msg_id, contact_id),
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE -- delete reactions when contact is deleted
)"#,
92
).await?;
}
if dbversion < 93 {
sql.execute_migration(
"CREATE TABLE sending_domains(domain TEXT PRIMARY KEY, dkim_works INTEGER DEFAULT 0);",
93,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
.unwrap_or_default();
if new_version != dbversion || !exists_before_update {
let created_db = if exists_before_update {
""
} else {
"Created new database; "
};
info!(
context,
"{}[migration] v{}-v{}", created_db, dbversion, new_version
);
}
Ok((
recalc_fingerprints,

View File

@@ -74,14 +74,6 @@ impl TestContextManager {
.await
}
/// Creates a new unconfigured test account.
pub async fn unconfigured(&mut self) -> TestContext {
TestContext::builder()
.with_log_sink(self.log_tx.clone())
.build()
.await
}
/// Writes info events to the log that mark a section, e.g.:
///
/// ========== `msg` goes here ==========
@@ -97,23 +89,19 @@ impl TestContextManager {
/// - Let the other TestContext receive it and accept the chat
/// - Assert that the message arrived
pub async fn send_recv_accept(&self, from: &TestContext, to: &TestContext, msg: &str) {
let received_msg = self.try_send_recv(from, to, msg).await;
assert_eq!(received_msg.text.as_ref().unwrap(), msg);
received_msg.chat_id.accept(to).await.unwrap();
}
/// - Let one TestContext send a message
/// - Let the other TestContext receive it
pub async fn try_send_recv(&self, from: &TestContext, to: &TestContext, msg: &str) -> Message {
self.section(&format!(
"{} sends a message '{}' to {}",
from.name(),
msg,
to.name()
));
let chat = from.create_chat(to).await;
let sent = from.send_text(chat.id, msg).await;
to.recv_msg(&sent).await
let received_msg = to.recv_msg(&sent).await;
received_msg.chat_id.accept(to).await.unwrap();
assert_eq!(received_msg.text.unwrap(), msg);
}
pub async fn change_addr(&self, test_context: &TestContext, new_addr: &str) {
@@ -381,12 +369,6 @@ impl TestContext {
///
/// Panics if there is no message or on any error.
pub async fn pop_sent_msg(&self) -> SentMessage {
self.pop_sent_msg_opt(Duration::from_secs(3))
.await
.expect("no sent message found in jobs table")
}
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage> {
let start = Instant::now();
let (rowid, msg_id, payload, recipients) = loop {
let row = self
@@ -411,25 +393,25 @@ impl TestContext {
if let Some(row) = row {
break row;
}
if start.elapsed() < timeout {
if start.elapsed() < Duration::from_secs(3) {
tokio::time::sleep(Duration::from_millis(100)).await;
} else {
return None;
panic!("no sent message found in jobs table");
}
};
self.ctx
.sql
.execute("DELETE FROM smtp WHERE id=?;", paramsv![rowid])
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
.await
.expect("failed to remove job");
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
.await
.expect("failed to update message state");
Some(SentMessage {
SentMessage {
payload,
sender_msg_id: msg_id,
recipients,
})
}
}
/// Parses a message.
@@ -743,7 +725,7 @@ impl Drop for LogSink {
/// passed through a SMTP-IMAP pipeline.
#[derive(Debug, Clone)]
pub struct SentMessage {
pub payload: String,
payload: String,
recipients: String,
pub sender_msg_id: MsgId,
}
@@ -876,10 +858,9 @@ impl EventTracker {
.await
}
/// Wait for the next IncomingMsg event.
pub async fn wait_next_incoming_message(&self) {
self.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
.await;
/// Consumes all pending events.
pub fn consume_events(&self) {
while self.try_recv().is_ok() {}
}
}

View File

@@ -12,7 +12,7 @@ use std::time::{Duration, SystemTime};
use anyhow::{bail, Error, Result};
use chrono::{Local, TimeZone};
use futures::{StreamExt, TryStreamExt};
use futures::StreamExt;
use mailparse::dateparse;
use mailparse::headers::Headers;
use mailparse::MailHeaderMap;
@@ -272,7 +272,7 @@ pub(crate) fn create_id() -> String {
rng.fill(&mut arr[..]);
// Take 11 base64 characters containing 66 random bits.
base64::encode_config(arr, base64::URL_SAFE)
base64::encode_config(&arr, base64::URL_SAFE)
.chars()
.take(11)
.collect()
@@ -495,13 +495,6 @@ pub fn open_file_std<P: AsRef<std::path::Path>>(
}
}
pub async fn read_dir(path: &Path) -> Result<Vec<fs::DirEntry>> {
let res = tokio_stream::wrappers::ReadDirStream::new(fs::read_dir(path).await?)
.try_collect()
.await?;
Ok(res)
}
pub(crate) fn time() -> i64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
@@ -722,9 +715,7 @@ hi
Message-ID: 2dfdbde7@example.org
Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000
DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
check_parse_receive_headers_integration(raw, expected).await;
let raw = include_bytes!("../test-data/message/encrypted_with_received_headers.eml");
@@ -741,9 +732,7 @@ Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net
Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000
Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000";
check_parse_receive_headers_integration(raw, expected).await;
}

View File

@@ -496,7 +496,7 @@ impl Context {
for update_item in updates.updates {
self.create_status_update_record(
&mut instance,
&serde_json::to_string(&update_item)?,
&*serde_json::to_string(&update_item)?,
timestamp,
can_info_msg,
from_id,
@@ -545,7 +545,7 @@ impl Context {
let (update_item_str, serial) = row;
let update_item = StatusUpdateItemAndSerial
{
item: serde_json::from_str(&update_item_str)?,
item: serde_json::from_str(&*update_item_str)?,
serial,
max_serial,
};
@@ -553,7 +553,7 @@ impl Context {
if !json.is_empty() {
json.push_str(",\n");
}
json.push_str(&serde_json::to_string(&update_item)?);
json.push_str(&*serde_json::to_string(&update_item)?);
}
Ok(json)
},
@@ -1825,7 +1825,7 @@ sth_for_the = "future""#
let instance = t.get_last_msg().await;
let html = instance.get_webxdc_blob(&t, "index.html").await?;
assert!(String::from_utf8_lossy(&html).contains("requires a newer Delta Chat version"));
assert!(String::from_utf8_lossy(&*html).contains("requires a newer Delta Chat version"));
Ok(())
}

View File

@@ -8,7 +8,6 @@ Transport | IMAP v4 ([RFC 3501](https://tools.ietf.org/ht
Proxy | SOCKS5 ([RFC 1928](https://tools.ietf.org/html/rfc1928))
Embedded media | MIME Document Series ([RFC 2045](https://tools.ietf.org/html/rfc2045), [RFC 2046](https://tools.ietf.org/html/rfc2046)), Content-Disposition Header ([RFC 2183](https://tools.ietf.org/html/rfc2183)), Multipart/Related ([RFC 2387](https://tools.ietf.org/html/rfc2387))
Text and Quote encoding | Fixed, Flowed ([RFC 3676](https://tools.ietf.org/html/rfc3676))
Reactions | Reaction: Indicating Summary Reaction to a Message [RFC 9078](https://datatracker.ietf.org/doc/rfc9078/)
Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf.org/html/rfc2047)), Encoded Word Extensions ([RFC 2231](https://tools.ietf.org/html/rfc2231))
Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154))
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas106.aol.mail.ne1.yahoo.com;
dkim=pass header.i=@buzon.uy header.s=2019;
spf=pass smtp.mailfrom=buzon.uy;
dmarc=pass(p=REJECT) header.from=buzon.uy;
From: <alice@buzon.uy>
To: <alice@aol.com>

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas206.aol.mail.ne1.yahoo.com;
dkim=unknown;
spf=none smtp.mailfrom=delta.blinzeln.de;
dmarc=unknown header.from=delta.blinzeln.de;
From: <alice@delta.blinzeln.de>
To: <alice@aol.com>

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas210.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@disroot.org header.s=mail;
spf=pass smtp.mailfrom=disroot.org;
dmarc=pass(p=QUARANTINE) header.from=disroot.org;
From: <alice@disroot.org>
To: <alice@aol.com>

View File

@@ -1,7 +0,0 @@
Authentication-Results: atlas105.aol.mail.ne1.yahoo.com;
dkim=pass header.i=@fastmail.com header.s=fm2;
dkim=pass header.i=@messagingengine.com header.s=fm2;
spf=pass smtp.mailfrom=fastmail.com;
dmarc=pass(p=NONE,sp=NONE) header.from=fastmail.com;
From: <alice@fastmail.com>
To: <alice@aol.com>

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas-baseline-production.v2-mail-prod1-gq1.omega.yahoo.com;
dkim=pass header.i=@gmail.com header.s=20210112;
spf=pass smtp.mailfrom=gmail.com;
dmarc=pass(p=NONE,sp=QUARANTINE) header.from=gmail.com;
From: <alice@gmail.com>
To: <alice@aol.com>

View File

@@ -1,8 +0,0 @@
Authentication-Results: atlas112.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@hotmail.com header.s=selector1;
spf=pass smtp.mailfrom=hotmail.com;
dmarc=pass(p=NONE) header.from=hotmail.com;
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
dkim=none; arc=none
From: <alice@hotmail.com>
To: <alice@aol.com>

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas101.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@icloud.com header.s=1a1hai;
spf=pass smtp.mailfrom=icloud.com;
dmarc=pass(p=QUARANTINE,sp=QUARANTINE) header.from=icloud.com;
From: <alice@icloud.com>
To: <alice@aol.com>

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
dkim=pass header.i=@ik.me header.s=20200325;
spf=pass smtp.mailfrom=ik.me;
dmarc=pass(p=REJECT) header.from=ik.me;
From: <alice@ik.me>
To: <alice@aol.com>

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas104.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@mail.ru header.s=mail4;
spf=pass smtp.mailfrom=mail.ru;
dmarc=pass(p=REJECT) header.from=mail.ru;
From: <alice@mail.ru>
To: <alice@aol.com>

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas211.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@mailo.com header.s=mailo;
spf=pass smtp.mailfrom=mailo.com;
dmarc=pass(p=NONE) header.from=mailo.com;
From: <alice@mailo.com>
To: <alice@aol.com>

View File

@@ -1,8 +0,0 @@
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
dkim=pass header.i=@outlook.com header.s=selector1;
spf=pass smtp.mailfrom=outlook.com;
dmarc=pass(p=NONE,sp=QUARANTINE) header.from=outlook.com;
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
dkim=none; arc=none
From: <alice@outlook.com>
To: <alice@aol.com>

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
dkim=pass header.i=@posteo.de header.s=2017;
spf=pass smtp.mailfrom=posteo.de;
dmarc=pass(p=NONE) header.from=posteo.de;
From: <alice@posteo.de>
To: <alice@aol.com>

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas114.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@yandex.ru header.s=mail;
spf=pass smtp.mailfrom=yandex.ru;
dmarc=pass(p=NONE) header.from=yandex.ru;
From: <alice@yandex.ru>
To: <alice@aol.com>

View File

@@ -1,6 +0,0 @@
Authentication-Results: atlas206.aol.mail.ne1.yahoo.com;
dkim=unknown;
spf=none smtp.mailfrom=delta.blinzeln.de;
dmarc=unknown header.from=delta.blinzeln.de;
From: forged-authres-added@example.com
Authentication-Results: aaa.com; dkim=pass header.i=@example.com

View File

@@ -1,5 +0,0 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=aol.com header.i=@aol.com header.b="sjmqxpKe";
dkim-atps=neutral
From: <alice@aol.com>
To: <alice@buzon.uy>

View File

@@ -1,3 +0,0 @@
From: <alice@delta.blinzeln.de>
To: <alice@buzon.uy>
Authentication-Results: secure-mailgate.com; auth=pass smtp.auth=91.203.111.88@webbox222.server-home.org

View File

@@ -1,5 +0,0 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="L9SmOHOj";
dkim-atps=neutral
From: <alice@disroot.org>
To: <alice@buzon.uy>

View File

@@ -1,6 +0,0 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=fastmail.com header.i=@fastmail.com header.b="kLB05is1";
dkim=pass (2048-bit key; unprotected) header.d=messagingengine.com header.i=@messagingengine.com header.b="B8mfR89g";
dkim-atps=neutral
From: <alice@fastmail.com>
To: <alice@buzon.uy>

View File

@@ -1,5 +0,0 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="Ngf1X5eN";
dkim-atps=neutral
From: <alice@gmail.com>
To: <alice@buzon.uy>

View File

@@ -1,7 +0,0 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=hotmail.com header.i=@hotmail.com header.b="dEHn9Szj";
dkim-atps=neutral
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
dkim=none; arc=none
From: <alice@hotmail.com>
To: <alice@buzon.uy>

View File

@@ -1,5 +0,0 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=icloud.com header.i=@icloud.com header.b="rAXD4xVN";
dkim-atps=neutral
From: <alice@icloud.com>
To: <alice@buzon.uy>

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