Compare commits

..
Author SHA1 Message Date
Hocuri ce6e766c72 --wip-- [skip ci] 2024-11-14 17:15:39 +01:00
28 changed files with 355 additions and 474 deletions
Binary file not shown.
-5
View File
@@ -506,11 +506,6 @@ 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.
+2 -6
View File
@@ -2520,12 +2520,8 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
Some(ChatId::new(chat_id))
};
block_on(async {
location::is_sending_locations_to_chat(ctx, chat_id)
.await
.map(|res| res as u8)
})
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
block_on(location::is_sending_locations_to_chat(ctx, chat_id))
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
}
#[no_mangle]
+2 -4
View File
@@ -629,9 +629,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
);
}
}
if location::is_sending_locations_to_chat(&context, None).await?
!= location::LocationSendingStatus::Disabled
{
if location::is_sending_locations_to_chat(&context, None).await? {
println!("Location streaming enabled.");
}
println!("{cnt} chats");
@@ -843,7 +841,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
println!(
"Location streaming: {:?}",
"Location streaming: {}",
location::is_sending_locations_to_chat(
&context,
Some(sel_chat.as_ref().unwrap().get_id())
@@ -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,7 +25,9 @@ def path_to_webxdc(request):
def log(msg):
logging.info(msg)
print()
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
print()
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
+2 -2
View File
@@ -57,8 +57,8 @@ def test_acfactory(acfactory) -> None:
if event.progress == 1000: # Success
break
else:
logging.info(event)
logging.info("Successful configuration")
print(event)
print("Successful configuration")
def test_configure_starttls(acfactory) -> None:
Generated
+31 -30
View File
@@ -7,11 +7,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1731356359,
"narHash": "sha256-vYqJnu6jotmWpPT4DgzHVdvNIZcKZCIUqS8QaptsZA0=",
"lastModified": 1729628358,
"narHash": "sha256-2HDSc6BL+bE3S1l3Gn0Z8wWvvfBEUEjvXkNIQ11Aifk=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "c028ead7e88edb2e94cd7c90ee37593f63ae494a",
"rev": "52b9e0c0f9cff887d2bb4932f8be4e062ba0802d",
"type": "github"
},
"original": {
@@ -47,11 +47,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1731393059,
"narHash": "sha256-rmzi0GHEwpzg1LGfGPO4SRD7D6QGV3UYGQxkJvn+J5U=",
"lastModified": 1714112748,
"narHash": "sha256-jq6Cpf/pQH85p+uTwPPrGG8Ky/zUOTwMJ7mcqc5M4So=",
"owner": "nix-community",
"repo": "fenix",
"rev": "fda8d5b59bb0dc0021ad3ba1d722f9ef6d36e4d9",
"rev": "3ae4b908a795b6a3824d401a0702e11a7157d7e1",
"type": "github"
},
"original": {
@@ -83,11 +83,11 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
@@ -101,11 +101,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1721727458,
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
"lastModified": 1713520724,
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"owner": "nix-community",
"repo": "naersk",
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"type": "github"
},
"original": {
@@ -116,11 +116,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1730207686,
"narHash": "sha256-SCHiL+1f7q9TAnxpasriP6fMarWE5H43t25F5/9e28I=",
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "776e68c1d014c3adde193a18db9d738458cd2ba4",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"type": "github"
},
"original": {
@@ -131,11 +131,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1729413321,
"narHash": "sha256-I4tuhRpZFa6Fu6dcH9Dlo5LlH17peT79vx1y1SpeKt0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "1997e4aa514312c1af7e2bda7fad1644e778ff26",
"type": "github"
},
"original": {
@@ -147,11 +147,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1713895582,
"narHash": "sha256-cfh1hi+6muQMbi9acOlju3V1gl8BEaZBXBR9jQfQi4U=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "572af610f6151fd41c212f897c71f7056e3fb518",
"type": "github"
},
"original": {
@@ -163,9 +163,10 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 0,
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
"path": "/nix/store/zq2axpgzd5kykk1v446rkffj3bxa2m2h-source",
"lastModified": 1711668574,
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
"type": "path"
},
"original": {
@@ -175,11 +176,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1729413321,
"narHash": "sha256-I4tuhRpZFa6Fu6dcH9Dlo5LlH17peT79vx1y1SpeKt0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "1997e4aa514312c1af7e2bda7fad1644e778ff26",
"type": "github"
},
"original": {
@@ -202,11 +203,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1731342671,
"narHash": "sha256-36eYDHoPzjavnpmEpc2MXdzMk557S0YooGms07mDuKk=",
"lastModified": 1714031783,
"narHash": "sha256-xS/niQsq1CQPOe4M4jvVPO2cnXS/EIeRG5gIopUbk+Q=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "fc98e0657abf3ce07eed513e38274c89bbb2f8ad",
"rev": "56bee2ddafa6177b19c631eedc88d43366553223",
"type": "github"
},
"original": {
+13 -8
View File
@@ -520,13 +520,8 @@ 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_not_ignored() -> Result<()> {
async fn test_autocrypt_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -538,18 +533,28 @@ 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());
// Bob can now write encrypted to Alice:
// This also means that 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(())
+1 -2
View File
@@ -4505,7 +4505,6 @@ 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,
@@ -5371,7 +5370,7 @@ mod tests {
// Eventually, first removal message arrives.
// This has no effect.
bob.recv_msg_trash(&remove1).await;
bob.recv_msg(&remove1).await;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
Ok(())
}
-6
View File
@@ -396,12 +396,6 @@ 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"))]
-6
View File
@@ -990,12 +990,6 @@ 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(),
+94 -5
View File
@@ -1,36 +1,125 @@
//! 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.
/// 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.
pub fn try_decrypt(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
) -> Result<Option<::pgp::composed::Message>> {
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
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)?;
Ok(Some(msg))
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,
}
/// Returns a reference to the encrypted payload of a message.
pub(crate) fn get_encrypted_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
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))
+4
View File
@@ -283,6 +283,7 @@ mod tests {
<meta name="color-scheme" content="light dark" />
</head><body>
This message does not have Content-Type nor Subject.<br/>
<br/>
</body></html>
"#
);
@@ -301,6 +302,7 @@ 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>
"#
);
@@ -323,6 +325,7 @@ 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>
"#
);
@@ -344,6 +347,7 @@ 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>
"#
);
+24 -27
View File
@@ -89,7 +89,7 @@ pub(crate) struct Imap {
oauth2: bool,
authentication_failed_once: bool,
login_failed_once: bool,
pub(crate) connectivity: ConnectivityStore,
@@ -254,7 +254,7 @@ impl Imap {
proxy_config,
strict_tls,
oauth2,
authentication_failed_once: false,
login_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.authentication_failed_once = false;
self.login_failed_once = false;
context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}",
lp.user
@@ -416,38 +416,35 @@ 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 err.to_string().to_lowercase().contains("authentication") {
if self.authentication_failed_once
&& !configuring
&& context.get_config_bool(Config::NotifyAboutWrongPw).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
{
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();
}
warn!(context, "Failed to add device message: {e:#}.");
} else {
self.authentication_failed_once = true;
context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
.log_err(context)
.ok();
}
} else {
self.authentication_failed_once = false;
self.login_failed_once = true;
}
}
}
+4 -34
View File
@@ -87,17 +87,6 @@ pub struct Kml {
pub curr: Location,
}
/// Location streaming status for one chat.
#[derive(Debug, PartialEq, Eq)]
pub enum LocationSendingStatus {
/// Location streaming is disabled.
Disabled = 0,
/// Location streaming is enabled.
Enabled = 1,
/// Location streaming is enabled but (currently) not possible.
Failure = 2,
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
enum KmlTag {
#[default]
@@ -285,8 +274,7 @@ pub async fn send_locations_to_chat(
ensure!(seconds >= 0);
ensure!(!chat_id.is_special());
let now = time();
let is_sending_locations_before = is_sending_locations_to_chat(context, Some(chat_id)).await?
== LocationSendingStatus::Enabled;
let is_sending_locations_before = is_sending_locations_to_chat(context, Some(chat_id)).await?;
context
.sql
.execute(
@@ -326,43 +314,25 @@ pub async fn send_locations_to_chat(
pub async fn is_sending_locations_to_chat(
context: &Context,
chat_id: Option<ChatId>,
) -> Result<LocationSendingStatus> {
) -> Result<bool> {
let exists = match chat_id {
Some(chat_id) => {
let enabled = context
context
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
(chat_id, time()),
)
.await?;
let functional: i32 = context
.sql
.query_get_value("SELECT locations_send_begin FROM chats WHERE id=?;", ())
.await?
.ok_or(anyhow::anyhow!("not able to select"))?;
if enabled && functional > 0 {
LocationSendingStatus::Enabled
} else if enabled && functional == 0 {
LocationSendingStatus::Failure
} else {
LocationSendingStatus::Disabled
}
}
None => {
if context
context
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
(time(),),
)
.await?
{
LocationSendingStatus::Enabled
} else {
LocationSendingStatus::Disabled
}
}
};
Ok(exists)
+3 -7
View File
@@ -20,7 +20,7 @@ use crate::e2ee::EncryptHelper;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::headerdef::HeaderDef;
use crate::html::new_html_mimepart;
use crate::location::{self, LocationSendingStatus};
use crate::location;
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
@@ -743,9 +743,7 @@ impl MimeFactory {
hidden_headers.push(header);
} else if header_name == "chat-user-avatar" {
hidden_headers.push(header);
} else if header_name == "autocrypt"
&& !context.get_config_bool(Config::ProtectAutocrypt).await?
{
} else if header_name == "autocrypt" {
unprotected_headers.push(header.clone());
} else if header_name == "from" {
// Unencrypted securejoin messages should _not_ include the display name:
@@ -1372,9 +1370,7 @@ impl MimeFactory {
parts.push(msg_kml_part);
}
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await?
!= LocationSendingStatus::Disabled
{
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await? {
if let Some(part) = self.get_location_kml_part(context).await? {
parts.push(part);
}
+40 -141
View File
@@ -4,7 +4,6 @@ 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};
@@ -15,7 +14,6 @@ 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;
@@ -23,8 +21,8 @@ use crate::constants::{self, Chattype};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::decrypt::{
get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
validate_detached_signature,
keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature,
DecryptionInfo,
};
use crate::dehtml::dehtml;
use crate::events::EventType;
@@ -73,8 +71,7 @@ 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 autocrypt_header: Option<Aheader>,
pub peerstate: Option<Peerstate>,
pub decryption_info: DecryptionInfo,
pub decrypting_failed: bool,
/// Set of valid signature fingerprints if a message is an
@@ -304,101 +301,42 @@ impl MimeMessage {
let mut from = from.context("No from in message")?;
let private_keyring = load_self_secret_keyring(context).await?;
let allow_aeap = get_encrypted_mime(&mail).is_some();
let dkim_results = handle_authres(context, &mail, &from.addr).await?;
let mut decryption_info =
prepare_decryption(context, &mail, &from.addr, timestamp_sent).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 += &dkim_results.to_string();
hop_info += &decryption_info.dkim_results.to_string();
let incoming = !context.is_self_addr(&from.addr).await?;
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
}
}
} 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 {
let public_keyring = match decryption_info.peerstate.is_none() && !incoming {
true => key::load_self_public_keyring(context).await?,
false => keyring_from_peerstate(peerstate.as_ref()),
false => keyring_from_peerstate(decryption_info.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, 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),
);
}
(Ok(decrypted_mail), signatures, true)
}
Ok(None) => (Ok(mail), HashSet::new(), false),
Err(err) => {
warn!(context, "decryption failed: {:#}", err);
(Err(err), HashSet::new(), false)
}
};
let mail = mail.as_ref().map(|mail| {
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
.unwrap_or((mail, Default::default()));
@@ -484,7 +422,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 peerstate, mail) {
if let (Some(peerstate), Ok(mail)) = (&mut decryption_info.peerstate, mail) {
if timestamp_sent > peerstate.last_seen_autocrypt
&& mail.ctype.mimetype != "multipart/report"
{
@@ -495,7 +433,7 @@ impl MimeMessage {
if !encrypted {
signatures.clear();
}
if let Some(peerstate) = &mut peerstate {
if let Some(peerstate) = &mut decryption_info.peerstate {
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await?;
@@ -511,8 +449,7 @@ impl MimeMessage {
from_is_signed,
incoming,
chat_disposition_notification_to,
autocrypt_header,
peerstate,
decryption_info,
decrypting_failed: mail.is_err(),
// only non-empty if it was a valid autocrypt message
@@ -1294,7 +1231,7 @@ impl MimeMessage {
if decoded_data.is_empty() {
return Ok(());
}
if let Some(peerstate) = &mut self.peerstate {
if let Some(peerstate) = &mut self.decryption_info.peerstate {
if peerstate.prefer_encrypt != EncryptPreference::Mutual
&& mime_type.type_() == mime::APPLICATION
&& mime_type.subtype().as_str() == "pgp-keys"
@@ -3708,28 +3645,6 @@ 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;
@@ -4097,8 +4012,12 @@ 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.timestamp_sent <= time() + 60);
assert!(mime_message.timestamp_sent >= beginning_time + 60);
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_rcvd <= time());
Ok(())
@@ -4169,24 +4088,4 @@ 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(())
}
}
+2 -2
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<impl SessionStream> {
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp_inner(addr).await?;
let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?;
Ok(tls_stream)
+28 -22
View File
@@ -2,39 +2,45 @@
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};
use crate::net::session::SessionStream;
// 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()
});
pub async fn wrap_tls(
pub async fn wrap_tls<T: AsyncRead + AsyncWrite + Unpin>(
strict_tls: bool,
hostname: &str,
alpn: &[&str],
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)
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
} else {
// 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)
tls_builder
.danger_accept_invalid_hostnames(true)
.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)
}
.danger_accept_invalid_certs(true)
};
let tls_stream = tls.connect(hostname, stream).await?;
Ok(tls_stream)
}
pub async fn wrap_rustls(
pub async fn wrap_rustls<T: AsyncRead + AsyncWrite + Unpin>(
hostname: &str,
alpn: &[&str],
stream: impl SessionStream,
) -> Result<impl SessionStream> {
stream: T,
) -> Result<tokio_rustls::client::TlsStream<T>> {
let mut root_cert_store = rustls::RootCertStore::empty();
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
+11 -9
View File
@@ -766,7 +766,8 @@ pub(crate) async fn maybe_do_aeap_transition(
context: &Context,
mime_parser: &mut crate::mimeparser::MimeMessage,
) -> Result<()> {
let Some(peerstate) = &mime_parser.peerstate else {
let info = &mime_parser.decryption_info;
let Some(peerstate) = &info.peerstate else {
return Ok(());
};
@@ -814,13 +815,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 mime_parser.timestamp_sent <= peerstate.last_seen {
if info.message_time <= peerstate.last_seen {
info!(
context,
"Not doing AEAP from {} to {} because {} < {}.",
&peerstate.addr,
&mime_parser.from.addr,
mime_parser.timestamp_sent,
info.message_time,
peerstate.last_seen
);
return Ok(());
@@ -831,23 +832,24 @@ pub(crate) async fn maybe_do_aeap_transition(
"Doing AEAP transition from {} to {}.", &peerstate.addr, &mime_parser.from.addr
);
let peerstate = mime_parser.peerstate.as_mut().context("no peerstate??")?;
let info = &mut mime_parser.decryption_info;
let peerstate = info.peerstate.as_mut().context("no peerstate??")?;
// Add info messages to chats with this (verified) contact
//
peerstate
.handle_setup_change(
context,
mime_parser.timestamp_sent,
PeerstateChange::Aeap(mime_parser.from.addr.clone()),
info.message_time,
PeerstateChange::Aeap(info.from.clone()),
)
.await?;
let old_addr = mem::take(&mut peerstate.addr);
peerstate.addr.clone_from(&mime_parser.from.addr);
let header = mime_parser.autocrypt_header.as_ref().context(
peerstate.addr.clone_from(&info.from);
let header = info.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(context, header, mime_parser.timestamp_sent);
peerstate.apply_header(context, header, info.message_time);
peerstate
.save_to_db_ex(&context.sql, Some(&old_addr))
+27 -40
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],
) -> Result<pgp::composed::Message> {
public_keys_for_validation: &[SignedPublicKey],
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
let cursor = Cursor::new(ctext);
let (msg, _headers) = Message::from_armor_single(cursor)?;
let (msg, _) = Message::from_armor_single(cursor)?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
let (msg, _key_ids) = msg.decrypt(|| "".into(), &skeys[..])?;
let (msg, _) = 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()?;
Ok(msg)
}
let content = match msg.get_content()? {
Some(content) => content,
None => bail!("The decrypted message is empty"),
};
/// 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 valid_signature_fingerprints(
}
}
}
Ok(ret_signature_fingerprints)
Ok((content, ret_signature_fingerprints))
}
/// Validates detached signature.
@@ -407,18 +407,6 @@ 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(
@@ -546,35 +534,34 @@ mod tests {
// Check decrypting as Alice
let decrypt_keyring = vec![KEYS.alice_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (msg, valid_signatures) = pk_decrypt_and_validate(
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(plain, 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 (msg, valid_signatures) = pk_decrypt_and_validate(
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(plain, 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 (msg, valid_signatures) =
pk_decrypt_and_validate(ctext_signed().await.as_bytes().to_vec(), &keyring, &[])
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
let (plain, valid_signatures) =
pk_decrypt(ctext_signed().await.as_bytes().to_vec(), &keyring, &[]).unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
@@ -583,26 +570,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 (msg, valid_signatures) = pk_decrypt_and_validate(
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(plain, 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 (msg, valid_signatures) = pk_decrypt_and_validate(
let (plain, valid_signatures) = pk_decrypt(
ctext_unsigned().await.as_bytes().to_vec(),
&decrypt_keyring,
&[],
)
.unwrap();
assert_eq!(msg.get_content().unwrap().unwrap(), CLEARTEXT);
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
}
+3 -25
View File
@@ -2,7 +2,7 @@
use once_cell::sync::Lazy;
use crate::simplify::remove_message_footer;
use crate::simplify::split_lines;
/// Plaintext message body together with format=flowed attributes.
#[derive(Debug)]
@@ -32,8 +32,7 @@ impl PlainText {
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
});
let lines: Vec<&str> = self.text.lines().collect();
let (lines, _footer) = remove_message_footer(&lines);
let lines = split_lines(&self.text);
let mut ret = r#"<!DOCTYPE html>
<html><head>
@@ -137,28 +136,7 @@ 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/>
</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/>
<br/>
</body></html>
"#
);
+11 -5
View File
@@ -175,6 +175,7 @@ async fn set_msg_id_reaction(
contact_id: ContactId,
timestamp: i64,
reaction: &Reaction,
rfc724_mid: Option<&str>,
) -> Result<()> {
if reaction.is_empty() {
// Simply remove the record instead of setting it to empty string.
@@ -191,11 +192,11 @@ async fn set_msg_id_reaction(
context
.sql
.execute(
"INSERT INTO reactions (msg_id, contact_id, reaction)
VALUES (?1, ?2, ?3)
"INSERT INTO reactions (msg_id, contact_id, reaction, rfc724_mid)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(msg_id, contact_id)
DO UPDATE SET reaction=excluded.reaction",
(msg_id, contact_id, reaction.as_str()),
DO UPDATE SET reaction=excluded.reaction, rfc724_mid=excluded.rfc724_mid",
(msg_id, contact_id, reaction.as_str(), rfc724_mid),
)
.await?;
let mut chat = Chat::load_from_db(context, chat_id).await?;
@@ -245,6 +246,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
ContactId::SELF,
reaction_msg.timestamp_sort,
&reaction,
None,
)
.await?;
Ok(reaction_msg_id)
@@ -276,9 +278,13 @@ pub(crate) async fn set_msg_reaction(
timestamp: i64,
reaction: Reaction,
is_incoming_fresh: bool,
rfc724_mid: Option<&str>,
) -> Result<()> {
if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
set_msg_id_reaction(
context, msg_id, chat_id, contact_id, timestamp, &reaction, rfc724_mid,
)
.await?;
if is_incoming_fresh
&& !reaction.is_empty()
+34 -50
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.peerstate {
if let Some(peerstate) = &mime_parser.decryption_info.peerstate {
peerstate
.handle_fingerprint_change(context, mime_parser.timestamp_sent)
.await?;
@@ -356,7 +356,8 @@ 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.peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
mime_parser.decryption_info.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
@@ -392,7 +393,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.peerstate {
if let Some(peerstate) = &mut mime_parser.decryption_info.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
@@ -1005,7 +1006,7 @@ async fn add_parts(
)
.await?;
}
if let Some(peerstate) = &mime_parser.peerstate {
if let Some(peerstate) = &mime_parser.decryption_info.peerstate {
restore_protection = new_protection != ProtectionStatus::Protected
&& peerstate.prefer_encrypt == EncryptPreference::Mutual
// Check that the contact still has the Autocrypt key same as the
@@ -1423,11 +1424,7 @@ async fn add_parts(
if let Some(msg) = group_changes_msgs.1 {
match &better_msg {
None => better_msg = Some(msg),
Some(_) => {
if !msg.is_empty() {
group_changes_msgs.0.push(msg)
}
}
Some(_) => group_changes_msgs.0.push(msg),
}
}
@@ -1485,6 +1482,7 @@ async fn add_parts(
sort_timestamp,
Reaction::from(reaction_str.as_str()),
is_incoming_fresh,
Some(rfc724_mid),
)
.await?;
}
@@ -1510,9 +1508,6 @@ 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)
@@ -2084,11 +2079,8 @@ async fn create_group(
/// Apply group member list, name, avatar and protection status changes from the MIME message.
///
/// 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.
/// Optionally returns better message to replace the original system message.
/// is_partial_download: whether the message is not fully downloaded.
#[allow(clippy::too_many_arguments)]
async fn apply_group_changes(
context: &Context,
@@ -2190,47 +2182,39 @@ 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?;
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)
};
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}."
);
}
} 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) {
if allow_member_list_changes {
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);
}
is_new_member = !chat_contacts.contains(&contact_id);
} else {
warn!(context, "Added {added_addr:?} has no contact id.");
is_new_member = false;
}
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
if is_new_member || self_added {
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?
{
added_id = Some(contact_id);
} else {
warn!(context, "Added {added_addr:?} has no contact id.")
}
}
} 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())
@@ -2679,7 +2663,7 @@ async fn update_verified_keys(
return Ok(None);
}
let Some(peerstate) = &mut mimeparser.peerstate else {
let Some(peerstate) = &mut mimeparser.decryption_info.peerstate else {
// No peerstate means no verified keys.
return Ok(None);
};
@@ -2752,7 +2736,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.peerstate else {
let Some(peerstate) = &mimeparser.decryption_info.peerstate else {
return Ok(NotVerified(
"No peerstate, the contact isn't verified".to_string(),
));
+2 -27
View File
@@ -4185,8 +4185,9 @@ 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(())
@@ -4946,32 +4947,6 @@ 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();
+1 -3
View File
@@ -21,9 +21,7 @@ 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)]
pub(crate) fn remove_message_footer<'a>(
lines: &'a [&str],
) -> (&'a [&'a str], Option<&'a [&'a str]>) {
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 {
+3 -6
View File
@@ -357,9 +357,9 @@ pub(crate) async fn send_msg_to_smtp(
.await
.context("failed to update retries count")?;
let Some((body, recipients, msg_id, retries)) = context
let (body, recipients, msg_id, retries) = context
.sql
.query_row_optional(
.query_row(
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
(rowid,),
|row| {
@@ -370,10 +370,7 @@ pub(crate) async fn send_msg_to_smtp(
Ok((mime, recipients, msg_id, retries))
},
)
.await?
else {
return Ok(());
};
.await?;
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.")
+9
View File
@@ -1068,6 +1068,15 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
migration_version,
)
.await?;
inc_and_check(&mut migration_version, 124)?;
if dbversion < migration_version {
sql.execute_migration(
"ALTER TABLE reactions ADD COLUMN rfc724_mid TEXT;",
migration_version,
)
.await?;
}
}
let new_version = sql