Compare commits

...

16 Commits

Author SHA1 Message Date
Simon Laux
6922a66f49 jsonrpc: fix daymarkers 2022-10-01 22:08:11 +02:00
Simon Laux
b2f7a7bb2e jsonrpc js client: ci upload and new name (#3618)
* jsonrpc js client: ci upload and new name

* Update jsonrpc-client-npm-package.yml

* Update jsonrpc-client-npm-package.yml

* Update jsonrpc-client-npm-package.yml

* change details message

* make sure to generate dist directory
2022-10-01 20:43:45 +02:00
bjoern
1965866813 prepare 1.95 (#3617)
* update changelog for 1.95.0

* bump version to 1.95.0
2022-09-30 12:43:44 +02:00
Simon Laux
110f56777d implement dclogin scheme (#3541)
* start implementing dclogin scheme

* fix formatting

* add test for usename+extension@host cases

* add test with all advanced options

* add changelog

* jsonrpc api and regenerate node constants

* Update src/qr/dclogin_scheme.rs

Co-authored-by: Hocuri <hocuri@gmx.de>

* apply Hocuris comments from code review

* fix clippy

* Use .eq_ignore_ascii_case()

* rename internal function apply_from_login_qr
to configure_from_login_qr

* fix error message

* cargo fmt

* remove test todo comment

Co-authored-by: Hocuri <hocuri@gmx.de>
2022-09-29 18:32:17 +02:00
Simon Laux
37c6001b6c jsonrpc: add chat_get_neighboring_media function (#3610)
* jsonrpc: add `chat_get_neighboring_media` function

* add number to changelog
2022-09-27 12:42:32 +02:00
Simon Laux
9a9c91e591 add method for desktop to get notification relevant information for a message (#3614)
* add method for desktop to get notification relevant information for a message

* add pr number to changelog

* rename MessageNotificationData to MessageNotificationInfo
2022-09-25 21:29:46 +02:00
link2xt
17276179e7 python: fix test_getinfo after bcc_self default change 2022-09-25 10:29:48 +00:00
link2xt
335d780f3e Fix Python test for bcc_self 2022-09-25 10:09:26 +00:00
link2xt
450d113993 Fix Rust 1.64 clippy warnings and tests 2022-09-25 03:17:00 +00:00
bjoern
cd6d181bbc enable BccSelf by default (#3612)
* enable `BccSelf` by default

enabling `BccSelf` improves user experience as
it is easier to set up another device
and ppl will also see "all" messages in other user agents directly.

for uncounted user problems, after diving into the issue,
the resulting device was "turn on BccSelf".
disabled `BccSelf` was probably the the number one single reason
of user problems.

main drawback of the change are potentially double notifications
when using a shared account and having another mail app on the same device.
however, we meanwhile do not recommend shared accounts at all,
the issue is also fixable by the other mail apps (as done by K-9)
and could be even regarded as a feature (you can decide which app to use for ansering).
but at the end the drawback is probably much smaller than the issues reported above.

* adapt tests to `BccSelf` enabled

* update CHANGELOG
2022-09-24 21:54:06 +02:00
Simon Laux
c92c6a24a0 jsonrpc: add mailingListAddress property to FullChat (#3607)
* jsonrpc: add `mailingListAddress` property to `FullChat`

* add pr number to changelog
2022-09-19 22:32:34 +02:00
link2xt
ffe7216194 Emit per-message events for expired messages
Instead of emitting single MsgsChanged event
with zero chat and msg IDs, emit one event per message.

Also emit WebxdcInstanceDeleted event if expired message
contains a webxdc.
2022-09-17 21:23:47 +00:00
link2xt
474eb7cbc8 Fix changelog PR reference 2022-09-17 17:51:33 +00:00
link2xt
252b528f40 Update "How messages are deleted" documentation
"IMAP folder and UID information" is no longer stored
in the `msgs` table since creation of the `imap` table.
As a result `msgs` table entries do not contain UID information in the
first place.

This commit updates documentation to reflect this change and also
points to `prune_tombstones()` procedure which actually deletes `msgs`
rows.
2022-09-17 17:18:53 +00:00
Simon Laux
a4357712bf fix changelog entry
was inserted at wrong place
2022-09-17 16:16:35 +02:00
Simon Laux
62afd3d4c3 truncate incoming messages by lines (#3480)
* truncate incoming messages by lines,
because many linebreaks seem to cause the chat open delay on deltachat-ios

* run cargo fmt

* remove DC_DESIRED_TEXT_LINES_THRESHOLD
and use Strings instead of Cow<str>

* remove usage of clippy::indexing_slicing in truncate_by_lines (#3596)

* adjust comments

* Fix truncate_by_lines tests

* Reword indexing/slicing error

* Remove unnecessary conditional

* Fix a typo in the comment

Co-authored-by: link2xt <link2xt@testrun.org>
2022-09-17 16:15:33 +02:00
48 changed files with 1220 additions and 268 deletions

View 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/"

View File

@@ -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
View File

@@ -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",

View File

@@ -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"

View File

@@ -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"

View File

@@ -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.

View File

@@ -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))
}

View File

@@ -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,

View File

@@ -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"

View File

@@ -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
// ---------------------------------------------

View File

@@ -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,
})
}
}

View File

@@ -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,
})
}
}

View File

@@ -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 },
}
}
}

View File

@@ -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();

View File

@@ -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

View File

@@ -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];

View File

@@ -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"
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
}

View File

@@ -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"
}

View File

@@ -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()

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)]

View File

@@ -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?,

View File

@@ -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 {

View File

@@ -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

View File

@@ -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;

View File

@@ -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());
}

View File

@@ -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(), "");

View File

@@ -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?;

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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));
}
}

View File

@@ -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,

View File

@@ -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 &lt;<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
View File

@@ -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
View 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(())
}
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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;

View File

@@ -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(());
}

View File

@@ -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((

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);