Compare commits

..

12 Commits

Author SHA1 Message Date
Sebastian Klähn
c284794ac2 double newline 2024-11-10 13:33:40 +01:00
Sebastian Klähn
e494226120 optional \n 2024-11-10 13:25:35 +01:00
Sebastian Klähn
276db00312 jsonrpc(feat): Add API to get json encryption info #6192
Add a new api to jsonrpc that returns json structured data about chat encryption.

close #4642
2024-11-10 12:22:54 +01:00
Sebastian Klähn
5fde02f9fd jsonrpc(feat): Add API to get json encryption info
Add a new api to jsonrpc that returns json structured data about chat encryption.
2024-11-10 11:45:28 +01:00
l
3b2f18f926 feat: use Rustls for connections with strict TLS (#6186) 2024-11-07 19:07:11 +00:00
iequidoo
c9cf2b7f2e fix: Only add "member added/removed" messages if they actually do that (#5992)
There were many cases in which "member added/removed" messages were added to chats even if they
actually do nothing because a member is already added or removed. But primarily this fixes a
scenario when Alice has several devices and shares an invite link somewhere, and both their devices
handle the SecureJoin and issue `ChatGroupMemberAdded` messages so all other members see a
duplicated group member addition.
2024-11-07 14:29:09 -03:00
link2xt
800edc6fce test: remove all calls to print() from deltachat-rpc-client tests
They frequently fail in CI with `OSError: [Errno 9] Bad file descriptor`.
2024-11-07 01:42:01 +00:00
iequidoo
4e5e9f6006 fix: send_msg_to_smtp: Return Ok if smtp row is deleted in parallel
Follow-up to ded8c02c0f. `smtp` rows may be deleted in parallel, in
this case there's just nothing to send.
2024-11-06 21:25:15 -03:00
link2xt
d9d694ead0 fix: remove footers from "Show Full Message..." 2024-11-07 00:24:21 +00:00
link2xt
faad576d10 feat: experimental header protection for Autocrypt
This change adds support for receiving
Autocrypt header in the protected part of encrypted message.

Autocrypt header is now also allowed in mailing lists.
Previously Autocrypt header was rejected when
List-Post header was present,
but the check for the address being equal to the From: address
is sufficient.

New experimental `protect_autocrypt` config is disabled
by default because Delta Chat with reception
support should be released first on all platforms.
2024-11-06 23:16:09 +00:00
Hocuri
b96593ed10 fix: Prevent accidental wrong-password-notifications (#6122)
Over the past years, it happend two times that a user came to me worried
about a false-positive "Cannot login as ***. Please check if the e-mail
address and the password are correct." message.

I'm not sure why this happened, but this PR makes the logic for
showing this notification stricter:
- Before: The notification is shown if connection fails two times in a
row, and the second error contains the word "authentication".
- Now: The notification is shown if the connection fails two times in a
row, and _both_ error messages contain the word "authentication".

The second commit just renames `login_failed_once` to
`authentication_failed_once` in order to reflect this change.
2024-11-05 21:13:21 +00:00
link2xt
d2324a8fc4 chore: fix nightly clippy warnings 2024-11-05 15:05:42 +00:00
25 changed files with 484 additions and 333 deletions

View File

@@ -506,6 +506,11 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
* This is an experimental option not compatible to other MUAs
* and older Delta Chat versions.
* 1 = enable.
* 0 = disable (default).
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.

View File

@@ -45,7 +45,7 @@ pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::chat::{EncryptionInfo, FullChat};
use types::contact::{ContactObject, VcardContact};
use types::events::Event;
use types::http::HttpResponse;
@@ -708,6 +708,19 @@ impl CommandApi {
ChatId::new(chat_id).get_encryption_info(&ctx).await
}
/// Get encryption info for a chat.
async fn get_chat_encryption_info_json(
&self,
account_id: u32,
chat_id: u32,
) -> Result<EncryptionInfo> {
let ctx = self.get_context(account_id).await?;
Ok(ChatId::new(chat_id)
.get_encryption_info_json(&ctx)
.await?
.into())
}
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
///
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.

View File

@@ -9,6 +9,7 @@ use deltachat::context::Context;
use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
use yerpc::JsonSchema;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
@@ -239,3 +240,23 @@ impl JSONRPCChatVisibility {
}
}
}
#[derive(Debug, JsonSchema, TypeDef, Serialize, Deserialize)]
pub struct EncryptionInfo {
/// Addresses with End-to-end encryption preferred.
pub mutual: Vec<String>,
/// Addresses with End-to-end encryption available.
pub no_preference: Vec<String>,
/// Addresses with no encryption.
pub reset: Vec<String>,
}
impl From<chat::EncryptionInfo> for EncryptionInfo {
fn from(encryption_info: chat::EncryptionInfo) -> Self {
EncryptionInfo {
mutual: encryption_info.mutual,
no_preference: encryption_info.no_preference,
reset: encryption_info.reset,
}
}
}

View File

@@ -7,8 +7,8 @@ If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import logging
import os
import sys
import threading
import time
@@ -25,9 +25,7 @@ def path_to_webxdc(request):
def log(msg):
print()
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
print()
logging.info(msg)
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):

View File

@@ -57,8 +57,8 @@ def test_acfactory(acfactory) -> None:
if event.progress == 1000: # Success
break
else:
print(event)
print("Successful configuration")
logging.info(event)
logging.info("Successful configuration")
def test_configure_starttls(acfactory) -> None:

