mirror of
https://github.com/chatmail/core.git
synced 2026-07-23 03:13:06 +03:00
Compare commits
16 Commits
py-1.94.0
...
jsonrpc/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6922a66f49 | ||
|
|
b2f7a7bb2e | ||
|
|
1965866813 | ||
|
|
110f56777d | ||
|
|
37c6001b6c | ||
|
|
9a9c91e591 | ||
|
|
17276179e7 | ||
|
|
335d780f3e | ||
|
|
450d113993 | ||
|
|
cd6d181bbc | ||
|
|
c92c6a24a0 | ||
|
|
ffe7216194 | ||
|
|
474eb7cbc8 | ||
|
|
252b528f40 | ||
|
|
a4357712bf | ||
|
|
62afd3d4c3 |
83
.github/workflows/jsonrpc-client-npm-package.yml
vendored
Normal file
83
.github/workflows/jsonrpc-client-npm-package.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: 'jsonrpc js client build'
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- '!py-*'
|
||||
|
||||
|
||||
jobs:
|
||||
pack-module:
|
||||
name: 'Package @deltachat/jsonrpc-client and upload to download.delta.chat'
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: install tree
|
||||
run: sudo apt install tree
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: get tag
|
||||
id: tag
|
||||
uses: dawidd6/action-get-tag@v1
|
||||
continue-on-error: true
|
||||
- name: Get Pullrequest ID
|
||||
id: prepare
|
||||
run: |
|
||||
tag=${{ steps.tag.outputs.tag }}
|
||||
if [ -z "$tag" ]; then
|
||||
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
|
||||
echo "No preview will be uploaded this time, but the $tag release"
|
||||
fi
|
||||
- name: System info
|
||||
run: |
|
||||
npm --version
|
||||
node --version
|
||||
echo $DELTACHAT_JSONRPC_TAR_GZ
|
||||
- name: install dependencies without running scripts
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm install --ignore-scripts
|
||||
- name: package
|
||||
shell: bash
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run build:tsc
|
||||
npm pack .
|
||||
ls -lah
|
||||
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: deltachat-jsonrpc-client.tgz
|
||||
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||
# Upload to download.delta.chat/node/preview/
|
||||
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
|
||||
if: ${{ ! steps.tag.outputs.tag }}
|
||||
id: upload-preview
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
||||
continue-on-error: true
|
||||
- name: "Post links to details"
|
||||
if: steps.upload-preview.outcome == 'success'
|
||||
run: node ./node/scripts/postLinksToDetails.js
|
||||
env:
|
||||
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
|
||||
# Upload to download.delta.chat/node/
|
||||
- name: Upload deltachat-jsonrpc-client build to download.delta.chat/node/
|
||||
if: ${{ steps.tag.outputs.tag }}
|
||||
id: upload
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -5,8 +5,28 @@
|
||||
### API-Changes
|
||||
|
||||
### Changes
|
||||
- jsonrpc js client:
|
||||
- Change package name from `deltachat-jsonrpc-client` to `@deltachat/jsonrpc-client`
|
||||
- remove relative file dependency to it from `deltachat-node` (because it did not work anyway and broke the nix build of desktop)
|
||||
- ci: add github ci action to upload it to our download server automaticaly on realease
|
||||
|
||||
### Fixes
|
||||
- jsonrpc: fix daymarkers
|
||||
|
||||
## 1.95.0
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc: add `mailingListAddress` property to `FullChat` #3607
|
||||
- jsonrpc: add `MessageNotificationInfo` & `messageGetNotificationInfo()` #3614
|
||||
- jsonrpc: add `chat_get_neighboring_media` function #3610
|
||||
|
||||
### Changes
|
||||
- added `dclogin:` scheme to allow configuration from a qr code
|
||||
(data inside qrcode, contrary to `dcaccount:` which points to an API to create an account) #3541
|
||||
- truncate incoming messages by lines instead of just length #3480
|
||||
- emit separate `DC_EVENT_MSGS_CHANGED` for each expired message,
|
||||
and `DC_EVENT_WEBXDC_INSTANCE_DELETED` when a message contains a webxdc #3605
|
||||
- enable `bcc_self` by default #3612
|
||||
|
||||
|
||||
## 1.94.0
|
||||
@@ -20,7 +40,7 @@
|
||||
and `dc_event_emitter_unref()` should be used instead of
|
||||
`dc_accounts_event_emitter_unref`.
|
||||
- add `dc_contact_was_seen_recently()` #3560
|
||||
- Fix get_connectivity_html and get_encrinfo futures not being Send. See rust-lang/rust#101650 for more information
|
||||
- Fix `get_connectivity_html` and `get_encrinfo` futures not being Send. See rust-lang/rust#101650 for more information
|
||||
- jsonrpc: add functions: #3586, #3587, #3590
|
||||
- `deleteChat()`
|
||||
- `getChatEncryptionInfo()`
|
||||
@@ -60,7 +80,7 @@
|
||||
- order contact lists by "last seen";
|
||||
this affects `dc_get_chat_contacts()`, `dc_get_contacts()` and `dc_get_blocked_contacts()` #3562
|
||||
- add `internet_access` flag to `dc_msg_get_webxdc_info()` #3516
|
||||
- `DC_EVENT_WEBXDC_INSTANCE_DELETED` is emitted when a message containing a webxdc gets deleted #3105
|
||||
- `DC_EVENT_WEBXDC_INSTANCE_DELETED` is emitted when a message containing a webxdc gets deleted #3592
|
||||
|
||||
### Fixes
|
||||
- do not emit notifications for blocked chats #3557
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -893,7 +893,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.94.0"
|
||||
version = "1.95.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -965,7 +965,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.94.0"
|
||||
version = "1.95.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -993,7 +993,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.94.0"
|
||||
version = "1.95.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.94.0"
|
||||
version = "1.95.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.94.0"
|
||||
version = "1.95.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -468,10 +468,10 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
|
||||
/**
|
||||
* Set configuration values from a QR code.
|
||||
* Before this function is called, dc_check_qr() should confirm the type of the
|
||||
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
|
||||
* QR code is DC_QR_ACCOUNT, DC_QR_LOGIN or DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN
|
||||
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -2270,6 +2270,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_WITHDRAW_VERIFYGROUP 502 // text1=groupname
|
||||
#define DC_QR_REVIVE_VERIFYCONTACT 510
|
||||
#define DC_QR_REVIVE_VERIFYGROUP 512 // text1=groupname
|
||||
#define DC_QR_LOGIN 520 // text1=email_address
|
||||
|
||||
/**
|
||||
* Check a scanned QR code.
|
||||
@@ -2342,6 +2343,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask the user if they want to revive the withdrawn group-invite code;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
*
|
||||
* - DC_QR_LOGIN with dc_lot_t::text1=email_address:
|
||||
* ask the user if they want to login with the email_address,
|
||||
* if so, call dc_set_config_from_qr() and then dc_configure().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param qr The text of the scanned QR code.
|
||||
|
||||
@@ -169,7 +169,7 @@ pub unsafe extern "C" fn dc_context_unref(context: *mut dc_context_t) {
|
||||
eprintln!("ignoring careless call to dc_context_unref()");
|
||||
return;
|
||||
}
|
||||
Box::from_raw(context);
|
||||
drop(Box::from_raw(context));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -463,7 +463,7 @@ pub unsafe extern "C" fn dc_event_unref(a: *mut dc_event_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(a);
|
||||
drop(Box::from_raw(a));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -687,7 +687,7 @@ pub unsafe extern "C" fn dc_event_emitter_unref(emitter: *mut dc_event_emitter_t
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(emitter);
|
||||
drop(Box::from_raw(emitter));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2426,7 +2426,7 @@ pub unsafe extern "C" fn dc_array_unref(a: *mut dc_array::dc_array_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(a);
|
||||
drop(Box::from_raw(a));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2607,7 +2607,7 @@ pub unsafe extern "C" fn dc_chatlist_unref(chatlist: *mut dc_chatlist_t) {
|
||||
eprintln!("ignoring careless call to dc_chatlist_unref()");
|
||||
return;
|
||||
}
|
||||
Box::from_raw(chatlist);
|
||||
drop(Box::from_raw(chatlist));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2752,7 +2752,7 @@ pub unsafe extern "C" fn dc_chat_unref(chat: *mut dc_chat_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(chat);
|
||||
drop(Box::from_raw(chat));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3022,7 +3022,7 @@ pub unsafe extern "C" fn dc_msg_unref(msg: *mut dc_msg_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(msg);
|
||||
drop(Box::from_raw(msg));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3744,7 +3744,7 @@ pub unsafe extern "C" fn dc_contact_unref(contact: *mut dc_contact_t) {
|
||||
eprintln!("ignoring careless call to dc_contact_unref()");
|
||||
return;
|
||||
}
|
||||
Box::from_raw(contact);
|
||||
drop(Box::from_raw(contact));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3908,7 +3908,7 @@ pub unsafe extern "C" fn dc_lot_unref(lot: *mut dc_lot_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(lot);
|
||||
drop(Box::from_raw(lot));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4200,7 +4200,8 @@ pub unsafe extern "C" fn dc_accounts_get_account(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.read().await.get_account(id).await })
|
||||
block_on(accounts.read())
|
||||
.get_account(id)
|
||||
.map(|ctx| Box::into_raw(Box::new(ctx)))
|
||||
.unwrap_or_else(std::ptr::null_mut)
|
||||
}
|
||||
@@ -4215,7 +4216,8 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.read().await.get_selected_account().await })
|
||||
block_on(accounts.read())
|
||||
.get_selected_account()
|
||||
.map(|ctx| Box::into_raw(Box::new(ctx)))
|
||||
.unwrap_or_else(std::ptr::null_mut)
|
||||
}
|
||||
@@ -4360,7 +4362,7 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let list = block_on(async move { accounts.read().await.get_all().await });
|
||||
let list = block_on(accounts.read()).get_all();
|
||||
let array: dc_array_t = list.into();
|
||||
|
||||
Box::into_raw(Box::new(array))
|
||||
@@ -4430,7 +4432,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let emitter = block_on(async move { accounts.read().await.get_event_emitter().await });
|
||||
let emitter = block_on(accounts.read()).get_event_emitter();
|
||||
|
||||
Box::into_raw(Box::new(emitter))
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ impl Lot {
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::ReviveVerifyContact { .. } => None,
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::Login { address, .. } => Some(address),
|
||||
},
|
||||
Self::Error(err) => Some(err),
|
||||
}
|
||||
@@ -108,6 +109,7 @@ impl Lot {
|
||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||
Qr::Login { .. } => LotState::QrLogin,
|
||||
},
|
||||
Self::Error(_err) => LotState::QrError,
|
||||
}
|
||||
@@ -131,6 +133,7 @@ impl Lot {
|
||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||
Qr::Login { .. } => Default::default(),
|
||||
},
|
||||
Self::Error(_) => Default::default(),
|
||||
}
|
||||
@@ -195,6 +198,9 @@ pub enum LotState {
|
||||
/// text1=groupname
|
||||
QrReviveVerifyGroup = 512,
|
||||
|
||||
/// text1=email_address
|
||||
QrLogin = 520,
|
||||
|
||||
// Message States
|
||||
MsgInFresh = 10,
|
||||
MsgInNoticed = 13,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.94.0"
|
||||
version = "1.95.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -6,6 +6,7 @@ use deltachat::{
|
||||
},
|
||||
chatlist::Chatlist,
|
||||
config::Config,
|
||||
constants::DC_MSG_ID_DAYMARKER,
|
||||
contact::{may_be_valid_addr, Contact, ContactId},
|
||||
context::get_info,
|
||||
message::{delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype},
|
||||
@@ -39,7 +40,7 @@ use types::webxdc::WebxdcMessageInfo;
|
||||
|
||||
use self::types::{
|
||||
chat::{BasicChat, MuteDuration},
|
||||
message::MessageViewtype,
|
||||
message::{MessageNotificationInfo, MessageViewtype},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -65,7 +66,6 @@ impl CommandApi {
|
||||
.read()
|
||||
.await
|
||||
.get_account(id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
|
||||
Ok(sc)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
async fn get_all_account_ids(&self) -> Vec<u32> {
|
||||
self.accounts.read().await.get_all().await
|
||||
self.accounts.read().await.get_all()
|
||||
}
|
||||
|
||||
/// Select account id for internally selected state.
|
||||
@@ -112,14 +112,14 @@ impl CommandApi {
|
||||
/// Get the selected account id of the internal state..
|
||||
/// TODO: Likely this is deprecated as all methods take an account id now.
|
||||
async fn get_selected_account_id(&self) -> Option<u32> {
|
||||
self.accounts.read().await.get_selected_account_id().await
|
||||
self.accounts.read().await.get_selected_account_id()
|
||||
}
|
||||
|
||||
/// Get a list of all configured accounts.
|
||||
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
|
||||
let mut accounts = Vec::new();
|
||||
for id in self.accounts.read().await.get_all().await {
|
||||
let context_option = self.accounts.read().await.get_account(id).await;
|
||||
for id in self.accounts.read().await.get_all() {
|
||||
let context_option = self.accounts.read().await.get_account(id);
|
||||
if let Some(ctx) = context_option {
|
||||
accounts.push(Account::from_context(&ctx, id).await?)
|
||||
} else {
|
||||
@@ -135,7 +135,7 @@ impl CommandApi {
|
||||
|
||||
/// Get top-level info for an account.
|
||||
async fn get_account_info(&self, account_id: u32) -> Result<Account> {
|
||||
let context_option = self.accounts.read().await.get_account(account_id).await;
|
||||
let context_option = self.accounts.read().await.get_account(account_id);
|
||||
if let Some(ctx) = context_option {
|
||||
Ok(Account::from_context(&ctx, account_id).await?)
|
||||
} else {
|
||||
@@ -605,6 +605,10 @@ impl CommandApi {
|
||||
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
|
||||
}
|
||||
|
||||
/// get ids of messages for a chat
|
||||
/// (this api is similar to the cffi api, because it's still used that way by desktop)
|
||||
///
|
||||
/// returns list with message ids and daymarkers (DC_MSG_ID_DAYMARKER = 9) if daymarkers are active
|
||||
async fn message_list_get_message_ids(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -615,9 +619,9 @@ impl CommandApi {
|
||||
let msg = get_chat_msgs(&ctx, ChatId::new(chat_id), flags).await?;
|
||||
Ok(msg
|
||||
.iter()
|
||||
.filter_map(|chat_item| match chat_item {
|
||||
deltachat::chat::ChatItem::Message { msg_id } => Some(msg_id.to_u32()),
|
||||
_ => None,
|
||||
.map(|chat_item| match chat_item {
|
||||
deltachat::chat::ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||
deltachat::chat::ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -643,6 +647,16 @@ impl CommandApi {
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
/// Fetch info desktop needs for creating a notification for a message
|
||||
async fn message_get_notification_info(
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
) -> Result<MessageNotificationInfo> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
MessageNotificationInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
|
||||
}
|
||||
|
||||
/// Delete messages. The messages are deleted on the current device and
|
||||
/// on the IMAP server.
|
||||
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||
@@ -841,6 +855,51 @@ impl CommandApi {
|
||||
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
|
||||
}
|
||||
|
||||
/// Search next/previous message based on a given message and a list of types.
|
||||
/// Typically used to implement the "next" and "previous" buttons
|
||||
/// in a gallery or in a media player.
|
||||
///
|
||||
/// 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 chat_get_neighboring_media(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
message_type: MessageViewtype,
|
||||
or_message_type2: Option<MessageViewtype>,
|
||||
or_message_type3: Option<MessageViewtype>,
|
||||
) -> Result<(Option<u32>, Option<u32>)> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
let msg_type: Viewtype = message_type.into();
|
||||
let msg_type2: Viewtype = or_message_type2.map(|v| v.into()).unwrap_or_default();
|
||||
let msg_type3: Viewtype = or_message_type3.map(|v| v.into()).unwrap_or_default();
|
||||
|
||||
let prev = chat::get_next_media(
|
||||
&ctx,
|
||||
MsgId::new(msg_id),
|
||||
chat::Direction::Backward,
|
||||
msg_type,
|
||||
msg_type2,
|
||||
msg_type3,
|
||||
)
|
||||
.await?
|
||||
.map(|id| id.to_u32());
|
||||
|
||||
let next = chat::get_next_media(
|
||||
&ctx,
|
||||
MsgId::new(msg_id),
|
||||
chat::Direction::Forward,
|
||||
msg_type,
|
||||
msg_type2,
|
||||
msg_type3,
|
||||
)
|
||||
.await?
|
||||
.map(|id| id.to_u32());
|
||||
|
||||
Ok((prev, next))
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// connectivity
|
||||
// ---------------------------------------------
|
||||
|
||||
@@ -37,6 +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: String,
|
||||
}
|
||||
|
||||
impl FullChat {
|
||||
@@ -80,6 +81,8 @@ impl FullChat {
|
||||
false
|
||||
};
|
||||
|
||||
let mailing_list_address = chat.get_mailinglist_addr().to_string();
|
||||
|
||||
Ok(FullChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
@@ -103,6 +106,7 @@ impl FullChat {
|
||||
ephemeral_timer,
|
||||
can_send,
|
||||
was_seen_recently,
|
||||
mailing_list_address,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat::chat::Chat;
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::download;
|
||||
@@ -286,3 +287,61 @@ impl From<download::DownloadState> for DownloadState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageNotificationInfo {
|
||||
id: u32,
|
||||
chat_id: u32,
|
||||
account_id: u32,
|
||||
|
||||
image: Option<String>,
|
||||
image_mime_type: Option<String>,
|
||||
|
||||
chat_name: String,
|
||||
chat_profile_image: Option<String>,
|
||||
|
||||
/// also known as summary_text1
|
||||
summary_prefix: Option<String>,
|
||||
/// also known as summary_text2
|
||||
summary_text: String,
|
||||
}
|
||||
|
||||
impl MessageNotificationInfo {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
|
||||
|
||||
let image = if matches!(
|
||||
message.get_viewtype(),
|
||||
Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
|
||||
) {
|
||||
message
|
||||
.get_file(context)
|
||||
.map(|path_buf| path_buf.to_str().map(|s| s.to_owned()))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let chat_profile_image = chat
|
||||
.get_profile_image(context)
|
||||
.await?
|
||||
.map(|path_buf| path_buf.to_str().map(|s| s.to_owned()))
|
||||
.unwrap_or_default();
|
||||
|
||||
let summary = message.get_summary(context, Some(&chat)).await?;
|
||||
|
||||
Ok(MessageNotificationInfo {
|
||||
id: msg_id.to_u32(),
|
||||
chat_id: message.get_chat_id().to_u32(),
|
||||
account_id: context.get_id(),
|
||||
image,
|
||||
image_mime_type: message.get_filemime(),
|
||||
chat_name: chat.name,
|
||||
chat_profile_image,
|
||||
summary_prefix: summary.prefix.map(|s| s.to_string()),
|
||||
summary_text: summary.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,9 @@ pub enum QrObject {
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
Login {
|
||||
address: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Qr> for QrObject {
|
||||
@@ -224,6 +227,7 @@ impl From<Qr> for QrObject {
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::Login { address, .. } => QrObject::Login { address },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) ->
|
||||
let (client, out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), api.clone());
|
||||
tokio::spawn(async move {
|
||||
let events = api.accounts.read().await.get_event_emitter().await;
|
||||
let events = api.accounts.read().await.get_event_emitter();
|
||||
while let Some(event) = events.recv().await {
|
||||
let event = event_to_json_rpc_notification(event);
|
||||
client.send_notification("event", Some(event)).await.ok();
|
||||
|
||||
@@ -385,7 +385,12 @@ export class RawClient {
|
||||
return (this._transport.request('markseen_msgs', [accountId, msgIds] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get ids of messages for a chat
|
||||
* (this api is similar to the cffi api, because it's still used that way by desktop)
|
||||
*
|
||||
* returns list with message ids and daymarkers (DC_MSG_ID_DAYMARKER = 9) if daymarkers are active
|
||||
*/
|
||||
public messageListGetMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('message_list_get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
@@ -400,6 +405,13 @@ export class RawClient {
|
||||
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 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>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete messages. The messages are deleted on the current device and
|
||||
* on the IMAP server.
|
||||
@@ -499,6 +511,18 @@ export class RawClient {
|
||||
return (this._transport.request('chat_get_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search next/previous message based on a given message and a list of types.
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
* 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 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)]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the network likely has come back.
|
||||
* or just that the network conditions might have changed
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
export type U32=number;
|
||||
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
|
||||
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
|
||||
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;}));
|
||||
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
|
||||
export type Usize=number;
|
||||
export type ChatListEntry=[U32,U32];
|
||||
export type I64=number;
|
||||
@@ -21,7 +21,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;};
|
||||
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:
|
||||
@@ -137,5 +137,14 @@ export type WebxdcMessageInfo={
|
||||
"internetAccess":boolean;};
|
||||
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
|
||||
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
|
||||
*/
|
||||
"summaryPrefix":(string|null);
|
||||
/**
|
||||
* also known as summary_text2
|
||||
*/
|
||||
"summaryText":string;};
|
||||
export type F64=number;
|
||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,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)>,U32,null,U32,null,U32,(U32)[],U32,U32,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,U32,null,U32,U32,U32,null,U32,U32,U32,null,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,Message,U32,(U32)[],Record<U32,Message>,U32,(U32)[],null,U32,U32,string,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,string,U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],null,U32,U32,U32,string,U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,null,U32,U32,(Message|null),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];
|
||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,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)>,U32,null,U32,null,U32,(U32)[],U32,U32,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,U32,null,U32,U32,U32,null,U32,U32,U32,null,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,Message,U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,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,string,U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],null,U32,U32,U32,string,U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,null,U32,U32,(Message|null),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];
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/deltachat.js",
|
||||
"name": "deltachat-jsonrpc-client",
|
||||
"name": "@deltachat/jsonrpc-client",
|
||||
"scripts": {
|
||||
"build": "run-s generate-bindings build:tsc build:bundle",
|
||||
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
||||
@@ -47,5 +47,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.94.0"
|
||||
"version": "1.95.0"
|
||||
}
|
||||
@@ -103,6 +103,7 @@ module.exports = {
|
||||
DC_QR_FPR_MISMATCH: 220,
|
||||
DC_QR_FPR_OK: 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR: 230,
|
||||
DC_QR_LOGIN: 520,
|
||||
DC_QR_REVIVE_VERIFYCONTACT: 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP: 512,
|
||||
DC_QR_TEXT: 330,
|
||||
|
||||
@@ -103,6 +103,7 @@ export enum C {
|
||||
DC_QR_FPR_MISMATCH = 220,
|
||||
DC_QR_FPR_OK = 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR = 230,
|
||||
DC_QR_LOGIN = 520,
|
||||
DC_QR_REVIVE_VERIFYCONTACT = 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP = 512,
|
||||
DC_QR_TEXT = 330,
|
||||
|
||||
@@ -12,11 +12,12 @@ const GITHUB_API_URL =
|
||||
|
||||
const file_url = process.env['URL']
|
||||
const GITHUB_TOKEN = process.env['GITHUB_TOKEN']
|
||||
const context = process.env['MSG_CONTEXT']
|
||||
|
||||
const STATUS_DATA = {
|
||||
state: 'success',
|
||||
description: '⏩ Click on "Details" to download →',
|
||||
context: 'Download the node-bindings.tar.gz',
|
||||
context: context || 'Download the node-bindings.tar.gz',
|
||||
target_url: base_url + file_url,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@deltachat/jsonrpc-client": "file:deltachat-jsonrpc/typescript",
|
||||
"debug": "^4.1.1",
|
||||
"napi-macros": "^2.0.0",
|
||||
"node-gyp-build": "^4.1.0"
|
||||
@@ -61,5 +60,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.94.0"
|
||||
"version": "1.95.0"
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class TestOfflineAccountBasic:
|
||||
d = ac1.get_info()
|
||||
assert d["arch"]
|
||||
assert d["number_of_chats"] == "0"
|
||||
assert d["bcc_self"] == "0"
|
||||
assert d["bcc_self"] == "1"
|
||||
|
||||
def test_is_not_configured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -118,7 +118,7 @@ class TestOfflineAccountBasic:
|
||||
def test_has_bccself(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
def test_selfcontact_if_unconfigured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
@@ -69,19 +69,19 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Get an account by its `id`:
|
||||
pub async fn get_account(&self, id: u32) -> Option<Context> {
|
||||
pub fn get_account(&self, id: u32) -> Option<Context> {
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Get the currently selected account.
|
||||
pub async fn get_selected_account(&self) -> Option<Context> {
|
||||
let id = self.config.get_selected_account().await;
|
||||
pub fn get_selected_account(&self) -> Option<Context> {
|
||||
let id = self.config.get_selected_account();
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Returns the currently selected account's id or None if no account is selected.
|
||||
pub async fn get_selected_account_id(&self) -> Option<u32> {
|
||||
match self.config.get_selected_account().await {
|
||||
pub fn get_selected_account_id(&self) -> Option<u32> {
|
||||
match self.config.get_selected_account() {
|
||||
0 => None,
|
||||
id => Some(id),
|
||||
}
|
||||
@@ -135,7 +135,7 @@ impl Accounts {
|
||||
ctx.stop_io().await;
|
||||
drop(ctx);
|
||||
|
||||
if let Some(cfg) = self.config.get_account(id).await {
|
||||
if let Some(cfg) = self.config.get_account(id) {
|
||||
// Spend up to 1 minute trying to remove the files.
|
||||
// Files may remain locked up to 30 seconds due to r2d2 bug:
|
||||
// https://github.com/sfackler/r2d2/issues/99
|
||||
@@ -171,7 +171,7 @@ impl Accounts {
|
||||
ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
|
||||
ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
|
||||
|
||||
let old_id = self.config.get_selected_account().await;
|
||||
let old_id = self.config.get_selected_account();
|
||||
|
||||
// create new account
|
||||
let account_config = self
|
||||
@@ -225,7 +225,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Get a list of all account ids.
|
||||
pub async fn get_all(&self) -> Vec<u32> {
|
||||
pub fn get_all(&self) -> Vec<u32> {
|
||||
self.accounts.keys().copied().collect()
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Returns event emitter.
|
||||
pub async fn get_event_emitter(&self) -> EventEmitter {
|
||||
pub fn get_event_emitter(&self) -> EventEmitter {
|
||||
self.events.get_emitter()
|
||||
}
|
||||
}
|
||||
@@ -380,7 +380,6 @@ impl Config {
|
||||
.context("failed to select just added account")?;
|
||||
let cfg = self
|
||||
.get_account(id)
|
||||
.await
|
||||
.context("failed to get just added account")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
@@ -402,11 +401,11 @@ impl Config {
|
||||
self.sync().await
|
||||
}
|
||||
|
||||
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
self.inner.accounts.iter().find(|e| e.id == id).cloned()
|
||||
}
|
||||
|
||||
pub async fn get_selected_account(&self) -> u32 {
|
||||
pub fn get_selected_account(&self) -> u32 {
|
||||
self.inner.selected_account
|
||||
}
|
||||
|
||||
@@ -458,7 +457,7 @@ mod tests {
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
|
||||
assert_eq!(accounts1.accounts.len(), 1);
|
||||
assert_eq!(accounts1.config.get_selected_account().await, 1);
|
||||
assert_eq!(accounts1.config.get_selected_account(), 1);
|
||||
|
||||
assert_eq!(accounts1.dir, accounts2.dir);
|
||||
assert_eq!(accounts1.config, accounts2.config,);
|
||||
@@ -472,23 +471,23 @@ mod tests {
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 2);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
assert_eq!(accounts.config.get_selected_account(), id);
|
||||
assert_eq!(accounts.accounts.len(), 2);
|
||||
|
||||
accounts.select_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
|
||||
accounts.remove_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
assert_eq!(accounts.config.get_selected_account(), 2);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
}
|
||||
|
||||
@@ -498,17 +497,17 @@ mod tests {
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
assert!(accounts.get_selected_account().is_none());
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
let id = accounts.add_account().await?;
|
||||
assert!(accounts.get_selected_account().await.is_some());
|
||||
assert!(accounts.get_selected_account().is_some());
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
assert_eq!(accounts.config.get_selected_account(), id);
|
||||
|
||||
accounts.remove_account(id).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
assert!(accounts.get_selected_account().is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -520,7 +519,7 @@ mod tests {
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
let extern_dbfile: PathBuf = dir.path().join("other");
|
||||
let ctx = Context::new(&extern_dbfile, 0, Events::new())
|
||||
@@ -537,9 +536,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
let ctx = accounts.get_selected_account().unwrap();
|
||||
assert_eq!(
|
||||
"me@mail.com",
|
||||
ctx.get_config(crate::config::Config::Addr)
|
||||
@@ -562,7 +561,7 @@ mod tests {
|
||||
assert_eq!(id, expected_id);
|
||||
}
|
||||
|
||||
let ids = accounts.get_all().await;
|
||||
let ids = accounts.get_all();
|
||||
for (i, expected_id) in (1..10).enumerate() {
|
||||
assert_eq!(ids.get(i), Some(&expected_id));
|
||||
}
|
||||
@@ -577,16 +576,16 @@ mod tests {
|
||||
let (id0, id1, id2) = {
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
let ids = accounts.get_all().await;
|
||||
let ids = accounts.get_all();
|
||||
assert_eq!(ids.len(), 1);
|
||||
|
||||
let id0 = *ids.first().unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
let ctx = accounts.get_account(id0).unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
|
||||
.await?;
|
||||
|
||||
let id1 = accounts.add_account().await?;
|
||||
let ctx = accounts.get_account(id1).await.unwrap();
|
||||
let ctx = accounts.get_account(id1).unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
|
||||
.await?;
|
||||
|
||||
@@ -597,7 +596,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let id2 = accounts.add_account().await?;
|
||||
let ctx = accounts.get_account(id2).await.unwrap();
|
||||
let ctx = accounts.get_account(id2).unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
|
||||
.await?;
|
||||
|
||||
@@ -611,31 +610,31 @@ mod tests {
|
||||
|
||||
let (id0_reopened, id1_reopened, id2_reopened) = {
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
let ctx = accounts.get_selected_account().unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("two@example.org".to_string())
|
||||
);
|
||||
|
||||
let ids = accounts.get_all().await;
|
||||
let ids = accounts.get_all();
|
||||
assert_eq!(ids.len(), 3);
|
||||
|
||||
let id0 = *ids.first().unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
let ctx = accounts.get_account(id0).unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("one@example.org".to_string())
|
||||
);
|
||||
|
||||
let id1 = *ids.get(1).unwrap();
|
||||
let t = accounts.get_account(id1).await.unwrap();
|
||||
let t = accounts.get_account(id1).unwrap();
|
||||
assert_eq!(
|
||||
t.get_config(crate::config::Config::Addr).await?,
|
||||
Some("two@example.org".to_string())
|
||||
);
|
||||
|
||||
let id2 = *ids.get(2).unwrap();
|
||||
let ctx = accounts.get_account(id2).await.unwrap();
|
||||
let ctx = accounts.get_account(id2).unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("three@example.org".to_string())
|
||||
@@ -661,7 +660,7 @@ mod tests {
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
|
||||
// Create event emitter.
|
||||
let event_emitter = accounts.get_event_emitter().await;
|
||||
let event_emitter = accounts.get_event_emitter();
|
||||
|
||||
// Test that event emitter does not return `None` immediately.
|
||||
let duration = std::time::Duration::from_millis(1);
|
||||
@@ -692,7 +691,6 @@ mod tests {
|
||||
.context("failed to add closed account")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.await
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.id, account_id);
|
||||
let passphrase_set_success = account
|
||||
@@ -707,7 +705,6 @@ mod tests {
|
||||
.context("failed to create second accounts manager")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.await
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.is_open().await, false);
|
||||
|
||||
|
||||
11
src/blob.rs
11
src/blob.rs
@@ -326,10 +326,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
|
||||
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
|
||||
if let Some(new_name) = self
|
||||
.recode_to_size(context, blob_abs, img_wh, Some(20_000))
|
||||
.await?
|
||||
{
|
||||
if let Some(new_name) = self.recode_to_size(context, blob_abs, img_wh, Some(20_000))? {
|
||||
self.name = new_name;
|
||||
}
|
||||
Ok(())
|
||||
@@ -352,8 +349,7 @@ impl<'a> BlobObject<'a> {
|
||||
};
|
||||
|
||||
if self
|
||||
.recode_to_size(context, blob_abs, img_wh, None)
|
||||
.await?
|
||||
.recode_to_size(context, blob_abs, img_wh, None)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(format_err!(
|
||||
@@ -363,7 +359,7 @@ impl<'a> BlobObject<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recode_to_size(
|
||||
fn recode_to_size(
|
||||
&self,
|
||||
context: &Context,
|
||||
mut blob_abs: PathBuf,
|
||||
@@ -746,7 +742,6 @@ mod tests {
|
||||
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
|
||||
|
||||
blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(file_size(&avatar_blob).await <= 3000);
|
||||
assert!(file_size(&avatar_blob).await > 2000);
|
||||
|
||||
34
src/chat.rs
34
src/chat.rs
@@ -4554,26 +4554,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_protection() {
|
||||
async fn test_set_protection() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
t.set_config_bool(Config::BccSelf, false).await?;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
// enable protection on unpromoted chat, the info-message is added via add_info_msg()
|
||||
chat_id
|
||||
.set_protection(&t, ProtectionStatus::Protected)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
@@ -4584,10 +4582,9 @@ mod tests {
|
||||
// disable protection again, still unpromoted
|
||||
chat_id
|
||||
.set_protection(&t, ProtectionStatus::Unprotected)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
@@ -4597,21 +4594,20 @@ mod tests {
|
||||
assert_eq!(msg.get_state(), MessageState::InNoticed);
|
||||
|
||||
// send a message, this switches to promoted state
|
||||
send_text_msg(&t, chat_id, "hi!".to_string()).await.unwrap();
|
||||
send_text_msg(&t, chat_id, "hi!".to_string()).await?;
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_unpromoted());
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
|
||||
assert_eq!(msgs.len(), 3);
|
||||
|
||||
// enable protection on promoted chat, the info-message is sent via send_msg() this time
|
||||
chat_id
|
||||
.set_protection(&t, ProtectionStatus::Protected)
|
||||
.await
|
||||
.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
assert!(!chat.is_unpromoted());
|
||||
|
||||
@@ -4619,6 +4615,8 @@ mod tests {
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -55,7 +55,7 @@ pub enum Config {
|
||||
Selfstatus,
|
||||
Selfavatar,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
#[strum(props(default = "1"))]
|
||||
BccSelf,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
@@ -198,7 +198,7 @@ impl Context {
|
||||
let rel_path = self.sql.get_raw_config(key).await?;
|
||||
rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned())
|
||||
}
|
||||
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
|
||||
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
|
||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||
_ => self.sql.get_raw_config(key).await?,
|
||||
|
||||
@@ -579,8 +579,7 @@ async fn try_imap_one_param(
|
||||
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
|
||||
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r).await
|
||||
{
|
||||
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
return Err(ConfigurationError {
|
||||
|
||||
@@ -167,6 +167,11 @@ pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
|
||||
|
||||
/// String that indicates that something is left out or truncated.
|
||||
pub const DC_ELLIPSIS: &str = "[...]";
|
||||
// how many lines desktop can display when fullscreen (fullscreen at zoomlevel 1x)
|
||||
// (taken from "subjective" testing what looks ok)
|
||||
pub const DC_DESIRED_TEXT_LINES: usize = 38;
|
||||
// how many chars desktop can display per line (from "subjective" testing)
|
||||
pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100;
|
||||
|
||||
/// Message length limit.
|
||||
///
|
||||
@@ -176,7 +181,7 @@ pub const DC_ELLIPSIS: &str = "[...]";
|
||||
///
|
||||
/// Note that for simplicity maximum length is defined as the number of Unicode Scalar Values (Rust
|
||||
/// `char`s), not Unicode Grapheme Clusters.
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = 5000;
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES;
|
||||
|
||||
// Flags for empty server job
|
||||
|
||||
|
||||
@@ -2278,7 +2278,7 @@ Hi."#;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_was_seen_recently() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ impl Context {
|
||||
if !blobdir.exists() {
|
||||
tokio::fs::create_dir_all(&blobdir).await?;
|
||||
}
|
||||
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events).await?;
|
||||
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events)?;
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ impl Context {
|
||||
self.sql.check_passphrase(passphrase).await
|
||||
}
|
||||
|
||||
pub(crate) async fn with_blobdir(
|
||||
pub(crate) fn with_blobdir(
|
||||
dbfile: PathBuf,
|
||||
blobdir: PathBuf,
|
||||
id: u32,
|
||||
@@ -883,7 +883,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new()).await;
|
||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new());
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -892,7 +892,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("blobs");
|
||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new()).await;
|
||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new());
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
|
||||
158
src/ephemeral.rs
158
src/ephemeral.rs
@@ -36,15 +36,21 @@
|
||||
//!
|
||||
//! ## How messages are deleted
|
||||
//!
|
||||
//! When the message is deleted locally, its contents is removed and
|
||||
//! it is moved to the trash chat. This database entry is then used to
|
||||
//! track the Message-ID and corresponding IMAP folder and UID until
|
||||
//! the message is deleted from the server. Vice versa, when device
|
||||
//! deletes the message from the server, it removes IMAP folder and
|
||||
//! UID information, but keeps the message contents. When database
|
||||
//! entry is both moved to trash chat and does not contain UID
|
||||
//! information, it is deleted from the database, leaving no trace of
|
||||
//! the message.
|
||||
//! When Delta Chat deletes the message locally, it moves the message
|
||||
//! to the trash chat and removes actual message contents. Messages in
|
||||
//! the trash chat are called "tombstones" and track the Message-ID to
|
||||
//! prevent accidental redownloading of the message from the server,
|
||||
//! e.g. in case of UID validity change.
|
||||
//!
|
||||
//! Vice versa, when Delta Chat deletes the message from the server,
|
||||
//! it removes IMAP folder and UID row from the `imap` table, but
|
||||
//! keeps the message in the `msgs` table.
|
||||
//!
|
||||
//! Delta Chat eventually removes tombstones from the `msgs` table,
|
||||
//! leaving no trace of the message, when it thinks there are no more
|
||||
//! copies of the message stored on the server, i.e. when there is no
|
||||
//! corresponding `imap` table entry. This is done in the
|
||||
//! `prune_tombstones()` procedure during housekeeping.
|
||||
//!
|
||||
//! ## When messages are deleted
|
||||
//!
|
||||
@@ -326,35 +332,35 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes messages which are expired according to
|
||||
/// Selects messages which are expired according to
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// Returns true if any message is deleted, so caller can emit
|
||||
/// MsgsChanged event. If nothing has been deleted, returns
|
||||
/// false. This function does not emit the MsgsChanged event itself,
|
||||
/// because it is also called when chatlist is reloaded, and emitting
|
||||
/// MsgsChanged there will cause infinite reload loop.
|
||||
pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Result<()> {
|
||||
let mut updated = context
|
||||
/// For each message a row ID, chat id and viewtype is returned.
|
||||
async fn select_expired_messages(
|
||||
context: &Context,
|
||||
now: i64,
|
||||
) -> Result<Vec<(MsgId, ChatId, Viewtype)>> {
|
||||
let mut rows = context
|
||||
.sql
|
||||
.execute(
|
||||
// If you change which information is removed here, also change MsgId::trash() and
|
||||
// which information receive_imf::add_parts() still adds to the db if the chat_id is TRASH
|
||||
.query_map(
|
||||
r#"
|
||||
UPDATE msgs
|
||||
SET
|
||||
chat_id=?, txt='', subject='', txt_raw='',
|
||||
mime_headers='', from_id=0, to_id=0, param=''
|
||||
SELECT id, chat_id, type
|
||||
FROM msgs
|
||||
WHERE
|
||||
ephemeral_timestamp != 0
|
||||
AND ephemeral_timestamp <= ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
paramsv![DC_CHAT_ID_TRASH, now, DC_CHAT_ID_TRASH],
|
||||
paramsv![now, DC_CHAT_ID_TRASH],
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
Ok((id, chat_id, viewtype))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await
|
||||
.context("update failed")?
|
||||
> 0;
|
||||
.await?;
|
||||
|
||||
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
|
||||
let self_chat_id = ChatId::lookup_by_contact(context, ContactId::SELF)
|
||||
@@ -366,36 +372,81 @@ WHERE
|
||||
|
||||
let threshold_timestamp = now.saturating_sub(delete_device_after);
|
||||
|
||||
// Delete expired messages
|
||||
//
|
||||
// Only update the rows that have to be updated, to avoid emitting
|
||||
// unnecessary "chat modified" events.
|
||||
let rows_modified = context
|
||||
let rows_expired = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET chat_id = ?, txt = '', subject='', txt_raw='', \
|
||||
mime_headers='', from_id=0, to_id=0, param='' \
|
||||
WHERE timestamp < ? \
|
||||
AND chat_id > ? \
|
||||
AND chat_id != ? \
|
||||
AND chat_id != ?",
|
||||
.query_map(
|
||||
r#"
|
||||
SELECT id, chat_id, type
|
||||
FROM msgs
|
||||
WHERE
|
||||
timestamp < ?
|
||||
AND chat_id > ?
|
||||
AND chat_id != ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
paramsv![
|
||||
DC_CHAT_ID_TRASH,
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
Ok((id, chat_id, viewtype))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await
|
||||
.context("deleted update failed")?;
|
||||
.await?;
|
||||
|
||||
updated |= rows_modified > 0;
|
||||
rows.extend(rows_expired);
|
||||
}
|
||||
|
||||
if updated {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Deletes messages which are expired according to
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// Emits relevant `MsgsChanged` and `WebxdcInstanceDeleted` events
|
||||
/// if messages are deleted.
|
||||
pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Result<()> {
|
||||
let rows = select_expired_messages(context, now).await?;
|
||||
|
||||
if !rows.is_empty() {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
// If you change which information is removed here, also change MsgId::trash() and
|
||||
// which information receive_imf::add_parts() still adds to the db if the chat_id is TRASH
|
||||
&format!(
|
||||
r#"
|
||||
UPDATE msgs
|
||||
SET
|
||||
chat_id=?, txt='', subject='', txt_raw='',
|
||||
mime_headers='', from_id=0, to_id=0, param=''
|
||||
WHERE id IN ({})
|
||||
"#,
|
||||
sql::repeat_vars(rows.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
std::iter::once(&DC_CHAT_ID_TRASH as &dyn crate::ToSql).chain(
|
||||
rows.iter()
|
||||
.map(|(msg_id, _chat_id, _viewtype)| msg_id as &dyn crate::ToSql),
|
||||
),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.context("update failed")?;
|
||||
|
||||
for (msg_id, chat_id, viewtype) in rows {
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
|
||||
if viewtype == Viewtype::Webxdc {
|
||||
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -950,6 +1001,19 @@ mod tests {
|
||||
|
||||
assert!(next_expiration < deleted_at);
|
||||
delete_expired_messages(t, deleted_at).await?;
|
||||
t.evtracker
|
||||
.get_matching(|evt| {
|
||||
if let EventType::MsgsChanged {
|
||||
msg_id: event_msg_id,
|
||||
..
|
||||
} = evt
|
||||
{
|
||||
*event_msg_id == msg_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let loaded = Message::load_from_db(t, msg_id).await?;
|
||||
assert_eq!(loaded.text.unwrap(), "");
|
||||
|
||||
@@ -98,7 +98,7 @@ impl HtmlMsgParser {
|
||||
|
||||
if parser.html.is_empty() {
|
||||
if let Some(plain) = &parser.plain {
|
||||
parser.html = plain.to_html().await;
|
||||
parser.html = plain.to_html();
|
||||
}
|
||||
} else {
|
||||
parser.cid_to_data_recursive(context, &parsedmail).await?;
|
||||
|
||||
@@ -237,7 +237,7 @@ impl Imap {
|
||||
/// Creates new disconnected IMAP client using the specific login parameters.
|
||||
///
|
||||
/// `addr` is used to renew token if OAuth2 authentication is used.
|
||||
pub async fn new(
|
||||
pub fn new(
|
||||
lp: &ServerLoginParam,
|
||||
socks5_config: Option<Socks5Config>,
|
||||
addr: &str,
|
||||
@@ -303,8 +303,7 @@ impl Imap {
|
||||
provider.strict_tls
|
||||
}),
|
||||
idle_interrupt,
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
Ok(imap)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::{context::Context, provider::Socket};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum CertificateChecks {
|
||||
|
||||
@@ -1177,17 +1177,17 @@ impl<'a> MimeFactory<'a> {
|
||||
if command == SystemMessage::MultiDeviceSync && self.is_e2ee_guaranteed() {
|
||||
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||
let ids = self.msg.param.get(Param::Arg2).unwrap_or_default();
|
||||
parts.push(context.build_sync_part(json.to_string()).await);
|
||||
parts.push(context.build_sync_part(json.to_string()));
|
||||
self.sync_ids_to_delete = Some(ids.to_string());
|
||||
} else if command == SystemMessage::WebxdcStatusUpdate {
|
||||
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||
parts.push(context.build_status_update_part(json).await);
|
||||
parts.push(context.build_status_update_part(json));
|
||||
} else if self.msg.viewtype == Viewtype::Webxdc {
|
||||
if let Some(json) = context
|
||||
.render_webxdc_status_update_object(self.msg.id, None)
|
||||
.await?
|
||||
{
|
||||
parts.push(context.build_status_update_part(&json).await);
|
||||
parts.push(context.build_status_update_part(&json));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use once_cell::sync::Lazy;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::constants::{DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
|
||||
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::{create_decryption_info, try_decrypt};
|
||||
@@ -28,7 +28,7 @@ use crate::peerstate::Peerstate;
|
||||
use crate::simplify::{simplify, SimplifiedText};
|
||||
use crate::stock_str;
|
||||
use crate::sync::SyncItems;
|
||||
use crate::tools::{get_filemeta, parse_receive_headers, truncate};
|
||||
use crate::tools::{get_filemeta, parse_receive_headers, truncate_by_lines};
|
||||
|
||||
/// A parsed MIME message.
|
||||
///
|
||||
@@ -392,15 +392,10 @@ impl MimeMessage {
|
||||
/// Parses system messages.
|
||||
fn parse_system_message_headers(&mut self, context: &Context) {
|
||||
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() {
|
||||
self.parts = self
|
||||
.parts
|
||||
.iter()
|
||||
.filter(|part| {
|
||||
part.mimetype.is_none()
|
||||
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
self.parts.retain(|part| {
|
||||
part.mimetype.is_none()
|
||||
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
|
||||
});
|
||||
|
||||
if self.parts.len() == 1 {
|
||||
self.is_system_message = SystemMessage::AutocryptSetupMessage;
|
||||
@@ -1012,14 +1007,15 @@ impl MimeMessage {
|
||||
(simplified_txt, top_quote)
|
||||
};
|
||||
|
||||
let simplified_txt = if simplified_txt.chars().count()
|
||||
> DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len()
|
||||
{
|
||||
self.is_mime_modified = true;
|
||||
truncate(&*simplified_txt, DC_DESIRED_TEXT_LEN).to_string()
|
||||
} else {
|
||||
simplified_txt
|
||||
};
|
||||
// Truncate text if it has too many lines
|
||||
let (simplified_txt, was_truncated) = truncate_by_lines(
|
||||
simplified_txt,
|
||||
DC_DESIRED_TEXT_LINES,
|
||||
DC_DESIRED_TEXT_LINE_LEN,
|
||||
);
|
||||
if was_truncated {
|
||||
self.is_mime_modified = was_truncated;
|
||||
}
|
||||
|
||||
if !simplified_txt.is_empty() || simplified_quote.is_some() {
|
||||
let mut part = Part {
|
||||
@@ -1817,7 +1813,7 @@ mod tests {
|
||||
use crate::{
|
||||
chatlist::Chatlist,
|
||||
config::Config,
|
||||
constants::Blocked,
|
||||
constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
|
||||
message::{Message, MessageState, MessengerMessage},
|
||||
receive_imf::receive_imf,
|
||||
test_utils::TestContext,
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct PlainText {
|
||||
impl PlainText {
|
||||
/// Convert plain text to HTML.
|
||||
/// The function handles quotes, links, fixed and floating text paragraphs.
|
||||
pub async fn to_html(&self) -> String {
|
||||
pub fn to_html(&self) -> String {
|
||||
static LINKIFY_MAIL_RE: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r#"\b([\w.\-+]+@[\w.\-]+)\b"#).unwrap());
|
||||
|
||||
@@ -111,8 +111,7 @@ http://link-at-start-of-line.org
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r##"<!DOCTYPE html>
|
||||
@@ -134,8 +133,7 @@ line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -153,8 +151,7 @@ line with <<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.l
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -172,8 +169,7 @@ line with nohttp://no.link here<br/>
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -191,8 +187,7 @@ just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:an
|
||||
flowed: true,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -213,8 +208,7 @@ line still line<br/>
|
||||
flowed: true,
|
||||
delsp: true,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -235,8 +229,7 @@ linestill line<br/>
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
|
||||
112
src/qr.rs
112
src/qr.rs
@@ -1,5 +1,8 @@
|
||||
//! # QR code module.
|
||||
|
||||
mod dclogin_scheme;
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context as _, Error, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use percent_encoding::percent_decode_str;
|
||||
@@ -17,8 +20,11 @@ use crate::peerstate::Peerstate;
|
||||
use crate::tools::time;
|
||||
use crate::{token, EventType};
|
||||
|
||||
use self::dclogin_scheme::configure_from_login_qr;
|
||||
|
||||
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
|
||||
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
|
||||
pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
|
||||
const DCWEBRTC_SCHEME: &str = "DCWEBRTC:";
|
||||
const MAILTO_SCHEME: &str = "mailto:";
|
||||
const MATMSG_SCHEME: &str = "MATMSG:";
|
||||
@@ -97,6 +103,10 @@ pub enum Qr {
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
Login {
|
||||
address: String,
|
||||
options: LoginOptions,
|
||||
},
|
||||
}
|
||||
|
||||
fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
|
||||
@@ -115,6 +125,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
.context("failed to decode OPENPGP4FPR QR code")?
|
||||
} else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) {
|
||||
decode_account(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) {
|
||||
dclogin_scheme::decode_login(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
|
||||
decode_webrtc_instance(context, qr)?
|
||||
} else if qr.starts_with(MAILTO_SCHEME) {
|
||||
@@ -462,6 +474,9 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
context.sync_qr_code_tokens(chat_id).await?;
|
||||
context.send_sync_msg().await?;
|
||||
}
|
||||
Qr::Login { address, options } => {
|
||||
configure_from_login_qr(context, &address, options).await?
|
||||
}
|
||||
_ => bail!("qr code {:?} does not contain config", qr),
|
||||
}
|
||||
|
||||
@@ -1024,6 +1039,103 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_and_apply_dclogin() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let result = check_qr(&ctx.ctx, "dclogin:usename+extension@host?p=1234&v=1").await?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "usename+extension@host".to_owned());
|
||||
|
||||
if let LoginOptions::V1 { mail_pw, .. } = options {
|
||||
assert_eq!(mail_pw, "1234".to_owned());
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
|
||||
assert!(ctx.ctx.get_config(Config::Addr).await?.is_none());
|
||||
assert!(ctx.ctx.get_config(Config::MailPw).await?.is_none());
|
||||
|
||||
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&v=1").await?;
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::Addr).await?,
|
||||
Some("username+extension@host".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailPw).await?,
|
||||
Some("1234".to_owned())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_and_apply_dclogin_advanced_options() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&spw=4321&sh=send.host&sp=7273&su=SendUser&ih=host.tld&ip=4343&iu=user&ipw=password&is=ssl&ic=1&sc=3&ss=plain&v=1").await?;
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::Addr).await?,
|
||||
Some("username+extension@host".to_owned())
|
||||
);
|
||||
|
||||
// `p=1234` is ignored, because `ipw=password` is set
|
||||
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailServer).await?,
|
||||
Some("host.tld".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailPort).await?,
|
||||
Some("4343".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailUser).await?,
|
||||
Some("user".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailPw).await?,
|
||||
Some("password".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailSecurity).await?,
|
||||
Some("1".to_owned()) // ssl
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::ImapCertificateChecks).await?,
|
||||
Some("1".to_owned())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendPw).await?,
|
||||
Some("4321".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendServer).await?,
|
||||
Some("send.host".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendPort).await?,
|
||||
Some("7273".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendUser).await?,
|
||||
Some("SendUser".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SmtpCertificateChecks).await?,
|
||||
Some("3".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendSecurity).await?,
|
||||
Some("3".to_owned()) // plain
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_account() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
388
src/qr/dclogin_scheme.rs
Normal file
388
src/qr/dclogin_scheme.rs
Normal file
@@ -0,0 +1,388 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::provider::Socket;
|
||||
use crate::{contact, login_param::CertificateChecks};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use num_traits::cast::ToPrimitive;
|
||||
|
||||
use super::{Qr, DCLOGIN_SCHEME};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LoginOptions {
|
||||
UnsuportedVersion(u32),
|
||||
V1 {
|
||||
mail_pw: String,
|
||||
imap_host: Option<String>,
|
||||
imap_port: Option<u16>,
|
||||
imap_username: Option<String>,
|
||||
imap_password: Option<String>,
|
||||
imap_security: Option<Socket>,
|
||||
imap_certificate_checks: Option<CertificateChecks>,
|
||||
smtp_host: Option<String>,
|
||||
smtp_port: Option<u16>,
|
||||
smtp_username: Option<String>,
|
||||
smtp_password: Option<String>,
|
||||
smtp_security: Option<Socket>,
|
||||
smtp_certificate_checks: Option<CertificateChecks>,
|
||||
},
|
||||
}
|
||||
|
||||
/// scheme: `dclogin://user@host/?p=password&v=1[&options]`
|
||||
/// read more about the scheme at <https://github.com/deltachat/interface/blob/master/uri-schemes.md#DCLOGIN>
|
||||
pub(super) fn decode_login(qr: &str) -> Result<Qr> {
|
||||
let url = url::Url::parse(qr).with_context(|| format!("Malformed url: {:?}", qr))?;
|
||||
|
||||
let url_without_scheme = qr
|
||||
.get(DCLOGIN_SCHEME.len()..)
|
||||
.context("invalid DCLOGIN payload E1")?;
|
||||
let payload = url_without_scheme
|
||||
.strip_prefix("//")
|
||||
.unwrap_or(url_without_scheme);
|
||||
|
||||
let addr = payload
|
||||
.split(|c| c == '?' || c == '/')
|
||||
.next()
|
||||
.context("invalid DCLOGIN payload E3")?;
|
||||
|
||||
if url.scheme().eq_ignore_ascii_case("dclogin") {
|
||||
let options = url.query_pairs();
|
||||
if options.count() == 0 {
|
||||
bail!("invalid DCLOGIN payload E4")
|
||||
}
|
||||
// load options into hashmap
|
||||
let parameter_map: HashMap<String, String> = options
|
||||
.map(|(key, value)| (key.into_owned(), value.into_owned()))
|
||||
.collect();
|
||||
|
||||
// check if username is there
|
||||
if !contact::may_be_valid_addr(addr) {
|
||||
bail!("invalid DCLOGIN payload: invalid username E5");
|
||||
}
|
||||
|
||||
// apply to result struct
|
||||
let options: LoginOptions = match parameter_map.get("v").map(|i| i.parse::<u32>()) {
|
||||
Some(Ok(1)) => LoginOptions::V1 {
|
||||
mail_pw: parameter_map
|
||||
.get("p")
|
||||
.map(|s| s.to_owned())
|
||||
.context("password missing")?,
|
||||
imap_host: parameter_map.get("ih").map(|s| s.to_owned()),
|
||||
imap_port: parse_port(parameter_map.get("ip"))
|
||||
.context("could not parse imap port")?,
|
||||
imap_username: parameter_map.get("iu").map(|s| s.to_owned()),
|
||||
imap_password: parameter_map.get("ipw").map(|s| s.to_owned()),
|
||||
imap_security: parse_socket_security(parameter_map.get("is"))?,
|
||||
imap_certificate_checks: parse_certificate_checks(parameter_map.get("ic"))?,
|
||||
smtp_host: parameter_map.get("sh").map(|s| s.to_owned()),
|
||||
smtp_port: parse_port(parameter_map.get("sp"))
|
||||
.context("could not parse smtp port")?,
|
||||
smtp_username: parameter_map.get("su").map(|s| s.to_owned()),
|
||||
smtp_password: parameter_map.get("spw").map(|s| s.to_owned()),
|
||||
smtp_security: parse_socket_security(parameter_map.get("ss"))?,
|
||||
smtp_certificate_checks: parse_certificate_checks(parameter_map.get("sc"))?,
|
||||
},
|
||||
Some(Ok(v)) => LoginOptions::UnsuportedVersion(v),
|
||||
Some(Err(_)) => bail!("version could not be parsed as number E6"),
|
||||
None => bail!("invalid DCLOGIN payload: version missing E7"),
|
||||
};
|
||||
|
||||
Ok(Qr::Login {
|
||||
address: addr.to_owned(),
|
||||
options,
|
||||
})
|
||||
} else {
|
||||
bail!("Bad scheme for account URL: {:?}.", payload);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_port(port: Option<&String>) -> core::result::Result<Option<u16>, std::num::ParseIntError> {
|
||||
match port {
|
||||
Some(p) => Ok(Some(p.parse::<u16>()?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_socket_security(security: Option<&String>) -> Result<Option<Socket>> {
|
||||
Ok(match security.map(|s| s.as_str()) {
|
||||
Some("ssl") => Some(Socket::Ssl),
|
||||
Some("starttls") => Some(Socket::Starttls),
|
||||
Some("default") => Some(Socket::Automatic),
|
||||
Some("plain") => Some(Socket::Plain),
|
||||
Some(other) => bail!("Unknown security level: {}", other),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_certificate_checks(
|
||||
certificate_checks: Option<&String>,
|
||||
) -> Result<Option<CertificateChecks>> {
|
||||
Ok(match certificate_checks.map(|s| s.as_str()) {
|
||||
Some("0") => Some(CertificateChecks::Automatic),
|
||||
Some("1") => Some(CertificateChecks::Strict),
|
||||
Some("3") => Some(CertificateChecks::AcceptInvalidCertificates),
|
||||
Some(other) => bail!("Unknown certificatecheck level: {}", other),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn configure_from_login_qr(
|
||||
context: &Context,
|
||||
address: &str,
|
||||
options: LoginOptions,
|
||||
) -> Result<()> {
|
||||
context.set_config(Config::Addr, Some(address)).await?;
|
||||
|
||||
match options {
|
||||
LoginOptions::V1 {
|
||||
mail_pw,
|
||||
imap_host,
|
||||
imap_port,
|
||||
imap_username,
|
||||
imap_password,
|
||||
imap_security,
|
||||
imap_certificate_checks,
|
||||
smtp_host,
|
||||
smtp_port,
|
||||
smtp_username,
|
||||
smtp_password,
|
||||
smtp_security,
|
||||
smtp_certificate_checks,
|
||||
} => {
|
||||
context.set_config(Config::MailPw, Some(&mail_pw)).await?;
|
||||
if let Some(value) = imap_host {
|
||||
context.set_config(Config::MailServer, Some(&value)).await?;
|
||||
}
|
||||
if let Some(value) = imap_port {
|
||||
context
|
||||
.set_config(Config::MailPort, Some(&value.to_string()))
|
||||
.await?;
|
||||
}
|
||||
if let Some(value) = imap_username {
|
||||
context.set_config(Config::MailUser, Some(&value)).await?;
|
||||
}
|
||||
if let Some(value) = imap_password {
|
||||
context.set_config(Config::MailPw, Some(&value)).await?;
|
||||
}
|
||||
if let Some(value) = imap_security {
|
||||
let code = value
|
||||
.to_u8()
|
||||
.context("could not convert imap security value to number")?;
|
||||
context
|
||||
.set_config(Config::MailSecurity, Some(&code.to_string()))
|
||||
.await?;
|
||||
}
|
||||
if let Some(value) = imap_certificate_checks {
|
||||
let code = value
|
||||
.to_u32()
|
||||
.context("could not convert imap certificate checks value to number")?;
|
||||
context
|
||||
.set_config(Config::ImapCertificateChecks, Some(&code.to_string()))
|
||||
.await?;
|
||||
}
|
||||
if let Some(value) = smtp_host {
|
||||
context.set_config(Config::SendServer, Some(&value)).await?;
|
||||
}
|
||||
if let Some(value) = smtp_port {
|
||||
context
|
||||
.set_config(Config::SendPort, Some(&value.to_string()))
|
||||
.await?;
|
||||
}
|
||||
if let Some(value) = smtp_username {
|
||||
context.set_config(Config::SendUser, Some(&value)).await?;
|
||||
}
|
||||
if let Some(value) = smtp_password {
|
||||
context.set_config(Config::SendPw, Some(&value)).await?;
|
||||
}
|
||||
if let Some(value) = smtp_security {
|
||||
let code = value
|
||||
.to_u8()
|
||||
.context("could not convert smtp security value to number")?;
|
||||
context
|
||||
.set_config(Config::SendSecurity, Some(&code.to_string()))
|
||||
.await?;
|
||||
}
|
||||
if let Some(value) = smtp_certificate_checks {
|
||||
let code = value
|
||||
.to_u32()
|
||||
.context("could not convert smtp certificate checks value to number")?;
|
||||
context
|
||||
.set_config(Config::SmtpCertificateChecks, Some(&code.to_string()))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => bail!(
|
||||
"DeltaChat does not understand this QR Code yet, please update the app and try again."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{decode_login, LoginOptions};
|
||||
use crate::{login_param::CertificateChecks, provider::Socket, qr::Qr};
|
||||
use anyhow::{self, bail};
|
||||
|
||||
macro_rules! login_options_just_pw {
|
||||
($pw: expr) => {
|
||||
LoginOptions::V1 {
|
||||
mail_pw: $pw,
|
||||
imap_host: None,
|
||||
imap_port: None,
|
||||
imap_username: None,
|
||||
imap_password: None,
|
||||
imap_security: None,
|
||||
imap_certificate_checks: None,
|
||||
smtp_host: None,
|
||||
smtp_port: None,
|
||||
smtp_username: None,
|
||||
smtp_password: None,
|
||||
smtp_security: None,
|
||||
smtp_certificate_checks: None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimal_no_options() -> anyhow::Result<()> {
|
||||
let result = decode_login("dclogin://email@host.tld?p=123&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "email@host.tld".to_owned());
|
||||
assert_eq!(options, login_options_just_pw!("123".to_owned()));
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
let result = decode_login("dclogin://email@host.tld/?p=123456&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "email@host.tld".to_owned());
|
||||
assert_eq!(options, login_options_just_pw!("123456".to_owned()));
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
let result = decode_login("dclogin://email@host.tld/ignored/path?p=123456&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "email@host.tld".to_owned());
|
||||
assert_eq!(options, login_options_just_pw!("123456".to_owned()));
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn minimal_no_options_no_double_slash() -> anyhow::Result<()> {
|
||||
let result = decode_login("dclogin:email@host.tld?p=123&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "email@host.tld".to_owned());
|
||||
assert_eq!(options, login_options_just_pw!("123".to_owned()));
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
let result = decode_login("dclogin:email@host.tld/?p=123456&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "email@host.tld".to_owned());
|
||||
assert_eq!(options, login_options_just_pw!("123456".to_owned()));
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
let result = decode_login("dclogin:email@host.tld/ignored/path?p=123456&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "email@host.tld".to_owned());
|
||||
assert_eq!(options, login_options_just_pw!("123456".to_owned()));
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_version_set() {
|
||||
assert!(decode_login("dclogin:email@host.tld?p=123").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_version_set() {
|
||||
assert!(decode_login("dclogin:email@host.tld?p=123&v=").is_err());
|
||||
assert!(decode_login("dclogin:email@host.tld?p=123&v=%40").is_err());
|
||||
assert!(decode_login("dclogin:email@host.tld?p=123&v=-20").is_err());
|
||||
assert!(decode_login("dclogin:email@host.tld?p=123&v=hi").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_too_new() -> anyhow::Result<()> {
|
||||
let result = decode_login("dclogin:email@host.tld/?p=123456&v=2")?;
|
||||
if let Qr::Login { options, .. } = result {
|
||||
assert_eq!(options, LoginOptions::UnsuportedVersion(2));
|
||||
} else {
|
||||
bail!("wrong type");
|
||||
}
|
||||
let result = decode_login("dclogin:email@host.tld/?p=123456&v=5")?;
|
||||
if let Qr::Login { options, .. } = result {
|
||||
assert_eq!(options, LoginOptions::UnsuportedVersion(5));
|
||||
} else {
|
||||
bail!("wrong type");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_advanced_options() -> anyhow::Result<()> {
|
||||
let result = decode_login(
|
||||
"dclogin:email@host.tld?p=secret&v=1&ih=imap.host.tld&ip=4000&iu=max&ipw=87654&is=ssl&ic=1&sh=mail.host.tld&sp=3000&su=max@host.tld&spw=3242HS&ss=plain&sc=3",
|
||||
)?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "email@host.tld".to_owned());
|
||||
assert_eq!(
|
||||
options,
|
||||
LoginOptions::V1 {
|
||||
mail_pw: "secret".to_owned(),
|
||||
imap_host: Some("imap.host.tld".to_owned()),
|
||||
imap_port: Some(4000),
|
||||
imap_username: Some("max".to_owned()),
|
||||
imap_password: Some("87654".to_owned()),
|
||||
imap_security: Some(Socket::Ssl),
|
||||
imap_certificate_checks: Some(CertificateChecks::Strict),
|
||||
smtp_host: Some("mail.host.tld".to_owned()),
|
||||
smtp_port: Some(3000),
|
||||
smtp_username: Some("max@host.tld".to_owned()),
|
||||
smtp_password: Some("3242HS".to_owned()),
|
||||
smtp_security: Some(Socket::Plain),
|
||||
smtp_certificate_checks: Some(CertificateChecks::AcceptInvalidCertificates),
|
||||
}
|
||||
);
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uri_encoded_password() -> anyhow::Result<()> {
|
||||
let result = decode_login(
|
||||
"dclogin:email@host.tld?p=%7BDaehFl%3B%22as%40%21fhdodn5%24234%22%7B%7Dfg&v=1",
|
||||
)?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "email@host.tld".to_owned());
|
||||
assert_eq!(
|
||||
options,
|
||||
login_options_just_pw!("{DaehFl;\"as@!fhdodn5$234\"{}fg".to_owned())
|
||||
);
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_with_plus_extension() -> anyhow::Result<()> {
|
||||
let result = decode_login("dclogin:usename+extension@host?p=1234&v=1")?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "usename+extension@host".to_owned());
|
||||
assert_eq!(options, login_options_just_pw!("1234".to_owned()));
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -3121,7 +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().await;
|
||||
t.evtracker.consume_events();
|
||||
receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap();
|
||||
|
||||
// Check that no notification is displayed for blocked mailing list message.
|
||||
@@ -4949,7 +4949,7 @@ Reply from different address
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_long_filenames() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
@@ -5001,7 +5001,7 @@ Reply from different address
|
||||
/// Tests that contact request is accepted automatically on outgoing message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_accept_outgoing() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = tcm.alice().await;
|
||||
let alice2 = tcm.alice().await;
|
||||
let bob1 = tcm.bob().await;
|
||||
@@ -5046,7 +5046,7 @@ Reply from different address
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outgoing_private_reply_multidevice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = tcm.alice().await;
|
||||
let alice2 = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
@@ -5134,7 +5134,7 @@ Reply from different address
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_private_reply_to_blocked_account() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use futures::{join, try_join};
|
||||
use futures::try_join;
|
||||
use futures_lite::FutureExt;
|
||||
use tokio::task;
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Context {
|
||||
pub async fn maybe_network(&self) {
|
||||
let lock = self.scheduler.read().await;
|
||||
if let Some(scheduler) = &*lock {
|
||||
scheduler.maybe_network().await;
|
||||
scheduler.maybe_network();
|
||||
}
|
||||
connectivity::idle_interrupted(lock).await;
|
||||
}
|
||||
@@ -51,32 +51,32 @@ impl Context {
|
||||
pub async fn maybe_network_lost(&self) {
|
||||
let lock = self.scheduler.read().await;
|
||||
if let Some(scheduler) = &*lock {
|
||||
scheduler.maybe_network_lost().await;
|
||||
scheduler.maybe_network_lost();
|
||||
}
|
||||
connectivity::maybe_network_lost(self, lock).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_inbox(&self, info: InterruptInfo) {
|
||||
if let Some(scheduler) = &*self.scheduler.read().await {
|
||||
scheduler.interrupt_inbox(info).await;
|
||||
scheduler.interrupt_inbox(info);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_smtp(&self, info: InterruptInfo) {
|
||||
if let Some(scheduler) = &*self.scheduler.read().await {
|
||||
scheduler.interrupt_smtp(info).await;
|
||||
scheduler.interrupt_smtp(info);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_ephemeral_task(&self) {
|
||||
if let Some(scheduler) = &*self.scheduler.read().await {
|
||||
scheduler.interrupt_ephemeral_task().await;
|
||||
scheduler.interrupt_ephemeral_task();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_location(&self) {
|
||||
if let Some(scheduler) = &*self.scheduler.read().await {
|
||||
scheduler.interrupt_location().await;
|
||||
scheduler.interrupt_location();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -501,45 +501,41 @@ impl Scheduler {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn maybe_network(&self) {
|
||||
join!(
|
||||
self.interrupt_inbox(InterruptInfo::new(true)),
|
||||
self.interrupt_mvbox(InterruptInfo::new(true)),
|
||||
self.interrupt_sentbox(InterruptInfo::new(true)),
|
||||
self.interrupt_smtp(InterruptInfo::new(true))
|
||||
);
|
||||
fn maybe_network(&self) {
|
||||
self.interrupt_inbox(InterruptInfo::new(true));
|
||||
self.interrupt_mvbox(InterruptInfo::new(true));
|
||||
self.interrupt_sentbox(InterruptInfo::new(true));
|
||||
self.interrupt_smtp(InterruptInfo::new(true));
|
||||
}
|
||||
|
||||
async fn maybe_network_lost(&self) {
|
||||
join!(
|
||||
self.interrupt_inbox(InterruptInfo::new(false)),
|
||||
self.interrupt_mvbox(InterruptInfo::new(false)),
|
||||
self.interrupt_sentbox(InterruptInfo::new(false)),
|
||||
self.interrupt_smtp(InterruptInfo::new(false))
|
||||
);
|
||||
fn maybe_network_lost(&self) {
|
||||
self.interrupt_inbox(InterruptInfo::new(false));
|
||||
self.interrupt_mvbox(InterruptInfo::new(false));
|
||||
self.interrupt_sentbox(InterruptInfo::new(false));
|
||||
self.interrupt_smtp(InterruptInfo::new(false));
|
||||
}
|
||||
|
||||
async fn interrupt_inbox(&self, info: InterruptInfo) {
|
||||
self.inbox.interrupt(info).await;
|
||||
fn interrupt_inbox(&self, info: InterruptInfo) {
|
||||
self.inbox.interrupt(info);
|
||||
}
|
||||
|
||||
async fn interrupt_mvbox(&self, info: InterruptInfo) {
|
||||
self.mvbox.interrupt(info).await;
|
||||
fn interrupt_mvbox(&self, info: InterruptInfo) {
|
||||
self.mvbox.interrupt(info);
|
||||
}
|
||||
|
||||
async fn interrupt_sentbox(&self, info: InterruptInfo) {
|
||||
self.sentbox.interrupt(info).await;
|
||||
fn interrupt_sentbox(&self, info: InterruptInfo) {
|
||||
self.sentbox.interrupt(info);
|
||||
}
|
||||
|
||||
async fn interrupt_smtp(&self, info: InterruptInfo) {
|
||||
self.smtp.interrupt(info).await;
|
||||
fn interrupt_smtp(&self, info: InterruptInfo) {
|
||||
self.smtp.interrupt(info);
|
||||
}
|
||||
|
||||
async fn interrupt_ephemeral_task(&self) {
|
||||
fn interrupt_ephemeral_task(&self) {
|
||||
self.ephemeral_interrupt_send.try_send(()).ok();
|
||||
}
|
||||
|
||||
async fn interrupt_location(&self) {
|
||||
fn interrupt_location(&self) {
|
||||
self.location_interrupt_send.try_send(()).ok();
|
||||
}
|
||||
|
||||
@@ -603,7 +599,7 @@ impl ConnectionState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn interrupt(&self, info: InterruptInfo) {
|
||||
fn interrupt(&self, info: InterruptInfo) {
|
||||
// Use try_send to avoid blocking on interrupts.
|
||||
self.idle_interrupt_sender.try_send(info).ok();
|
||||
}
|
||||
@@ -637,8 +633,8 @@ impl SmtpConnectionState {
|
||||
}
|
||||
|
||||
/// Interrupt any form of idle.
|
||||
async fn interrupt(&self, info: InterruptInfo) {
|
||||
self.state.interrupt(info).await;
|
||||
fn interrupt(&self, info: InterruptInfo) {
|
||||
self.state.interrupt(info);
|
||||
}
|
||||
|
||||
/// Shutdown this connection completely.
|
||||
@@ -682,8 +678,8 @@ impl ImapConnectionState {
|
||||
}
|
||||
|
||||
/// Interrupt any form of idle.
|
||||
async fn interrupt(&self, info: InterruptInfo) {
|
||||
self.state.interrupt(info).await;
|
||||
fn interrupt(&self, info: InterruptInfo) {
|
||||
self.state.interrupt(info);
|
||||
}
|
||||
|
||||
/// Shutdown this connection completely.
|
||||
|
||||
@@ -696,7 +696,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact() {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
assert_eq!(
|
||||
@@ -910,7 +910,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
@@ -1035,7 +1035,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_concurrent_calls() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
@@ -1066,7 +1066,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_secure_join() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
|
||||
10
src/smtp.rs
10
src/smtp.rs
@@ -64,7 +64,7 @@ impl Smtp {
|
||||
|
||||
/// Return true if smtp was connected but is not known to
|
||||
/// have been successfully used the last 60 seconds
|
||||
pub async fn has_maybe_stale_connection(&self) -> bool {
|
||||
pub fn has_maybe_stale_connection(&self) -> bool {
|
||||
if let Some(last_success) = self.last_success {
|
||||
SystemTime::now()
|
||||
.duration_since(last_success)
|
||||
@@ -77,7 +77,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
/// Check whether we are connected.
|
||||
pub async fn is_connected(&self) -> bool {
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.transport
|
||||
.as_ref()
|
||||
.map(|t| t.is_connected())
|
||||
@@ -86,12 +86,12 @@ impl Smtp {
|
||||
|
||||
/// Connect using configured parameters.
|
||||
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
|
||||
if self.has_maybe_stale_connection().await {
|
||||
if self.has_maybe_stale_connection() {
|
||||
info!(context, "Closing stale connection");
|
||||
self.disconnect().await;
|
||||
}
|
||||
|
||||
if self.is_connected().await {
|
||||
if self.is_connected() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ impl Smtp {
|
||||
addr: &str,
|
||||
provider_strict_tls: bool,
|
||||
) -> Result<()> {
|
||||
if self.is_connected().await {
|
||||
if self.is_connected() {
|
||||
warn!(context, "SMTP already connected.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn build_sync_part(&self, json: String) -> PartBuilder {
|
||||
pub(crate) fn build_sync_part(&self, json: String) -> PartBuilder {
|
||||
PartBuilder::new()
|
||||
.content_type(&"application/json".parse::<mime::Mime>().unwrap())
|
||||
.header((
|
||||
|
||||
@@ -44,7 +44,7 @@ pub struct TestContextManager {
|
||||
}
|
||||
|
||||
impl TestContextManager {
|
||||
pub async fn new() -> Self {
|
||||
pub fn new() -> Self {
|
||||
let (log_tx, _log_sink) = LogSink::create();
|
||||
Self { log_tx, _log_sink }
|
||||
}
|
||||
@@ -858,7 +858,7 @@ impl EventTracker {
|
||||
}
|
||||
|
||||
/// Consumes all pending events.
|
||||
pub async fn consume_events(&self) {
|
||||
pub fn consume_events(&self) {
|
||||
while self.try_recv().is_ok() {}
|
||||
}
|
||||
}
|
||||
@@ -1039,7 +1039,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_with_both() {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::test_utils::TestContextManager;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_primary_self_addr() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
@@ -130,7 +130,7 @@ async fn check_aeap_transition(
|
||||
// the case where Bob already had contact with Alice's new address
|
||||
const ALICE_NEW_ADDR: &str = "fiona@example.net";
|
||||
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
@@ -362,7 +362,7 @@ async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option<Message>
|
||||
/// to make Bob think that there was a transition to Fiona's address.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_aeap_replay_attack() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
|
||||
132
src/tools.rs
132
src/tools.rs
@@ -49,6 +49,65 @@ pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shortens a string to a specified line count and adds "[...]" to the
|
||||
/// end of the shortened string.
|
||||
///
|
||||
/// returns tuple with the String and a boolean whether is was truncated
|
||||
pub(crate) fn truncate_by_lines(
|
||||
buf: String,
|
||||
max_lines: usize,
|
||||
max_line_len: usize,
|
||||
) -> (String, bool) {
|
||||
let mut lines = 0;
|
||||
let mut line_chars = 0;
|
||||
let mut break_point: Option<usize> = None;
|
||||
|
||||
for (index, char) in buf.char_indices() {
|
||||
if char == '\n' {
|
||||
line_chars = 0;
|
||||
lines += 1;
|
||||
} else {
|
||||
line_chars += 1;
|
||||
if line_chars > max_line_len {
|
||||
line_chars = 1;
|
||||
lines += 1;
|
||||
}
|
||||
}
|
||||
if lines == max_lines {
|
||||
break_point = Some(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(end_pos) = break_point {
|
||||
// Text has too many lines and needs to be truncated.
|
||||
let text = {
|
||||
if let Some(buffer) = buf.get(..end_pos) {
|
||||
if let Some(index) = buffer.rfind(|c| c == ' ' || c == '\n') {
|
||||
buf.get(..=index)
|
||||
} else {
|
||||
buf.get(..end_pos)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(truncated_text) = text {
|
||||
(format!("{}{}", truncated_text, DC_ELLIPSIS), true)
|
||||
} else {
|
||||
// In case of indexing/slicing error, we return an error
|
||||
// message as a preview and add HTML version. This should
|
||||
// never happen.
|
||||
let error_text = "[Truncation of the message failed, this is a bug in the Delta Chat core. Please report it.\nYou can still open the full text to view the original message.]";
|
||||
(error_text.to_string(), true)
|
||||
}
|
||||
} else {
|
||||
// text is unchanged
|
||||
(buf, false)
|
||||
}
|
||||
}
|
||||
|
||||
/* ******************************************************************************
|
||||
* date/time tools
|
||||
******************************************************************************/
|
||||
@@ -744,6 +803,79 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
);
|
||||
}
|
||||
|
||||
mod truncate_by_lines {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_just_text() {
|
||||
let s = "this is a little test string".to_string();
|
||||
assert_eq!(
|
||||
truncate_by_lines(s, 4, 6),
|
||||
("this is a little test [...]".to_string(), true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_linebreaks() {
|
||||
let s = "this\n is\n a little test string".to_string();
|
||||
assert_eq!(
|
||||
truncate_by_lines(s, 4, 6),
|
||||
("this\n is\n a little [...]".to_string(), true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_only_linebreaks() {
|
||||
let s = "\n\n\n\n\n\n\n".to_string();
|
||||
assert_eq!(
|
||||
truncate_by_lines(s, 4, 5),
|
||||
("\n\n\n[...]".to_string(), true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limit_hits_end() {
|
||||
let s = "hello\n world !".to_string();
|
||||
assert_eq!(
|
||||
truncate_by_lines(s, 2, 8),
|
||||
("hello\n world !".to_string(), false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge() {
|
||||
assert_eq!(
|
||||
truncate_by_lines("".to_string(), 2, 4),
|
||||
("".to_string(), false)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
truncate_by_lines("\n hello \n world".to_string(), 2, 4),
|
||||
("\n [...]".to_string(), true)
|
||||
);
|
||||
assert_eq!(
|
||||
truncate_by_lines("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ".to_string(), 1, 2),
|
||||
("𐠈0[...]".to_string(), true)
|
||||
);
|
||||
assert_eq!(
|
||||
truncate_by_lines("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ".to_string(), 1, 0),
|
||||
("[...]".to_string(), true)
|
||||
);
|
||||
|
||||
// 9 characters, so no truncation
|
||||
assert_eq!(
|
||||
truncate_by_lines("𑒀ὐ¢🜀\u{1e01b}A a🟠".to_string(), 1, 12),
|
||||
("𑒀ὐ¢🜀\u{1e01b}A a🟠".to_string(), false),
|
||||
);
|
||||
|
||||
// 12 characters, truncation
|
||||
assert_eq!(
|
||||
truncate_by_lines("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd".to_string(), 1, 7),
|
||||
("𑒀ὐ¢🜀\u{1e01b}A [...]".to_string(), true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_id() {
|
||||
let buf = create_id();
|
||||
|
||||
@@ -445,7 +445,7 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn build_status_update_part(&self, json: &str) -> PartBuilder {
|
||||
pub(crate) fn build_status_update_part(&self, json: &str) -> PartBuilder {
|
||||
PartBuilder::new()
|
||||
.content_type(&"application/json".parse::<mime::Mime>().unwrap())
|
||||
.header((
|
||||
@@ -948,6 +948,7 @@ mod tests {
|
||||
async fn test_resend_webxdc_instance_and_info() -> Result<()> {
|
||||
// Alice uses webxdc in a group
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice.set_config_bool(Config::BccSelf, false).await?;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
let alice_instance = send_webxdc_instance(&alice, alice_grp).await?;
|
||||
assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 1);
|
||||
|
||||
Reference in New Issue
Block a user