mirror of
https://github.com/chatmail/core.git
synced 2026-09-22 04:58:47 +03:00
Compare commits
7 Commits
link2xt/pg
...
sk/hide_dr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c68e25a970 | ||
|
|
e04c4446d6 | ||
|
|
1b9f3368fa | ||
|
|
3b9e6d6ffa | ||
|
|
8f3be764d2 | ||
|
|
c181db631f | ||
|
|
c18a476806 |
@@ -16,6 +16,7 @@
|
||||
clippy::cloned_instead_of_copied
|
||||
)]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![recursion_limit = "256"]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
pub mod api;
|
||||
pub use yerpc;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
//!
|
||||
//! For received messages, DelSp parameter is honoured.
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
|
||||
/// Wraps line to 72 characters using format=flowed soft breaks.
|
||||
///
|
||||
|
||||
@@ -705,7 +705,7 @@ class TestOfflineChat:
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
assert qr.startswith("OPENPGP4FPR:")
|
||||
assert qr.startswith("https://i.delta.chat")
|
||||
res = ac2.check_qr(qr)
|
||||
assert res.is_ask_verifycontact()
|
||||
assert not res.is_ask_verifygroup()
|
||||
|
||||
93
src/chat.rs
93
src/chat.rs
@@ -1,11 +1,11 @@
|
||||
//! # Chat module.
|
||||
|
||||
use std::cmp;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use std::{cmp, convert};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context as _, Result};
|
||||
use deltachat_contact_tools::{sanitize_bidi_characters, sanitize_single_line, ContactAddress};
|
||||
@@ -809,6 +809,14 @@ impl ChatId {
|
||||
///
|
||||
/// Passing `None` as message just deletes the draft
|
||||
pub async fn set_draft(self, context: &Context, mut msg: Option<&mut Message>) -> Result<()> {
|
||||
let self_in_chat = self.is_self_in_chat(context).await?;
|
||||
if !self_in_chat {
|
||||
warn!(
|
||||
context,
|
||||
"Not setting draft because self is not member of the chat"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if self.is_special() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -875,6 +883,22 @@ impl ChatId {
|
||||
> 0)
|
||||
}
|
||||
|
||||
async fn get_chat_type(self, context: &Context) -> Result<Chattype> {
|
||||
context
|
||||
.sql
|
||||
.query_get_value("SELECT type FROM chats WHERE id=?;", (self,))
|
||||
.await
|
||||
.map(|res| res.ok_or(anyhow!("Can't get type for char")))
|
||||
.and_then(convert::identity)
|
||||
}
|
||||
|
||||
async fn is_self_in_chat(self, context: &Context) -> Result<bool> {
|
||||
match self.get_chat_type(context).await? {
|
||||
Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
|
||||
Chattype::Group => is_contact_in_chat(context, self, ContactId::SELF).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set provided message as draft message for specified chat.
|
||||
/// Returns true if the draft was added or updated in place.
|
||||
async fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<bool> {
|
||||
@@ -1838,8 +1862,9 @@ impl Chat {
|
||||
/// This is somewhat experimental, even more so than the rest of
|
||||
/// deltachat, and the data returned is still subject to change.
|
||||
pub async fn get_info(&self, context: &Context) -> Result<ChatInfo> {
|
||||
let self_in_chat = self.is_self_in_chat(context).await?;
|
||||
let draft = match self.id.get_draft(context).await? {
|
||||
Some(message) => message.text,
|
||||
Some(message) if self_in_chat => message.text,
|
||||
_ => String::new(),
|
||||
};
|
||||
Ok(ChatInfo {
|
||||
@@ -4894,6 +4919,70 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_draft_only_in_accesible_chat_summary() -> Result<()> {
|
||||
let mut t = TestContextManager::new();
|
||||
let alice = t.alice().await;
|
||||
let bob = t.bob().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "chat").await?;
|
||||
let bob_contact = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
add_contact_to_chat(&alice, chat_id, bob_contact).await?;
|
||||
let bob_chat_id = bob
|
||||
.recv_msg(&alice.send_text(chat_id, "hello bob").await)
|
||||
.await
|
||||
.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// Set draft and assure it is in chat info.
|
||||
let draft = String::from("I'm gonna send this!!");
|
||||
bob_chat_id
|
||||
.set_draft(&bob, Some(&mut Message::new_text(draft.clone())))
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
|
||||
let info = chat.get_info(&bob).await?;
|
||||
assert_eq!(info.draft, draft);
|
||||
|
||||
// Alice removes bob, so draft is not shown in chat info.
|
||||
remove_contact_from_chat(&alice, chat_id, bob_contact).await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
|
||||
assert!(!chat.can_send(&bob).await?);
|
||||
let info = chat.get_info(&bob).await?;
|
||||
assert_eq!(info.draft, String::from(""));
|
||||
|
||||
// Alice re-adds bob, so draft is shown again.
|
||||
add_contact_to_chat(&alice, chat_id, bob_contact).await?;
|
||||
let bob_chat_id = bob
|
||||
.recv_msg(&alice.send_text(chat_id, "hello again, bob").await)
|
||||
.await
|
||||
.chat_id;
|
||||
|
||||
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
|
||||
assert!(chat.can_send(&bob).await?);
|
||||
let info = chat.get_info(&bob).await?;
|
||||
assert_eq!(info.draft, draft);
|
||||
|
||||
// Bob leaves group so draft is not shown.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
|
||||
assert!(!chat.can_send(&bob).await?);
|
||||
let info = chat.get_info(&bob).await?;
|
||||
assert_eq!(info.draft, String::from(""));
|
||||
|
||||
// Bob can not set a draft if not in chat.
|
||||
bob_chat_id
|
||||
.set_draft(
|
||||
&bob,
|
||||
Some(&mut Message::new_text(String::from(
|
||||
"I'm gonna send this fr fr!!",
|
||||
))),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(bob_chat_id.get_draft(&bob).await?.unwrap().text, draft);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
@@ -472,6 +472,14 @@ impl Context {
|
||||
// Allow at least 1 message every second + a burst of 3.
|
||||
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
|
||||
}
|
||||
|
||||
// The next line is mainly for iOS:
|
||||
// iOS starts a separate process for receiving notifications and if the user concurrently
|
||||
// starts the app, the UI process opens the database but waits with calling start_io()
|
||||
// until the notifications process finishes.
|
||||
// Now, some configs may have changed, so, we need to invalidate the cache.
|
||||
self.sql.config_cache.write().await.clear();
|
||||
|
||||
self.scheduler.start(self.clone()).await;
|
||||
}
|
||||
|
||||
@@ -2064,4 +2072,41 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Change the config circumventing the cache
|
||||
// This simulates what the notification plugin on iOS might do
|
||||
// because it runs in a different process
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Alice's Delta Chat doesn't know about it yet:
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Starting IO will fail of course because no server settings are configured,
|
||||
// but it should invalidate the caches:
|
||||
alice.start_io().await;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
10
src/key.rs
10
src/key.rs
@@ -382,9 +382,9 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul
|
||||
pub struct Fingerprint(Vec<u8>);
|
||||
|
||||
impl Fingerprint {
|
||||
/// Creates new 160-bit (20 bytes) or 256-bit (32 bytes) fingerprint.
|
||||
/// Creates new 160-bit (20 bytes) fingerprint.
|
||||
pub fn new(v: Vec<u8>) -> Fingerprint {
|
||||
debug_assert!(v.len() == 20 || v.len() == 32);
|
||||
debug_assert_eq!(v.len(), 20);
|
||||
Fingerprint(v)
|
||||
}
|
||||
|
||||
@@ -438,11 +438,7 @@ impl std::str::FromStr for Fingerprint {
|
||||
.filter(|&c| c.is_ascii_hexdigit())
|
||||
.collect();
|
||||
let v: Vec<u8> = hex::decode(&hex_repr)?;
|
||||
ensure!(
|
||||
v.len() == 20 || v.len() == 32,
|
||||
"wrong fingerprint length: {}",
|
||||
hex_repr
|
||||
);
|
||||
ensure!(v.len() == 20, "wrong fingerprint length: {}", hex_repr);
|
||||
let fp = Fingerprint::new(v);
|
||||
Ok(fp)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
clippy::cloned_instead_of_copied
|
||||
)]
|
||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
|
||||
@@ -11,6 +11,7 @@ use pgp::composed::{
|
||||
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
|
||||
SignedPublicSubKey, SignedSecretKey, StandaloneSignature, SubkeyParamsBuilder,
|
||||
};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToKey};
|
||||
@@ -186,12 +187,14 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
|
||||
let (signing_key_type, encryption_key_type) = match keygen_type {
|
||||
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
|
||||
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::Ed25519, PgpKeyType::X25519),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (
|
||||
PgpKeyType::EdDSALegacy,
|
||||
PgpKeyType::ECDH(ECCCurve::Curve25519),
|
||||
),
|
||||
};
|
||||
|
||||
let user_id = format!("<{addr}>");
|
||||
let key_params = SecretKeyParamsBuilder::default()
|
||||
.version(pgp::types::KeyVersion::V6)
|
||||
.key_type(signing_key_type)
|
||||
.can_certify(true)
|
||||
.can_sign(true)
|
||||
@@ -215,7 +218,6 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
|
||||
])
|
||||
.subkey(
|
||||
SubkeyParamsBuilder::default()
|
||||
.version(pgp::types::KeyVersion::V6)
|
||||
.key_type(encryption_key_type)
|
||||
.can_encrypt(true)
|
||||
.passphrase(None)
|
||||
|
||||
15
src/qr.rs
15
src/qr.rs
@@ -807,11 +807,7 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
|
||||
.get(MAILTO_SCHEME.len()..)
|
||||
.context("Invalid mailto: scheme")?;
|
||||
|
||||
let (addr, query) = if let Some(query_index) = payload.find('?') {
|
||||
(&payload[..query_index], &payload[query_index + 1..])
|
||||
} else {
|
||||
(payload, "")
|
||||
};
|
||||
let (addr, query) = payload.split_once('?').unwrap_or((payload, ""));
|
||||
|
||||
let param: BTreeMap<&str, &str> = query
|
||||
.split('&')
|
||||
@@ -861,12 +857,9 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
|
||||
async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr.get(SMTP_SCHEME.len()..).context("Invalid SMTP scheme")?;
|
||||
|
||||
let addr = if let Some(query_index) = payload.find(':') {
|
||||
&payload[..query_index]
|
||||
} else {
|
||||
bail!("Invalid SMTP found");
|
||||
};
|
||||
|
||||
let (addr, _rest) = payload
|
||||
.split_once(':')
|
||||
.context("Invalid SMTP scheme payload")?;
|
||||
let addr = normalize_address(addr)?;
|
||||
let name = "";
|
||||
Qr::from_address(context, name, &addr, None).await
|
||||
|
||||
@@ -104,7 +104,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
format!(
|
||||
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
|
||||
"https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
self_addr_urlencoded,
|
||||
&group_name_urlencoded,
|
||||
@@ -119,7 +119,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
format!(
|
||||
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
|
||||
"https://i.delta.chat/#{}&a={}&n={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
self_addr_urlencoded,
|
||||
self_name_urlencoded,
|
||||
@@ -347,7 +347,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
send_alice_handshake_msg(
|
||||
context,
|
||||
contact_id,
|
||||
&format!("{}-auth-required", &step[..2]),
|
||||
&format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
|
||||
)
|
||||
.await
|
||||
.context("failed sending auth-required handshake message")?;
|
||||
|
||||
31
src/tools.rs
31
src/tools.rs
@@ -47,20 +47,27 @@ use crate::stock_str;
|
||||
/// end of the shortened string.
|
||||
pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
let count = buf.chars().count();
|
||||
if count > approx_chars + DC_ELLIPSIS.len() {
|
||||
let end_pos = buf
|
||||
.char_indices()
|
||||
.nth(approx_chars)
|
||||
.map(|(n, _)| n)
|
||||
.unwrap_or_default();
|
||||
if count <= approx_chars + DC_ELLIPSIS.len() {
|
||||
return Cow::Borrowed(buf);
|
||||
}
|
||||
let end_pos = buf
|
||||
.char_indices()
|
||||
.nth(approx_chars)
|
||||
.map(|(n, _)| n)
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(index) = buf[..end_pos].rfind([' ', '\n']) {
|
||||
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSIS))
|
||||
} else {
|
||||
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSIS))
|
||||
}
|
||||
if let Some(index) = buf.get(..end_pos).and_then(|s| s.rfind([' ', '\n'])) {
|
||||
Cow::Owned(format!(
|
||||
"{}{}",
|
||||
&buf.get(..=index).unwrap_or_default(),
|
||||
DC_ELLIPSIS
|
||||
))
|
||||
} else {
|
||||
Cow::Borrowed(buf)
|
||||
Cow::Owned(format!(
|
||||
"{}{}",
|
||||
&buf.get(..end_pos).unwrap_or_default(),
|
||||
DC_ELLIPSIS
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user