View File

@@ -520,8 +520,13 @@ Authentication-Results: dkim=";
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
}
// Test that Autocrypt works with mailing list.
//
// Previous versions of Delta Chat ignored Autocrypt based on the List-Post header.
// This is not needed: comparing of the From address to Autocrypt header address is enough.
// If the mailing list is not rewriting the From header, Autocrypt should be applied.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_autocrypt_in_mailinglist_ignored() -> Result<()> {
async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -533,28 +538,18 @@ Authentication-Results: dkim=";
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_none());
// Do the same without the mailing list header, this time the peerstate should be accepted
let sent = alice
.send_text(alice_bob_chat.id, "hellooo without mailing list")
.await;
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_some());
// This also means that Bob can now write encrypted to Alice:
// Bob can now write encrypted to Alice:
let mut sent = bob
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
.await;
assert!(sent.load_from_db().await.get_showpadlock());
// But if Bob writes to a mailing list, Alice doesn't show a padlock
// since she can't verify the signature without accepting Bob's key:
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await;
assert!(!rcvd.get_showpadlock());
assert!(rcvd.get_showpadlock());
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
Ok(())

View File

@@ -105,6 +105,17 @@ pub enum ProtectionStatus {
ProtectionBroken = 3, // `2` was never used as a value.
}
/// Encryption info for a single chat.
#[derive(Debug)]
pub struct EncryptionInfo {
/// Addresses with End-to-end encryption preferred.
pub mutual: Vec<String>,
/// Addresses with End-to-end encryption available.
pub no_preference: Vec<String>,
/// Addresses with no encryption.
pub reset: Vec<String>,
}
/// The reason why messages cannot be sent to the chat.
///
/// The reason is mainly for logging and displaying in debug REPL, thus not translated.
@@ -1283,9 +1294,39 @@ impl ChatId {
///
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
let mut ret_mutual = String::new();
let mut ret_nopreference = String::new();
let mut ret_reset = String::new();
let encr_info = self.get_encryption_info_json(context).await?;
let mut ret = String::new();
if !encr_info.reset.is_empty() {
ret += &stock_str::encr_none(context).await;
ret.push_str(":\n");
ret += &encr_info.reset.join("\n");
}
if !encr_info.no_preference.is_empty() {
if !ret.is_empty() {
ret.push_str("\n\n");
}
ret += &stock_str::e2e_available(context).await;
ret.push_str(":\n");
ret += &encr_info.no_preference.join("\n");
}
if !encr_info.mutual.is_empty() {
if !ret.is_empty() {
ret.push_str("\n\n");
}
ret += &stock_str::e2e_preferred(context).await;
ret.push_str(":\n");
ret += &encr_info.mutual.join("\n");
}
Ok(ret)
}
/// Returns encryption preferences of all chat contacts.
pub async fn get_encryption_info_json(self, context: &Context) -> Result<EncryptionInfo> {
let mut mutual = vec![];
let mut no_preference = vec![];
let mut reset = vec![];
for contact_id in get_chat_contacts(context, self)
.await?
@@ -1293,46 +1334,24 @@ impl ChatId {
.filter(|&contact_id| !contact_id.is_special())
{
let contact = Contact::get_by_id(context, *contact_id).await?;
let addr = contact.get_addr();
let peerstate = Peerstate::from_addr(context, addr).await?;
let addr = contact.get_addr().to_string();
let peerstate = Peerstate::from_addr(context, &addr).await?;
match peerstate
.filter(|peerstate| peerstate.peek_key(false).is_some())
.map(|peerstate| peerstate.prefer_encrypt)
{
Some(EncryptPreference::Mutual) => ret_mutual += &format!("{addr}\n"),
Some(EncryptPreference::NoPreference) => ret_nopreference += &format!("{addr}\n"),
Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"),
Some(EncryptPreference::Mutual) => mutual.push(addr),
Some(EncryptPreference::NoPreference) => no_preference.push(addr),
Some(EncryptPreference::Reset) | None => reset.push(addr),
};
}
let mut ret = String::new();
if !ret_reset.is_empty() {
ret += &stock_str::encr_none(context).await;
ret.push(':');
ret.push('\n');
ret += &ret_reset;
}
if !ret_nopreference.is_empty() {
if !ret.is_empty() {
ret.push('\n');
}
ret += &stock_str::e2e_available(context).await;
ret.push(':');
ret.push('\n');
ret += &ret_nopreference;
}
if !ret_mutual.is_empty() {
if !ret.is_empty() {
ret.push('\n');
}
ret += &stock_str::e2e_preferred(context).await;
ret.push(':');
ret.push('\n');
ret += &ret_mutual;
}
Ok(ret.trim().to_string())
Ok(EncryptionInfo {
mutual,
no_preference,
reset,
})
}
/// Bad evil escape hatch.
@@ -4505,6 +4524,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
/// Adds an informational message to chat.
///
/// For example, it can be a message showing that a member was added to a group.
/// Doesn't fail if the chat doesn't exist.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn add_info_msg_with_cmd(
context: &Context,
@@ -5370,7 +5390,7 @@ mod tests {
// Eventually, first removal message arrives.
// This has no effect.
bob.recv_msg(&remove1).await;
bob.recv_msg_trash(&remove1).await;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
Ok(())
}

View File

@@ -396,6 +396,12 @@ pub enum Config {
/// Make all outgoing messages with Autocrypt header "multipart/signed".
SignUnencrypted,
/// Enable header protection for `Autocrypt` header.
///
/// This is an experimental setting not compatible to other MUAs
/// and older Delta Chat versions (core version <= v1.149.0).
ProtectAutocrypt,
/// Let the core save all events to the database.
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]

View File

@@ -990,6 +990,12 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"protect_autocrypt",
self.get_config_int(Config::ProtectAutocrypt)
.await?
.to_string(),
);
res.insert(
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),

View File

@@ -1,125 +1,36 @@
//! End-to-end decryption support.
use std::collections::HashSet;
use std::str::FromStr;
use anyhow::Result;
use deltachat_contact_tools::addr_cmp;
use mailparse::ParsedMail;
use crate::aheader::Aheader;
use crate::authres::handle_authres;
use crate::authres::{self, DkimResults};
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::peerstate::Peerstate;
use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
///
/// If successful and the message is encrypted, returns decrypted body and a set of valid
/// signature fingerprints.
///
/// If the message is wrongly signed, HashSet will be empty.
/// If successful and the message is encrypted, returns decrypted body.
pub fn try_decrypt(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
) -> Result<Option<::pgp::composed::Message>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
};
let data = encrypted_data_part.get_body_raw()?;
let msg = pgp::pk_decrypt(data, private_keyring)?;
let (plain, ret_valid_signatures) =
pgp::pk_decrypt(data, private_keyring, public_keyring_for_validate)?;
Ok(Some((plain, ret_valid_signatures)))
}
pub(crate) async fn prepare_decryption(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
message_time: i64,
) -> Result<DecryptionInfo> {
if mail.headers.get_header(HeaderDef::ListPost).is_some() {
if mail.headers.get_header(HeaderDef::Autocrypt).is_some() {
info!(
context,
"Ignoring autocrypt header since this is a mailing list message. \
NOTE: For privacy reasons, the mailing list software should remove Autocrypt headers."
);
}
return Ok(DecryptionInfo {
from: from.to_string(),
autocrypt_header: None,
peerstate: None,
message_time,
dkim_results: DkimResults { dkim_passed: false },
});
}
let autocrypt_header = if context.is_self_addr(from).await? {
None
} else if let Some(aheader_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) {
match Aheader::from_str(&aheader_value) {
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
} else {
None
};
let dkim_results = handle_authres(context, mail, from).await?;
let allow_aeap = get_encrypted_mime(mail).is_some();
let peerstate = get_autocrypt_peerstate(
context,
from,
autocrypt_header.as_ref(),
message_time,
allow_aeap,
)
.await?;
Ok(DecryptionInfo {
from: from.to_string(),
autocrypt_header,
peerstate,
message_time,
dkim_results,
})
}
#[derive(Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
pub from: String,
pub autocrypt_header: Option<Aheader>,
/// The peerstate that will be used to validate the signatures
pub peerstate: Option<Peerstate>,
/// The timestamp when the message was sent.
/// If this is older than the peerstate's last_seen, this probably
/// means out-of-order message arrival, We don't modify the
/// peerstate in this case.
pub message_time: i64,
pub(crate) dkim_results: authres::DkimResults,
Ok(Some(msg))
}
/// Returns a reference to the encrypted payload of a message.
fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
pub(crate) fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))

View File

@@ -144,12 +144,12 @@ impl HtmlMsgParser {
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().to_ascii_lowercase() == "flowed"
format.as_str().eq_ignore_ascii_case("flowed")
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
delsp.as_str().eq_ignore_ascii_case("yes")
} else {
false
},
@@ -283,7 +283,6 @@ mod tests {
<meta name="color-scheme" content="light dark" />
</head><body>
This message does not have Content-Type nor Subject.<br/>
<br/>
</body></html>
"#
);
@@ -302,7 +301,6 @@ This message does not have Content-Type nor Subject.<br/>
<meta name="color-scheme" content="light dark" />
</head><body>
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
<br/>
</body></html>
"#
);
@@ -325,7 +323,6 @@ This line ends with a space and will be merged with the next one due to format=f
<br/>
This line does not end with a space<br/>
and will be wrapped as usual.<br/>
<br/>
</body></html>
"#
);
@@ -347,7 +344,6 @@ mime-modified should not be set set as there is no html and no special stuff;<br
although not being a delta-message.<br/>
test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x27; :)<br/>
<br/>
<br/>
</body></html>
"#
);

View File

@@ -89,7 +89,7 @@ pub(crate) struct Imap {
oauth2: bool,
login_failed_once: bool,
authentication_failed_once: bool,
pub(crate) connectivity: ConnectivityStore,
@@ -254,7 +254,7 @@ impl Imap {
proxy_config,
strict_tls,
oauth2,
login_failed_once: false,
authentication_failed_once: false,
connectivity: Default::default(),
conn_last_try: UNIX_EPOCH,
conn_backoff_ms: 0,
@@ -402,7 +402,7 @@ impl Imap {
let mut lock = context.server_id.write().await;
lock.clone_from(&session.capabilities.server_id);
self.login_failed_once = false;
self.authentication_failed_once = false;
context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}",
lp.user
@@ -416,35 +416,38 @@ impl Imap {
let imap_user = lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user).await;
let err_str = err.to_string();
warn!(context, "IMAP failed to login: {err:#}.");
first_error.get_or_insert(format_err!("{message} ({err:#})"));
// If it looks like the password is wrong, send a notification:
let _lock = context.wrong_pw_warning_mutex.lock().await;
if !configuring
&& self.login_failed_once
&& err_str.to_lowercase().contains("authentication")
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
let mut msg = Message::new_text(message);
if let Err(e) = chat::add_device_msg_with_importance(
context,
None,
Some(&mut msg),
true,
)
.await
if err.to_string().to_lowercase().contains("authentication") {
if self.authentication_failed_once
&& !configuring
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
warn!(context, "Failed to add device message: {e:#}.");
let mut msg = Message::new_text(message);
if let Err(e) = chat::add_device_msg_with_importance(
context,
None,
Some(&mut msg),
true,
)
.await
{
warn!(context, "Failed to add device message: {e:#}.");
} else {
context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
.log_err(context)
.ok();
}
} else {
context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
.log_err(context)
.ok();
self.authentication_failed_once = true;
}
} else {
self.login_failed_once = true;
self.authentication_failed_once = false;
}
}
}
@@ -1913,7 +1916,7 @@ async fn needs_move_to_mvbox(
&& has_chat_version
&& headers
.get_header_value(HeaderDef::AutoSubmitted)
.filter(|val| val.to_ascii_lowercase() == "auto-generated")
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
.is_some()
{
if let Some(from) = mimeparser::get_from(headers) {

View File

@@ -743,7 +743,9 @@ impl MimeFactory {
hidden_headers.push(header);
} else if header_name == "chat-user-avatar" {
hidden_headers.push(header);
} else if header_name == "autocrypt" {
} else if header_name == "autocrypt"
&& !context.get_config_bool(Config::ProtectAutocrypt).await?
{
unprotected_headers.push(header.clone());
} else if header_name == "from" {
// Unencrypted securejoin messages should _not_ include the display name:

View File

@@ -4,6 +4,7 @@ use std::cmp::min;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::str;
use std::str::FromStr;
use anyhow::{bail, Context as _, Result};
use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters};
@@ -14,6 +15,7 @@ use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, Si
use rand::distributions::{Alphanumeric, DistString};
use crate::aheader::{Aheader, EncryptPreference};
use crate::authres::handle_authres;
use crate::blob::BlobObject;
use crate::chat::{add_info_msg, ChatId};
use crate::config::Config;
@@ -21,8 +23,8 @@ use crate::constants::{self, Chattype};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::decrypt::{
keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature,
DecryptionInfo,
get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
validate_detached_signature,
};
use crate::dehtml::dehtml;
use crate::events::EventType;
@@ -71,7 +73,8 @@ pub(crate) struct MimeMessage {
/// messages to this address to post them to the list.
pub list_post: Option<String>,
pub chat_disposition_notification_to: Option<SingleInfo>,
pub decryption_info: DecryptionInfo,
pub autocrypt_header: Option<Aheader>,
pub peerstate: Option<Peerstate>,
pub decrypting_failed: bool,
/// Set of valid signature fingerprints if a message is an
@@ -301,42 +304,101 @@ impl MimeMessage {
let mut from = from.context("No from in message")?;
let private_keyring = load_self_secret_keyring(context).await?;
let mut decryption_info =
prepare_decryption(context, &mail, &from.addr, timestamp_sent).await?;
let allow_aeap = get_encrypted_mime(&mail).is_some();
let dkim_results = handle_authres(context, &mail, &from.addr).await?;
// Memory location for a possible decrypted message.
let mut mail_raw = Vec::new();
let mut gossiped_keys = Default::default();
let mut from_is_signed = false;
hop_info += "\n\n";
hop_info += &decryption_info.dkim_results.to_string();
hop_info += &dkim_results.to_string();
let incoming = !context.is_self_addr(&from.addr).await?;
let public_keyring = match decryption_info.peerstate.is_none() && !incoming {
true => key::load_self_public_keyring(context).await?,
false => keyring_from_peerstate(decryption_info.peerstate.as_ref()),
};
let (mail, mut signatures, encrypted) = match tokio::task::block_in_place(|| {
try_decrypt(&mail, &private_keyring, &public_keyring)
}) {
Ok(Some((raw, signatures))) => {
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
let mut aheader_value: Option<String> = mail.headers.get_header_value(HeaderDef::Autocrypt);
let mail_raw; // Memory location for a possible decrypted message.
let decrypted_msg; // Decrypted signed OpenPGP message.
let (mail, encrypted) =
match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
Ok(Some(msg)) => {
mail_raw = msg.get_content()?.unwrap_or_default();
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}
decrypted_msg = Some(msg);
if let Some(protected_aheader_value) = decrypted_mail
.headers
.get_header_value(HeaderDef::Autocrypt)
{
aheader_value = Some(protected_aheader_value);
}
(Ok(decrypted_mail), true)
}
Ok(None) => {
mail_raw = Vec::new();
decrypted_msg = None;
(Ok(mail), false)
}
Err(err) => {
mail_raw = Vec::new();
decrypted_msg = None;
warn!(context, "decryption failed: {:#}", err);
(Err(err), false)
}
};
let autocrypt_header = if !incoming {
None
} else if let Some(aheader_value) = aheader_value {
match Aheader::from_str(&aheader_value) {
Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
(Ok(decrypted_mail), signatures, true)
}
Ok(None) => (Ok(mail), HashSet::new(), false),
Err(err) => {
warn!(context, "decryption failed: {:#}", err);
(Err(err), HashSet::new(), false)
}
} else {
None
};
// The peerstate that will be used to validate the signatures.
let mut peerstate = get_autocrypt_peerstate(
context,
&from.addr,
autocrypt_header.as_ref(),
timestamp_sent,
allow_aeap,
)
.await?;
let public_keyring = match peerstate.is_none() && !incoming {
true => key::load_self_public_keyring(context).await?,
false => keyring_from_peerstate(peerstate.as_ref()),
};
let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)?
} else {
HashSet::new()
};
let mail = mail.as_ref().map(|mail| {
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
.unwrap_or((mail, Default::default()));
@@ -422,7 +484,7 @@ impl MimeMessage {
Self::remove_secured_headers(&mut headers);
// If it is not a read receipt, degrade encryption.
if let (Some(peerstate), Ok(mail)) = (&mut decryption_info.peerstate, mail) {
if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) {
if timestamp_sent > peerstate.last_seen_autocrypt
&& mail.ctype.mimetype != "multipart/report"
{
@@ -433,7 +495,7 @@ impl MimeMessage {
if !encrypted {
signatures.clear();
}
if let Some(peerstate) = &mut decryption_info.peerstate {
if let Some(peerstate) = &mut peerstate {
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await?;
@@ -449,7 +511,8 @@ impl MimeMessage {
from_is_signed,
incoming,
chat_disposition_notification_to,
decryption_info,
autocrypt_header,
peerstate,
decrypting_failed: mail.is_err(),
// only non-empty if it was a valid autocrypt message
@@ -1158,7 +1221,7 @@ impl MimeMessage {
let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
{
format.as_str().to_ascii_lowercase() == "flowed"
format.as_str().eq_ignore_ascii_case("flowed")
} else {
false
};
@@ -1168,7 +1231,7 @@ impl MimeMessage {
&& is_format_flowed
{
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
delsp.as_str().eq_ignore_ascii_case("yes")
} else {
false
};
@@ -1231,7 +1294,7 @@ impl MimeMessage {
if decoded_data.is_empty() {
return Ok(());
}
if let Some(peerstate) = &mut self.decryption_info.peerstate {
if let Some(peerstate) = &mut self.peerstate {
if peerstate.prefer_encrypt != EncryptPreference::Mutual
&& mime_type.type_() == mime::APPLICATION
&& mime_type.subtype().as_str() == "pgp-keys"
@@ -3645,6 +3708,28 @@ On 2020-10-25, Bob wrote:
Ok(())
}
/// Tests that sender status (signature) does not appear
/// in HTML view of a long message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_large_message_no_signature() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice
.set_config(Config::Selfstatus, Some("Some signature"))
.await?;
let chat = alice.create_chat(bob).await;
let txt = "Hello!\n".repeat(500);
let sent = alice.send_text(chat.id, &txt).await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.has_html(), true);
let html = msg.id.get_html(bob).await?.unwrap();
assert_eq!(html.contains("Some signature"), false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_x_microsoft_original_message_id() {
let t = TestContext::new_alice().await;
@@ -4012,12 +4097,8 @@ Content-Disposition: reaction\n\
// We do allow the time to be in the future a bit (because of unsynchronized clocks),
// but only 60 seconds:
assert!(mime_message.decryption_info.message_time <= time() + 60);
assert!(mime_message.decryption_info.message_time >= beginning_time + 60);
assert_eq!(
mime_message.decryption_info.message_time,
mime_message.timestamp_sent
);
assert!(mime_message.timestamp_sent <= time() + 60);
assert!(mime_message.timestamp_sent >= beginning_time + 60);
assert!(mime_message.timestamp_rcvd <= time());
Ok(())
@@ -4088,4 +4169,24 @@ Content-Type: text/plain; charset=utf-8
"alice@example.org"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protect_autocrypt() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice
.set_config_bool(Config::ProtectAutocrypt, true)
.await?;
bob.set_config_bool(Config::ProtectAutocrypt, true).await?;
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
assert_eq!(msg.get_showpadlock(), false);
let msg = tcm.send_recv(bob, alice, "Hi!").await;
assert_eq!(msg.get_showpadlock(), true);
Ok(())
}
}

View File

@@ -5,13 +5,13 @@ use std::pin::Pin;
use std::time::Duration;
use anyhow::{format_err, Context as _, Result};
use async_native_tls::TlsStream;
use tokio::net::TcpStream;
use tokio::task::JoinSet;
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::net::session::SessionStream;
use crate::sql::Sql;
use crate::tools::time;
@@ -128,7 +128,7 @@ pub(crate) async fn connect_tls_inner(
host: &str,
strict_tls: bool,
alpn: &[&str],
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
) -> Result<impl SessionStream> {
let tcp_stream = connect_tcp_inner(addr).await?;
let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?;
Ok(tls_stream)

View File

@@ -2,45 +2,39 @@
use std::sync::Arc;
use anyhow::Result;
use async_native_tls::{Certificate, Protocol, TlsConnector, TlsStream};
use once_cell::sync::Lazy;
use tokio::io::{AsyncRead, AsyncWrite};
// this certificate is missing on older android devices (eg. lg with android6 from 2017)
// certificate downloaded from https://letsencrypt.org/certificates/
static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
Certificate::from_der(include_bytes!(
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
))
.unwrap()
});
use crate::net::session::SessionStream;
pub async fn wrap_tls<T: AsyncRead + AsyncWrite + Unpin>(
pub async fn wrap_tls(
strict_tls: bool,
hostname: &str,
alpn: &[&str],
stream: T,
) -> Result<TlsStream<T>> {
let tls_builder = TlsConnector::new()
.min_protocol_version(Some(Protocol::Tlsv12))
.request_alpns(alpn)
.add_root_certificate(LETSENCRYPT_ROOT.clone());
let tls = if strict_tls {
tls_builder
stream: impl SessionStream + 'static,
) -> Result<impl SessionStream> {
if strict_tls {
let tls_stream = wrap_rustls(hostname, alpn, stream).await?;
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
Ok(boxed_stream)
} else {
tls_builder
// We use native_tls because it accepts 1024-bit RSA keys.
// Rustls does not support them even if
// certificate checks are disabled: <https://github.com/rustls/rustls/issues/234>.
let tls = async_native_tls::TlsConnector::new()
.min_protocol_version(Some(async_native_tls::Protocol::Tlsv12))
.request_alpns(alpn)
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
};
let tls_stream = tls.connect(hostname, stream).await?;
Ok(tls_stream)
.danger_accept_invalid_certs(true);
let tls_stream = tls.connect(hostname, stream).await?;
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
Ok(boxed_stream)
}
}
pub async fn wrap_rustls<T: AsyncRead + AsyncWrite + Unpin>(
pub async fn wrap_rustls(
hostname: &str,
alpn: &[&str],
stream: T,
) -> Result<tokio_rustls::client::TlsStream<T>> {
stream: impl SessionStream,
) -> Result<impl SessionStream> {
let mut root_cert_store = rustls::RootCertStore::empty();
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());

View File

@@ -766,8 +766,7 @@ pub(crate) async fn maybe_do_aeap_transition(
context: &Context,
mime_parser: &mut crate::mimeparser::MimeMessage,
) -> Result<()> {
let info = &mime_parser.decryption_info;
let Some(peerstate) = &info.peerstate else {
let Some(peerstate) = &mime_parser.peerstate else {
return Ok(());
};
@@ -815,13 +814,13 @@ pub(crate) async fn maybe_do_aeap_transition(
// DC avoids sending messages with the same timestamp, that's why messages
// with equal timestamps are ignored here unlike in `Peerstate::apply_header()`.
if info.message_time <= peerstate.last_seen {
if mime_parser.timestamp_sent <= peerstate.last_seen {
info!(
context,
"Not doing AEAP from {} to {} because {} < {}.",
&peerstate.addr,
&mime_parser.from.addr,
info.message_time,
mime_parser.timestamp_sent,
peerstate.last_seen
);
return Ok(());
@@ -832,24 +831,23 @@ pub(crate) async fn maybe_do_aeap_transition(
"Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr
);
let info = &mut mime_parser.decryption_info;
let peerstate = info.peerstate.as_mut().context("no peerstate??")?;
let peerstate = mime_parser.peerstate.as_mut().context("no peerstate??")?;
// Add info messages to chats with this (verified) contact
//
peerstate
.handle_setup_change(
context,
info.message_time,
PeerstateChange::Aeap(info.from.clone()),
mime_parser.timestamp_sent,
PeerstateChange::Aeap(mime_parser.from.addr.clone()),
)
.await?;
let old_addr = mem::take(&mut peerstate.addr);
peerstate.addr.clone_from(&info.from);
let header = info.autocrypt_header.as_ref().context(
peerstate.addr.clone_from(&mime_parser.from.addr);
let header = mime_parser.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(context, header, info.message_time);
peerstate.apply_header(context, header, mime_parser.timestamp_sent);
peerstate
.save_to_db_ex(&context.sql, Some(&old_addr))

View File

@@ -297,34 +297,34 @@ pub fn pk_calc_signature(
///
/// Receiver private keys are provided in
/// `private_keys_for_decryption`.
///
/// Returns decrypted message and fingerprints
/// of all keys from the `public_keys_for_validation` keyring that
/// have valid signatures there.
#[allow(clippy::implicit_hasher)]
pub fn pk_decrypt(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
public_keys_for_validation: &[SignedPublicKey],
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
) -> Result<pgp::composed::Message> {
let cursor = Cursor::new(ctext);
let (msg, _) = Message::from_armor_single(cursor)?;
let (msg, _headers) = Message::from_armor_single(cursor)?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
let (msg, _) = msg.decrypt(|| "".into(), &skeys[..])?;
let (msg, _key_ids) = msg.decrypt(|| "".into(), &skeys[..])?;
// get_content() will decompress the message if needed,
// but this avoids decompressing it again to check signatures
let msg = msg.decompress()?;
let content = match msg.get_content()? {
Some(content) => content,
None => bail!("The decrypted message is empty"),
};
Ok(msg)
}
/// Returns fingerprints
/// of all keys from the `public_keys_for_validation` keyring that
/// have valid signatures there.
///
/// If the message is wrongly signed, HashSet will be empty.
pub fn valid_signature_fingerprints(
msg: &pgp::composed::Message,
public_keys_for_validation: &[SignedPublicKey],
) -> Result<HashSet<Fingerprint>> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in public_keys_for_validation {
if signed_msg.verify(&pkey.primary_key).is_ok() {
@@ -333,7 +333,7 @@ pub fn pk_decrypt(
}
}
}
Ok((content, ret_signature_fingerprints))
Ok(ret_signature_fingerprints)
}
/// Validates detached signature.
@@ -407,6 +407,18 @@ mod tests {
use super::*;
use crate::test_utils::{alice_keypair, bob_keypair};
fn pk_decrypt_and_validate(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
public_keys_for_validation: &[SignedPublicKey],
) -> Result<(pgp::composed::Message, HashSet<Fingerprint>)> {
let msg = pk_decrypt(ctext, private_keys_for_decryption)?;
let ret_signature_fingerprints =
valid_signature_fingerprints(&msg, public_keys_for_validation)?;
Ok((msg, ret_signature_fingerprints))
}
#[test]
fn test_split_armored_data_1() {
let (typ, _headers, base64) = split_armored_data(
@@ -534,34 +546,35 @@ mod tests {
// Check decrypting as Alice
let decrypt_keyring = vec![KEYS.alice_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (plain, valid_signatures) = pk_decrypt(
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (plain, valid_signatures) = pk_decrypt(
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_no_sig_check() {
let keyring = vec![KEYS.alice_secret.clone()];
let (plain, valid_signatures) =
pk_decrypt(ctext_signed().await.as_bytes().to_vec(), &keyring, &[]).unwrap();
assert_eq!(plain, CLEARTEXT);
let (msg, valid_signatures) =
pk_decrypt_and_validate(ctext_signed().await.as_bytes().to_vec(), &keyring, &[])
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
@@ -570,26 +583,26 @@ mod tests {
// The validation does not have the public key of the signer.
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.bob_public.clone()];
let (plain, valid_signatures) = pk_decrypt(
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_unsigned() {
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let (plain, valid_signatures) = pk_decrypt(
let (msg, valid_signatures) = pk_decrypt_and_validate(
ctext_unsigned().await.as_bytes().to_vec(),
&decrypt_keyring,
&[],
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
}

View File

@@ -2,7 +2,7 @@
use once_cell::sync::Lazy;
use crate::simplify::split_lines;
use crate::simplify::remove_message_footer;
/// Plaintext message body together with format=flowed attributes.
#[derive(Debug)]
@@ -32,7 +32,8 @@ impl PlainText {
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
});
let lines = split_lines(&self.text);
let lines: Vec<&str> = self.text.lines().collect();
let (lines, _footer) = remove_message_footer(&lines);
let mut ret = r#"<!DOCTYPE html>
<html><head>
@@ -136,7 +137,28 @@ line 1<br/>
line 2<br/>
line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a> and <a href="http://link-end-of-line.com/file?foo=bar%20">http://link-end-of-line.com/file?foo=bar%20</a><br/>
<a href="http://link-at-start-of-line.org">http://link-at-start-of-line.org</a><br/>
<br/>
</body></html>
"#
);
}
#[test]
fn test_plain_remove_signature() {
let html = PlainText {
text: "Foo\nbar\n-- \nSignature here".to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
Foo<br/>
bar<br/>
</body></html>
"#
);

View File

@@ -201,7 +201,7 @@ pub(crate) async fn receive_imf_inner(
};
crate::peerstate::maybe_do_aeap_transition(context, &mut mime_parser).await?;
if let Some(peerstate) = &mime_parser.decryption_info.peerstate {
if let Some(peerstate) = &mime_parser.peerstate {
peerstate
.handle_fingerprint_change(context, mime_parser.timestamp_sent)
.await?;
@@ -356,8 +356,7 @@ pub(crate) async fn receive_imf_inner(
// Peerstate could be updated by handling the Securejoin handshake.
let contact = Contact::get_by_id(context, from_id).await?;
mime_parser.decryption_info.peerstate =
Peerstate::from_addr(context, contact.get_addr()).await?;
mime_parser.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
} else {
let to_id = to_ids.first().copied().unwrap_or_default();
// handshake may mark contacts as verified and must be processed before chats are created
@@ -393,7 +392,7 @@ pub(crate) async fn receive_imf_inner(
if verified_encryption == VerifiedEncryption::Verified
&& mime_parser.get_header(HeaderDef::ChatVerified).is_some()
{
if let Some(peerstate) = &mut mime_parser.decryption_info.peerstate {
if let Some(peerstate) = &mut mime_parser.peerstate {
// NOTE: it might be better to remember ID of the key
// that we used to decrypt the message, but
// it is unlikely that default key ever changes
@@ -1006,7 +1005,7 @@ async fn add_parts(
)
.await?;
}
if let Some(peerstate) = &mime_parser.decryption_info.peerstate {
if let Some(peerstate) = &mime_parser.peerstate {
restore_protection = new_protection != ProtectionStatus::Protected
&& peerstate.prefer_encrypt == EncryptPreference::Mutual
// Check that the contact still has the Autocrypt key same as the
@@ -1424,7 +1423,11 @@ async fn add_parts(
if let Some(msg) = group_changes_msgs.1 {
match &better_msg {
None => better_msg = Some(msg),
Some(_) => group_changes_msgs.0.push(msg),
Some(_) => {
if !msg.is_empty() {
group_changes_msgs.0.push(msg)
}
}
}
}
@@ -1507,6 +1510,9 @@ async fn add_parts(
let mut txt_raw = "".to_string();
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
if better_msg.is_empty() && is_partial_download.is_none() {
chat_id = DC_CHAT_ID_TRASH;
}
(better_msg, Viewtype::Text)
} else {
(&part.msg, part.typ)
@@ -2078,8 +2084,11 @@ async fn create_group(
/// Apply group member list, name, avatar and protection status changes from the MIME message.
///
/// Optionally returns better message to replace the original system message.
/// is_partial_download: whether the message is not fully downloaded.
/// Returns `Vec` of group changes messages and, optionally, a better message to replace the
/// original system message. If the better message is empty, the original system message should be
/// just omitted.
///
/// * `is_partial_download` - whether the message is not fully downloaded.
#[allow(clippy::too_many_arguments)]
async fn apply_group_changes(
context: &Context,
@@ -2181,39 +2190,47 @@ async fn apply_group_changes(
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
better_msg = if removed_id == Some(from_id) {
Some(stock_str::msg_group_left_local(context, from_id).await)
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
if removed_id.is_some() {
if !allow_member_list_changes {
info!(
context,
"Ignoring removal of {removed_addr:?} from {chat_id}."
);
if let Some(id) = removed_id {
if allow_member_list_changes && chat_contacts.contains(&id) {
better_msg = if id == from_id {
Some(stock_str::msg_group_left_local(context, from_id).await)
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
}
} else {
warn!(context, "Removed {removed_addr:?} has no contact id.")
}
better_msg.get_or_insert_with(Default::default);
if !allow_member_list_changes {
info!(
context,
"Ignoring removal of {removed_addr:?} from {chat_id}."
);
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
if allow_member_list_changes {
if !recreate_member_list {
if let Some(contact_id) =
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
{
let is_new_member;
if let Some(contact_id) =
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
{
if !recreate_member_list {
added_id = Some(contact_id);
} else {
warn!(context, "Added {added_addr:?} has no contact id.")
}
is_new_member = !chat_contacts.contains(&contact_id);
} else {
warn!(context, "Added {added_addr:?} has no contact id.");
is_new_member = false;
}
if is_new_member || self_added {
better_msg =
Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
}
} else {
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
}
better_msg.get_or_insert_with(Default::default);
} else if let Some(old_name) = mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.map(|s| s.trim())
@@ -2662,7 +2679,7 @@ async fn update_verified_keys(
return Ok(None);
}
let Some(peerstate) = &mut mimeparser.decryption_info.peerstate else {
let Some(peerstate) = &mut mimeparser.peerstate else {
// No peerstate means no verified keys.
return Ok(None);
};
@@ -2735,7 +2752,7 @@ async fn has_verified_encryption(
// this check is skipped for SELF as there is no proper SELF-peerstate
// and results in group-splits otherwise.
if from_id != ContactId::SELF {
let Some(peerstate) = &mimeparser.decryption_info.peerstate else {
let Some(peerstate) = &mimeparser.peerstate else {
return Ok(NotVerified(
"No peerstate, the contact isn't verified".to_string(),
));

View File

@@ -4185,9 +4185,8 @@ async fn test_recreate_contact_list_on_missing_message() -> Result<()> {
// readd fiona
add_contact_to_chat(&alice, chat_id, alice_fiona).await?;
alice.recv_msg(&remove_msg).await;
// delayed removal of fiona shouldn't remove her
alice.recv_msg_trash(&remove_msg).await;
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
Ok(())
@@ -4947,6 +4946,32 @@ async fn test_unarchive_on_member_removal() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_op_member_added_is_trash() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "foos", &[bob])
.await;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
let msg = alice.pop_sent_msg().await;
bob.recv_msg(&msg).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
bob_chat_id.accept(bob).await?;
let fiona_id = Contact::create(alice, "", "fiona@example.net").await?;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
let msg = alice.pop_sent_msg().await;
let fiona_id = Contact::create(bob, "", "fiona@example.net").await?;
add_contact_to_chat(bob, bob_chat_id, fiona_id).await?;
bob.recv_msg_trash(&msg).await;
let contacts = get_chat_contacts(bob, bob_chat_id).await?;
assert_eq!(contacts.len(), 3);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forged_from() -> Result<()> {
let mut tcm = TestContextManager::new();

View File

@@ -135,7 +135,7 @@ impl SchedulerState {
/// If in the meantime [`SchedulerState::start`] or [`SchedulerState::stop`] is called
/// resume will do the right thing and restore the scheduler to the state requested by
/// the last call.
pub(crate) async fn pause<'a>(&'_ self, context: Context) -> Result<IoPausedGuard> {
pub(crate) async fn pause(&'_ self, context: Context) -> Result<IoPausedGuard> {
{
let mut inner = self.inner.write().await;
match *inner {

View File

@@ -21,7 +21,9 @@ pub fn escape_message_footer_marks(text: &str) -> String {
/// `footer_lines` is set to `Some` if the footer was actually removed from `lines`
/// (which is equal to the input array otherwise).
#[allow(clippy::indexing_slicing)]
fn remove_message_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<&'a [&'a str]>) {
pub(crate) fn remove_message_footer<'a>(
lines: &'a [&str],
) -> (&'a [&'a str], Option<&'a [&'a str]>) {
let mut nearly_standard_footer = None;
for (ix, &line) in lines.iter().enumerate() {
match line {

View File

@@ -357,9 +357,9 @@ pub(crate) async fn send_msg_to_smtp(
.await
.context("failed to update retries count")?;
let (body, recipients, msg_id, retries) = context
let Some((body, recipients, msg_id, retries)) = context
.sql
.query_row(
.query_row_optional(
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
(rowid,),
|row| {
@@ -370,7 +370,10 @@ pub(crate) async fn send_msg_to_smtp(
Ok((mime, recipients, msg_id, retries))
},
)
.await?;
.await?
else {
return Ok(());
};
if retries > 6 {
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.")