Merge remote-tracking branch 'origin/main' into hoc/channels-encryption-only-qrcodes

This commit is contained in:
Hocuri
2025-09-09 10:11:39 +02:00
51 changed files with 749 additions and 528 deletions

View File

@@ -17,7 +17,6 @@ pub enum EncryptPreference {
#[default]
NoPreference = 0,
Mutual = 1,
Reset = 20,
}
impl fmt::Display for EncryptPreference {
@@ -25,7 +24,6 @@ impl fmt::Display for EncryptPreference {
match *self {
EncryptPreference::Mutual => write!(fmt, "mutual"),
EncryptPreference::NoPreference => write!(fmt, "nopreference"),
EncryptPreference::Reset => write!(fmt, "reset"),
}
}
}
@@ -48,21 +46,13 @@ pub struct Aheader {
pub addr: String,
pub public_key: SignedPublicKey,
pub prefer_encrypt: EncryptPreference,
}
impl Aheader {
/// Creates new autocrypt header
pub fn new(
addr: String,
public_key: SignedPublicKey,
prefer_encrypt: EncryptPreference,
) -> Self {
Aheader {
addr,
public_key,
prefer_encrypt,
}
}
// Whether `_verified` attribute is present.
//
// `_verified` attribute is an extension to `Autocrypt-Gossip`
// header that is used to tell that the sender
// marked this key as verified.
pub verified: bool,
}
impl fmt::Display for Aheader {
@@ -71,6 +61,9 @@ impl fmt::Display for Aheader {
if self.prefer_encrypt == EncryptPreference::Mutual {
write!(fmt, " prefer-encrypt=mutual;")?;
}
if self.verified {
write!(fmt, " _verified=1;")?;
}
// adds a whitespace every 78 characters, this allows
// email crate to wrap the lines according to RFC 5322
@@ -125,6 +118,8 @@ impl FromStr for Aheader {
.and_then(|raw| raw.parse().ok())
.unwrap_or_default();
let verified = attributes.remove("_verified").is_some();
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
// Autocrypt-Level0: unknown attribute, treat the header as invalid
if attributes.keys().any(|k| !k.starts_with('_')) {
@@ -135,6 +130,7 @@ impl FromStr for Aheader {
addr,
public_key,
prefer_encrypt,
verified,
})
}
}
@@ -152,10 +148,11 @@ mod tests {
assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
assert_eq!(h.verified, false);
Ok(())
}
// EncryptPreference::Reset is an internal value, parser should never return it
// Non-standard values of prefer-encrypt such as `reset` are treated as no preference.
#[test]
fn test_from_str_reset() -> Result<()> {
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
@@ -245,11 +242,12 @@ mod tests {
assert!(
format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual,
verified: false
}
)
.contains("prefer-encrypt=mutual;")
);
@@ -260,11 +258,12 @@ mod tests {
assert!(
!format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::NoPreference
)
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference,
verified: false
}
)
.contains("prefer-encrypt")
);
@@ -273,13 +272,27 @@ mod tests {
assert!(
format!(
"{}",
Aheader::new(
"TeSt@eXaMpLe.cOm".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
Aheader {
addr: "TeSt@eXaMpLe.cOm".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual,
verified: false
}
)
.contains("test@example.com")
);
assert!(
format!(
"{}",
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference,
verified: true
}
)
.contains("_verified")
);
}
}

View File

@@ -1,16 +1,17 @@
//! # Handle calls.
//!
//! Internally, calls are bound to the user-visible info message initializing the call.
//! This means, the "Call ID" is a "Message ID" currently - similar to webxdc.
//! Internally, calls are bound a user-visible message initializing the call.
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
use crate::chat::{Chat, ChatId, send_msg};
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::message::{self, Message, MsgId, Viewtype, rfc724_mid_exists};
use crate::log::info;
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
use crate::sync::SyncData;
use crate::tools::time;
use anyhow::{Result, ensure};
use std::time::Duration;
@@ -28,29 +29,30 @@ use tokio::time::sleep;
/// as the callee won't start the call afterwards.
const RINGING_SECONDS: i64 = 60;
/// For persisting parameters in the call, we use Param::Arg*
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
/// Information about the status of a call.
#[derive(Debug, Default)]
pub struct CallInfo {
/// Incoming or outgoing call?
pub is_incoming: bool,
/// Was an incoming call accepted on this device?
/// For privacy reasons, only for accepted incoming calls, callee sends a message to caller on `end_call()`.
/// On other devices and for outgoing calls, `is_accepted` is never set.
pub is_accepted: bool,
/// User-defined text as given to place_outgoing_call()
pub place_call_info: String,
/// User-defined text as given to accept_incoming_call()
pub accept_call_info: String,
/// Info message referring to the call.
/// Message referring to the call.
/// Data are persisted along with the message using Param::Arg*
pub msg: Message,
}
impl CallInfo {
fn is_stale_call(&self) -> bool {
fn is_incoming(&self) -> bool {
self.msg.from_id != ContactId::SELF
}
fn is_stale(&self) -> bool {
self.remaining_ring_seconds() <= 0
}
@@ -69,6 +71,60 @@ impl CallInfo {
.await?;
Ok(())
}
async fn update_text_duration(&self, context: &Context) -> Result<()> {
let minutes = self.get_duration_seconds() / 60;
let duration = match minutes {
0 => "<1 minute".to_string(),
1 => "1 minute".to_string(),
n => format!("{} minutes", n),
};
if self.is_incoming() {
self.update_text(context, &format!("Incoming call\n{duration}"))
.await?;
} else {
self.update_text(context, &format!("Outgoing call\n{duration}"))
.await?;
}
Ok(())
}
/// Mark calls as accepted.
/// This is needed for all devices where a stale-timer runs, to prevent accepted calls being terminated as stale.
async fn mark_as_accepted(&mut self, context: &Context) -> Result<()> {
self.msg.param.set_i64(CALL_ACCEPTED_TIMESTAMP, time());
self.msg.update_param(context).await?;
Ok(())
}
fn is_accepted(&self) -> bool {
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
}
async fn mark_as_ended(&mut self, context: &Context) -> Result<()> {
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, time());
self.msg.update_param(context).await?;
Ok(())
}
fn is_ended(&self) -> bool {
self.msg.param.exists(CALL_ENDED_TIMESTAMP)
}
fn get_duration_seconds(&self) -> i64 {
if let (Some(start), Some(end)) = (
self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP),
self.msg.param.get_i64(CALL_ENDED_TIMESTAMP),
) {
let seconds = end - start;
if seconds <= 0 {
return 1;
}
return seconds;
}
0
}
}
impl Context {
@@ -82,11 +138,10 @@ impl Context {
ensure!(chat.typ == Chattype::Single && !chat.is_self_talk());
let mut call = Message {
viewtype: Viewtype::Text,
text: "Calling...".into(),
viewtype: Viewtype::Call,
text: "Outgoing call".into(),
..Default::default()
};
call.param.set_cmd(SystemMessage::OutgoingCall);
call.param.set(Param::WebrtcRoom, &place_call_info);
call.id = send_msg(self, chat_id, &mut call).await?;
@@ -107,60 +162,70 @@ impl Context {
accept_call_info: String,
) -> Result<()> {
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
ensure!(call.is_incoming);
ensure!(call.is_incoming());
if call.is_accepted() || call.is_ended() {
info!(self, "Call already accepted/ended");
return Ok(());
}
call.mark_as_accepted(self).await?;
let chat = Chat::load_from_db(self, call.msg.chat_id).await?;
if chat.is_contact_request() {
chat.id.accept(self).await?;
}
call.msg
.mark_call_as_accepted(self, accept_call_info.to_string())
.await?;
// send an acceptance message around: to the caller as well as to the other devices of the callee
let mut msg = Message {
viewtype: Viewtype::Text,
text: "Call accepted".into(),
text: "[Call accepted]".into(),
..Default::default()
};
msg.param.set_cmd(SystemMessage::CallAccepted);
msg.hidden = true;
msg.param
.set(Param::WebrtcAccepted, accept_call_info.to_string());
msg.set_quote(self, Some(&call.msg)).await?;
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
self.emit_event(EventType::IncomingCallAccepted {
msg_id: call.msg.id,
accept_call_info,
});
self.emit_msgs_changed(call.msg.chat_id, call_id);
Ok(())
}
/// Cancel, reject or hangup an incoming or outgoing call.
/// Cancel, decline or hangup an incoming or outgoing call.
pub async fn end_call(&self, call_id: MsgId) -> Result<()> {
let call: CallInfo = self.load_call_by_id(call_id).await?;
if call.is_accepted || !call.is_incoming {
let mut msg = Message {
viewtype: Viewtype::Text,
text: "Call ended".into(),
..Default::default()
};
msg.param.set_cmd(SystemMessage::CallEnded);
msg.set_quote(self, Some(&call.msg)).await?;
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
} else if call.is_incoming {
// to protect privacy, we do not send a message to others from callee for unaccepted calls
self.add_sync_item(SyncData::RejectIncomingCall {
msg: call.msg.rfc724_mid,
})
.await?;
self.scheduler.interrupt_inbox().await;
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
if call.is_ended() {
info!(self, "Call already ended");
return Ok(());
}
call.mark_as_ended(self).await?;
if !call.is_accepted() {
if call.is_incoming() {
call.update_text(self, "Declined call").await?;
} else {
call.update_text(self, "Cancelled call").await?;
}
} else {
call.update_text_duration(self).await?;
}
let mut msg = Message {
viewtype: Viewtype::Text,
text: "[Call ended]".into(),
..Default::default()
};
msg.param.set_cmd(SystemMessage::CallEnded);
msg.hidden = true;
msg.set_quote(self, Some(&call.msg)).await?;
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
self.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
});
self.emit_msgs_changed(call.msg.chat_id, call_id);
Ok(())
}
@@ -170,8 +235,15 @@ impl Context {
call_id: MsgId,
) -> Result<()> {
sleep(Duration::from_secs(wait)).await;
let call = context.load_call_by_id(call_id).await?;
if !call.is_accepted {
let mut call = context.load_call_by_id(call_id).await?;
if !call.is_accepted() && !call.is_ended() {
call.mark_as_ended(&context).await?;
if call.is_incoming() {
call.update_text(&context, "Missed call").await?;
} else {
call.update_text(&context, "Cancelled call").await?;
}
context.emit_msgs_changed(call.msg.chat_id, call_id);
context.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
});
@@ -181,70 +253,94 @@ impl Context {
pub(crate) async fn handle_call_msg(
&self,
mime_message: &MimeMessage,
call_id: MsgId,
mime_message: &MimeMessage,
from_id: ContactId,
) -> Result<()> {
match mime_message.is_system_message {
SystemMessage::IncomingCall => {
let call = self.load_call_by_id(call_id).await?;
if call.is_incoming {
if call.is_stale_call() {
call.update_text(self, "Missed call").await?;
self.emit_incoming_msg(call.msg.chat_id, call_id);
} else {
self.emit_msgs_changed(call.msg.chat_id, call_id);
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
place_call_info: call.place_call_info.to_string(),
});
let wait = call.remaining_ring_seconds();
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),
wait.try_into()?,
call.msg.id,
));
if mime_message.is_call() {
let call = self.load_call_by_id(call_id).await?;
if call.is_incoming() {
if call.is_stale() {
call.update_text(self, "Missed call").await?;
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
} else {
call.update_text(self, "Incoming call").await?;
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
place_call_info: call.place_call_info.to_string(),
});
let wait = call.remaining_ring_seconds();
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),
wait.try_into()?,
call.msg.id,
));
}
} else {
call.update_text(self, "Outgoing call").await?;
self.emit_msgs_changed(call.msg.chat_id, call_id);
}
} else {
match mime_message.is_system_message {
SystemMessage::CallAccepted => {
let mut call = self.load_call_by_id(call_id).await?;
if call.is_ended() || call.is_accepted() {
info!(self, "CallAccepted received for accepted/ended call");
return Ok(());
}
} else {
self.emit_msgs_changed(call.msg.chat_id, call_id);
}
}
SystemMessage::CallAccepted => {
let call = self.load_call_by_id(call_id).await?;
self.emit_msgs_changed(call.msg.chat_id, call_id);
if call.is_incoming {
self.emit_event(EventType::IncomingCallAccepted {
msg_id: call.msg.id,
accept_call_info: call.accept_call_info,
});
} else {
let accept_call_info = mime_message
.get_header(HeaderDef::ChatWebrtcAccepted)
.unwrap_or_default();
call.msg
.clone()
.mark_call_as_accepted(self, accept_call_info.to_string())
.await?;
self.emit_event(EventType::OutgoingCallAccepted {
msg_id: call.msg.id,
accept_call_info: accept_call_info.to_string(),
});
}
}
SystemMessage::CallEnded => {
let call = self.load_call_by_id(call_id).await?;
self.emit_msgs_changed(call.msg.chat_id, call_id);
self.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
});
}
_ => {}
}
Ok(())
}
pub(crate) async fn sync_call_rejection(&self, rfc724_mid: &str) -> Result<()> {
if let Some((msg_id, _)) = rfc724_mid_exists(self, rfc724_mid).await? {
self.emit_event(EventType::CallEnded { msg_id });
call.mark_as_accepted(self).await?;
self.emit_msgs_changed(call.msg.chat_id, call_id);
if call.is_incoming() {
self.emit_event(EventType::IncomingCallAccepted {
msg_id: call.msg.id,
});
} else {
let accept_call_info = mime_message
.get_header(HeaderDef::ChatWebrtcAccepted)
.unwrap_or_default();
self.emit_event(EventType::OutgoingCallAccepted {
msg_id: call.msg.id,
accept_call_info: accept_call_info.to_string(),
});
}
}
SystemMessage::CallEnded => {
let mut call = self.load_call_by_id(call_id).await?;
if call.is_ended() {
// may happen eg. if a a message is missed
info!(self, "CallEnded received for ended call");
return Ok(());
}
call.mark_as_ended(self).await?;
if !call.is_accepted() {
if call.is_incoming() {
if from_id == ContactId::SELF {
call.update_text(self, "Declined call").await?;
} else {
call.update_text(self, "Missed call").await?;
}
} else {
// outgoing
if from_id == ContactId::SELF {
call.update_text(self, "Cancelled call").await?;
} else {
call.update_text(self, "Declined call").await?;
}
}
} else {
call.update_text_duration(self).await?;
}
self.emit_msgs_changed(call.msg.chat_id, call_id);
self.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
});
}
_ => {}
}
}
Ok(())
}
@@ -255,14 +351,9 @@ impl Context {
}
fn load_call_by_message(&self, call: Message) -> Result<CallInfo> {
ensure!(
call.get_info_type() == SystemMessage::IncomingCall
|| call.get_info_type() == SystemMessage::OutgoingCall
);
ensure!(call.viewtype == Viewtype::Call);
Ok(CallInfo {
is_incoming: call.get_info_type() == SystemMessage::IncomingCall,
is_accepted: call.is_call_accepted()?,
place_call_info: call
.param
.get(Param::WebrtcRoom)
@@ -278,30 +369,5 @@ impl Context {
}
}
impl Message {
async fn mark_call_as_accepted(
&mut self,
context: &Context,
accept_call_info: String,
) -> Result<()> {
ensure!(
self.get_info_type() == SystemMessage::IncomingCall
|| self.get_info_type() == SystemMessage::OutgoingCall
);
self.param.set_int(Param::Arg, 1);
self.param.set(Param::WebrtcAccepted, accept_call_info);
self.update_param(context).await?;
Ok(())
}
fn is_call_accepted(&self) -> Result<bool> {
ensure!(
self.get_info_type() == SystemMessage::IncomingCall
|| self.get_info_type() == SystemMessage::OutgoingCall
);
Ok(self.param.get_int(Param::Arg) == Some(1))
}
}
#[cfg(test)]
mod calls_tests;

View File

@@ -1,17 +1,23 @@
use super::*;
use crate::config::Config;
use crate::test_utils::{TestContext, TestContextManager, sync};
use crate::test_utils::{TestContext, TestContextManager};
struct CallSetup {
pub alice: TestContext,
pub alice2: TestContext,
pub alice_call: Message,
pub alice2_call: Message,
pub bob: TestContext,
pub bob2: TestContext,
pub bob_call: Message,
pub bob2_call: Message,
}
async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()> {
assert_eq!(Message::load_from_db(t, call_id).await?.text, text);
Ok(())
}
async fn setup_call() -> Result<CallSetup> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
@@ -26,18 +32,20 @@ async fn setup_call() -> Result<CallSetup> {
// Alice's other device sees the same message as an outgoing call.
let alice_chat = alice.create_chat(&bob).await;
let test_msg_id = alice
.place_outgoing_call(alice_chat.id, "place_info".to_string())
.place_outgoing_call(alice_chat.id, "place-info-123".to_string())
.await?;
let sent1 = alice.pop_sent_msg().await;
assert_eq!(sent1.sender_msg_id, test_msg_id);
let alice_call = Message::load_from_db(&alice, sent1.sender_msg_id).await?;
let alice2_call = alice2.recv_msg(&sent1).await;
for (t, m) in [(&alice, &alice_call), (&alice2, &alice2_call)] {
assert!(m.is_info());
assert_eq!(m.get_info_type(), SystemMessage::OutgoingCall);
assert!(!m.is_info());
assert_eq!(m.viewtype, Viewtype::Call);
let info = t.load_call_by_id(m.id).await?;
assert!(!info.is_accepted);
assert_eq!(info.place_call_info, "place_info");
assert!(!info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
assert_text(t, m.id, "Outgoing call").await?;
}
// Bob receives the message referring to the call on two devices;
@@ -45,20 +53,23 @@ async fn setup_call() -> Result<CallSetup> {
let bob_call = bob.recv_msg(&sent1).await;
let bob2_call = bob2.recv_msg(&sent1).await;
for (t, m) in [(&bob, &bob_call), (&bob2, &bob2_call)] {
assert!(m.is_info());
assert_eq!(m.get_info_type(), SystemMessage::IncomingCall);
assert!(!m.is_info());
assert_eq!(m.viewtype, Viewtype::Call);
t.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCall { .. }))
.await;
let info = t.load_call_by_id(m.id).await?;
assert!(!info.is_accepted);
assert_eq!(info.place_call_info, "place_info");
assert!(info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
assert_text(t, m.id, "Incoming call").await?;
}
Ok(CallSetup {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
@@ -71,55 +82,53 @@ async fn accept_call() -> Result<CallSetup> {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
} = setup_call().await?;
// Bob accepts the incoming call, this does not add an additional message to the chat
bob.accept_incoming_call(bob_call.id, "accepted_info".to_string())
// Bob accepts the incoming call
bob.accept_incoming_call(bob_call.id, "accept-info-456".to_string())
.await?;
assert_text(&bob, bob_call.id, "Incoming call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
let sent2 = bob.pop_sent_msg().await;
let info = bob.load_call_by_id(bob_call.id).await?;
assert!(info.is_accepted);
assert_eq!(info.place_call_info, "place_info");
assert_eq!(info.accept_call_info, "accepted_info");
assert!(info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
let bob_accept_msg = bob2.recv_msg(&sent2).await;
assert!(bob_accept_msg.is_info());
assert_eq!(bob_accept_msg.get_info_type(), SystemMessage::CallAccepted);
bob2.recv_msg_trash(&sent2).await;
assert_text(&bob, bob_call.id, "Incoming call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
let info = bob2.load_call_by_id(bob2_call.id).await?;
assert!(!info.is_accepted); // "accepted" is only true on the device that does the call
assert!(info.is_accepted());
// Alice receives the acceptance message
let alice_accept_msg = alice.recv_msg(&sent2).await;
assert!(alice_accept_msg.is_info());
assert_eq!(
alice_accept_msg.get_info_type(),
SystemMessage::CallAccepted
);
alice
alice.recv_msg_trash(&sent2).await;
assert_text(&alice, alice_call.id, "Outgoing call").await?;
let ev = alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
.await;
let info = alice.load_call_by_id(alice_call.id).await?;
assert!(info.is_accepted);
assert_eq!(info.place_call_info, "place_info");
assert_eq!(info.accept_call_info, "accepted_info");
let alice2_accept_msg = alice2.recv_msg(&sent2).await;
assert!(alice2_accept_msg.is_info());
assert_eq!(
alice2_accept_msg.get_info_type(),
SystemMessage::CallAccepted
ev,
EventType::OutgoingCallAccepted {
msg_id: alice2_call.id,
accept_call_info: "accept-info-456".to_string()
}
);
let info = alice.load_call_by_id(alice_call.id).await?;
assert!(info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
alice2.recv_msg_trash(&sent2).await;
assert_text(&alice2, alice2_call.id, "Outgoing call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
@@ -129,6 +138,7 @@ async fn accept_call() -> Result<CallSetup> {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
@@ -136,46 +146,45 @@ async fn accept_call() -> Result<CallSetup> {
})
}
fn assert_is_call_ended_info_msg(msg: Message) {
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::CallEnded);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_accept_call_callee_ends() -> Result<()> {
// Alice calls Bob, Bob accepts
let CallSetup {
alice,
alice_call,
alice2,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = accept_call().await?;
// Bob has accepted the call and also ends it
bob.end_call(bob_call.id).await?;
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = bob.pop_sent_msg().await;
let bob2_end_call_msg = bob2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(bob2_end_call_msg);
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
// Alice receives the ending message
let alice_end_call_msg = alice.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(alice_end_call_msg);
alice.recv_msg_trash(&sent3).await;
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let alice2_end_call_msg = alice2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(alice2_end_call_msg);
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -189,37 +198,41 @@ async fn test_accept_call_caller_ends() -> Result<()> {
// Alice calls Bob, Bob accepts
let CallSetup {
alice,
alice_call,
alice2,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = accept_call().await?;
// Bob has accepted the call but Alice ends it
alice.end_call(bob_call.id).await?;
alice.end_call(alice_call.id).await?;
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = alice.pop_sent_msg().await;
let alice2_end_call_msg = alice2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(alice2_end_call_msg);
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
// Bob receives the ending message
let bob_end_call_msg = bob.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(bob_end_call_msg);
bob.recv_msg_trash(&sent3).await;
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let bob2_end_call_msg = bob2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(bob2_end_call_msg);
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -231,25 +244,47 @@ async fn test_accept_call_caller_ends() -> Result<()> {
async fn test_callee_rejects_call() -> Result<()> {
// Alice calls Bob
let CallSetup {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = setup_call().await?;
// Bob does not want to talk with Alice.
// To protect Bob's privacy, no message is sent to Alice (who will time out).
// To let Bob close the call window on all devices, a sync message is used instead.
// Bob has accepted Alice before, but does not want to talk with Alice
bob_call.chat_id.accept(&bob).await?;
bob.end_call(bob_call.id).await?;
assert_text(&bob, bob_call.id, "Declined call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = bob.pop_sent_msg().await;
sync(&bob, &bob2).await;
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Declined call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
// Alice receives decline message
alice.recv_msg_trash(&sent3).await;
assert_text(&alice, alice_call.id, "Declined call").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Declined call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
Ok(())
}
@@ -260,35 +295,39 @@ async fn test_caller_cancels_call() -> Result<()> {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = setup_call().await?;
// Alice changes their mind before Bob picks up
alice.end_call(alice_call.id).await?;
assert_text(&alice, alice_call.id, "Cancelled call").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = alice.pop_sent_msg().await;
let alice2_call_ended_msg = alice2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(alice2_call_ended_msg);
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Cancelled call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
// Bob receives the ending message
let bob_call_ended_msg = bob.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(bob_call_ended_msg);
bob.recv_msg_trash(&sent3).await;
assert_text(&bob, bob_call.id, "Missed call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let bob2_call_ended_msg = bob2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(bob2_call_ended_msg);
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Missed call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -306,7 +345,7 @@ async fn test_is_stale_call() -> Result<()> {
},
..Default::default()
};
assert!(!call_info.is_stale_call());
assert!(!call_info.is_stale());
let remaining_seconds = call_info.remaining_ring_seconds();
assert!(remaining_seconds == RINGING_SECONDS || remaining_seconds == RINGING_SECONDS - 1);
@@ -318,7 +357,7 @@ async fn test_is_stale_call() -> Result<()> {
},
..Default::default()
};
assert!(!call_info.is_stale_call());
assert!(!call_info.is_stale());
let remaining_seconds = call_info.remaining_ring_seconds();
assert!(remaining_seconds == RINGING_SECONDS - 5 || remaining_seconds == RINGING_SECONDS - 6);
@@ -330,28 +369,32 @@ async fn test_is_stale_call() -> Result<()> {
},
..Default::default()
};
assert!(call_info.is_stale_call());
assert!(call_info.is_stale());
assert_eq!(call_info.remaining_ring_seconds(), 0);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mark_call_as_accepted() -> Result<()> {
async fn test_mark_calls() -> Result<()> {
let CallSetup {
alice, alice_call, ..
} = setup_call().await?;
assert!(!alice_call.is_call_accepted()?);
let mut alice_call = Message::load_from_db(&alice, alice_call.id).await?;
assert!(!alice_call.is_call_accepted()?);
alice_call
.mark_call_as_accepted(&alice, "accepted_info".to_string())
.await?;
assert!(alice_call.is_call_accepted()?);
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
assert!(!call_info.is_accepted());
assert!(!call_info.is_ended());
call_info.mark_as_accepted(&alice).await?;
assert!(call_info.is_accepted());
assert!(!call_info.is_ended());
let alice_call = Message::load_from_db(&alice, alice_call.id).await?;
assert!(alice_call.is_call_accepted()?);
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
assert!(call_info.is_accepted());
assert!(!call_info.is_ended());
call_info.mark_as_ended(&alice).await?;
assert!(call_info.is_accepted());
assert!(call_info.is_ended());
Ok(())
}

View File

@@ -2717,7 +2717,10 @@ impl ChatIdBlocked {
}
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
if msg.viewtype == Viewtype::Text
|| msg.viewtype == Viewtype::VideochatInvitation
|| msg.viewtype == Viewtype::Call
{
// the caller should check if the message text is empty
} else if msg.viewtype.has_file() {
let viewtype_orig = msg.viewtype;
@@ -3204,6 +3207,7 @@ pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: Strin
original_msg.viewtype != Viewtype::VideochatInvitation,
"Cannot edit videochat invitations"
);
ensure!(original_msg.viewtype != Viewtype::Call, "Cannot edit calls");
ensure!(
!original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings
"Cannot add text"

View File

@@ -151,10 +151,6 @@ pub enum Config {
/// setting up a second device, or receiving a sync message.
BccSelf,
/// True if encryption is preferred according to Autocrypt standard.
#[strum(props(default = "1"))]
E2eeEnabled,
/// True if Message Delivery Notifications (read receipts) should
/// be sent and requested.
#[strum(props(default = "1"))]
@@ -705,7 +701,6 @@ impl Context {
Config::Socks5Enabled
| Config::ProxyEnabled
| Config::BccSelf
| Config::E2eeEnabled
| Config::MdnsEnabled
| Config::SentboxWatch
| Config::MvboxMove

View File

@@ -833,7 +833,6 @@ impl Context {
.query_get_value("PRAGMA journal_mode;", ())
.await?
.unwrap_or_else(|| "unknown".to_string());
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
let sync_msgs = self.get_config_int(Config::SyncMsgs).await?;
@@ -967,7 +966,6 @@ impl Context {
res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("configured_trash_folder", configured_trash_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert("bcc_self", bcc_self.to_string());
res.insert("sync_msgs", sync_msgs.to_string());
res.insert("disable_idle", disable_idle.to_string());

View File

@@ -4,10 +4,8 @@ use std::io::Cursor;
use anyhow::Result;
use mail_builder::mime::MimePart;
use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
use crate::context::Context;
use crate::key::{SignedPublicKey, load_self_public_key, load_self_secret_key};
use crate::pgp;
@@ -21,9 +19,7 @@ pub struct EncryptHelper {
impl EncryptHelper {
pub async fn new(context: &Context) -> Result<EncryptHelper> {
let prefer_encrypt =
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
.unwrap_or_default();
let prefer_encrypt = EncryptPreference::Mutual;
let addr = context.get_primary_self_addr().await?;
let public_key = load_self_public_key(context).await?;
@@ -35,9 +31,12 @@ impl EncryptHelper {
}
pub fn get_aheader(&self) -> Aheader {
let pk = self.public_key.clone();
let addr = self.addr.to_string();
Aheader::new(addr, pk, self.prefer_encrypt)
Aheader {
addr: self.addr.clone(),
public_key: self.public_key.clone(),
prefer_encrypt: self.prefer_encrypt,
verified: false,
}
}
/// Tries to encrypt the passed in `mail`.

View File

@@ -388,8 +388,6 @@ pub enum EventType {
IncomingCallAccepted {
/// ID of the message referring to the call.
msg_id: MsgId,
/// User-defined info as passed to accept_incoming_call()
accept_call_info: String,
},
/// Outgoing call accepted.

View File

@@ -140,32 +140,8 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
}
async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
// try hard to only modify key-state
let (private_key, header) = SignedSecretKey::from_asc(armored)?;
let private_key = SignedSecretKey::from_asc(armored)?;
let public_key = private_key.split_public_key()?;
if let Some(preferencrypt) = header.get("Autocrypt-Prefer-Encrypt") {
let e2ee_enabled = match preferencrypt.as_str() {
"nopreference" => 0,
"mutual" => 1,
_ => {
bail!("invalid Autocrypt-Prefer-Encrypt header: {:?}", header);
}
};
context
.sql
.set_raw_config_int("e2ee_enabled", e2ee_enabled)
.await?;
} else {
// `Autocrypt-Prefer-Encrypt` is not included
// in keys exported to file.
//
// `Autocrypt-Prefer-Encrypt` also SHOULD be sent
// in Autocrypt Setup Message according to Autocrypt specification,
// but K-9 6.802 does not include this header.
//
// We keep current setting in this case.
info!(context, "No Autocrypt-Prefer-Encrypt header.");
};
let keypair = pgp::KeyPair {
public: public_key,

View File

@@ -93,10 +93,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
bail!("Passphrase must be at least 2 chars long.");
};
let private_key = load_self_secret_key(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
};
let ac_headers = Some(("Autocrypt-Prefer-Encrypt", "mutual"));
let private_key_asc = private_key.to_asc(ac_headers);
let encr = pgp::symm_encrypt_setup_file(passphrase, private_key_asc.into_bytes())
.await?

View File

@@ -3,7 +3,6 @@
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;
use std::collections::BTreeMap;
use crate::chat::ChatId;
use crate::context::Context;
@@ -14,7 +13,7 @@ pub use crate::pgp;
use self::pgp::KeyPair;
pub fn key_from_asc(data: &str) -> Result<(key::SignedSecretKey, BTreeMap<String, String>)> {
pub fn key_from_asc(data: &str) -> Result<key::SignedSecretKey> {
key::SignedSecretKey::from_asc(data)
}

View File

@@ -71,31 +71,17 @@ pub(crate) trait DcKey: Serialize + Deserializable + Clone {
}
/// Create a key from an ASCII-armored string.
///
/// Returns the key and a map of any headers which might have been set in
/// the ASCII-armored representation.
fn from_asc(data: &str) -> Result<(Self, BTreeMap<String, String>)> {
fn from_asc(data: &str) -> Result<Self> {
let bytes = data.as_bytes();
let res = Self::from_armor_single(Cursor::new(bytes));
let (key, headers) = match res {
let (key, _headers) = match res {
Err(pgp::errors::Error::NoMatchingPacket { .. }) => match Self::is_private() {
true => bail!("No private key packet found"),
false => bail!("No public key packet found"),
},
_ => res.context("rPGP error")?,
};
let headers = headers
.into_iter()
.map(|(key, values)| {
(
key.trim().to_lowercase(),
values
.last()
.map_or_else(String::new, |s| s.trim().to_string()),
)
})
.collect();
Ok((key, headers))
Ok(key)
}
/// Serialise the key as bytes.
@@ -446,7 +432,7 @@ pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) ->
/// to avoid generating the key in tests.
/// Use import/export APIs instead.
pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Result<()> {
let secret = SignedSecretKey::from_asc(secret_data)?.0;
let secret = SignedSecretKey::from_asc(secret_data)?;
let public = secret.split_public_key()?;
let keypair = KeyPair { public, secret };
store_self_keypair(context, &keypair).await?;
@@ -532,7 +518,7 @@ mod tests {
#[test]
fn test_from_armored_string() {
let (private_key, _) = SignedSecretKey::from_asc(
let private_key = SignedSecretKey::from_asc(
"-----BEGIN PGP PRIVATE KEY BLOCK-----
xcLYBF0fgz4BCADnRUV52V4xhSsU56ZaAn3+3oG86MZhXy4X8w14WZZDf0VJGeTh
@@ -600,17 +586,13 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
fn test_asc_roundtrip() {
let key = KEYPAIR.public.clone();
let asc = key.to_asc(Some(("spam", "ham")));
let (key2, hdrs) = SignedPublicKey::from_asc(&asc).unwrap();
let key2 = SignedPublicKey::from_asc(&asc).unwrap();
assert_eq!(key, key2);
assert_eq!(hdrs.len(), 1);
assert_eq!(hdrs.get("spam"), Some(&String::from("ham")));
let key = KEYPAIR.secret.clone();
let asc = key.to_asc(Some(("spam", "ham")));
let (key2, hdrs) = SignedSecretKey::from_asc(&asc).unwrap();
let key2 = SignedSecretKey::from_asc(&asc).unwrap();
assert_eq!(key, key2);
assert_eq!(hdrs.len(), 1);
assert_eq!(hdrs.get("spam"), Some(&String::from("ham")));
}
#[test]

View File

@@ -973,8 +973,6 @@ impl Message {
| SystemMessage::WebxdcStatusUpdate
| SystemMessage::WebxdcInfoMessage
| SystemMessage::IrohNodeAddr
| SystemMessage::OutgoingCall
| SystemMessage::IncomingCall
| SystemMessage::CallAccepted
| SystemMessage::CallEnded
| SystemMessage::Unknown => Ok(None),
@@ -2293,6 +2291,9 @@ pub enum Viewtype {
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
/// Message is an incoming or outgoing call.
Call = 71,
/// Message is an webxdc instance.
Webxdc = 80,
@@ -2316,6 +2317,7 @@ impl Viewtype {
Viewtype::Video => true,
Viewtype::File => true,
Viewtype::VideochatInvitation => false,
Viewtype::Call => false,
Viewtype::Webxdc => true,
Viewtype::Vcard => true,
}

View File

@@ -3,7 +3,7 @@
use std::collections::{BTreeSet, HashSet};
use std::io::Cursor;
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Context as _, Result, bail};
use base64::Engine as _;
use data_encoding::BASE32_NOPAD;
use deltachat_contact_tools::sanitize_bidi_characters;
@@ -1132,13 +1132,14 @@ impl MimeFactory {
continue;
}
let header = Aheader::new(
addr.clone(),
key.clone(),
let header = Aheader {
addr: addr.clone(),
public_key: key.clone(),
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included.
EncryptPreference::NoPreference,
)
prefer_encrypt: EncryptPreference::NoPreference,
verified: false,
}
.to_string();
message = message.header(
@@ -1614,15 +1615,6 @@ impl MimeFactory {
.into(),
));
}
SystemMessage::OutgoingCall => {
headers.push((
"Chat-Content",
mail_builder::headers::raw::Raw::new("call").into(),
));
}
SystemMessage::IncomingCall => {
return Err(anyhow!("Unexpected incoming call rendering."));
}
SystemMessage::CallAccepted => {
headers.push((
"Chat-Content",
@@ -1659,6 +1651,14 @@ impl MimeFactory {
"Chat-Content",
mail_builder::headers::raw::Raw::new("videochat-invitation").into(),
));
} else if msg.viewtype == Viewtype::Call {
headers.push((
"Chat-Content",
mail_builder::headers::raw::Raw::new("call").into(),
));
placeholdertext = Some(
"[This is a 'Call'. The sender uses an experiment not supported on your version yet]".to_string(),
);
}
if msg.param.exists(Param::WebrtcRoom) {

View File

@@ -1,7 +1,7 @@
//! # MIME message parsing module.
use std::cmp::min;
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::Path;
use std::str;
use std::str::FromStr;
@@ -36,6 +36,17 @@ use crate::tools::{
use crate::{chatlist_events, location, stock_str, tools};
use crate::{constants, token};
/// Public key extracted from `Autocrypt-Gossip`
/// header with associated information.
#[derive(Debug)]
pub struct GossipedKey {
/// Public key extracted from `keydata` attribute.
pub public_key: SignedPublicKey,
/// True if `Autocrypt-Gossip` has a `_verified` attribute.
pub verified: bool,
}
/// A parsed MIME message.
///
/// This represents the relevant information of a parsed MIME message
@@ -85,7 +96,7 @@ pub(crate) struct MimeMessage {
/// The addresses for which there was a gossip header
/// and their respective gossiped keys.
pub gossiped_keys: HashMap<String, SignedPublicKey>,
pub gossiped_keys: BTreeMap<String, GossipedKey>,
/// Fingerprint of the key in the Autocrypt header.
///
@@ -217,17 +228,7 @@ pub enum SystemMessage {
/// "Messages are end-to-end encrypted."
ChatE2ee = 50,
/// This system message represents an outgoing call.
/// This message is visible to the user as an "info" message.
OutgoingCall = 60,
/// This system message represents an incoming call.
/// This message is visible to the user as an "info" message.
IncomingCall = 65,
/// Message indicating that a call was accepted.
/// While the 1:1 call may be established elsewhere,
/// the message is still needed for a multidevice setup, so that other devices stop ringing.
CallAccepted = 66,
/// Message indicating that a call was ended.
@@ -705,12 +706,6 @@ impl MimeMessage {
self.is_system_message = SystemMessage::ChatProtectionDisabled;
} else if value == "group-avatar-changed" {
self.is_system_message = SystemMessage::GroupImageChanged;
} else if value == "call" {
self.is_system_message = if self.incoming {
SystemMessage::IncomingCall
} else {
SystemMessage::OutgoingCall
};
} else if value == "call-accepted" {
self.is_system_message = SystemMessage::CallAccepted;
} else if value == "call-ended" {
@@ -751,6 +746,8 @@ impl MimeMessage {
if let Some(room) = room {
if content == "videochat-invitation" {
part.typ = Viewtype::VideochatInvitation;
} else if content == "call" {
part.typ = Viewtype::Call
}
part.param.set(Param::WebrtcRoom, room);
} else if let Some(accepted) = accepted {
@@ -780,7 +777,10 @@ impl MimeMessage {
| Viewtype::Vcard
| Viewtype::File
| Viewtype::Webxdc => true,
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
Viewtype::Unknown
| Viewtype::Text
| Viewtype::VideochatInvitation
| Viewtype::Call => false,
})
{
let mut parts = std::mem::take(&mut self.parts);
@@ -1532,7 +1532,7 @@ impl MimeMessage {
);
return Ok(false);
}
Ok((key, _)) => key,
Ok(key) => key,
};
if let Err(err) = key.verify() {
warn!(context, "Attached PGP key verification failed: {err:#}.");
@@ -1595,6 +1595,13 @@ impl MimeMessage {
}
}
/// Check if a message is a call.
pub(crate) fn is_call(&self) -> bool {
self.parts
.first()
.is_some_and(|part| part.typ == Viewtype::Call)
}
pub fn replace_msg_by_error(&mut self, error_msg: &str) {
self.is_system_message = SystemMessage::Unknown;
if let Some(part) = self.parts.first_mut() {
@@ -1989,9 +1996,9 @@ async fn parse_gossip_headers(
from: &str,
recipients: &[SingleInfo],
gossip_headers: Vec<String>,
) -> Result<HashMap<String, SignedPublicKey>> {
) -> Result<BTreeMap<String, GossipedKey>> {
// XXX split the parsing from the modification part
let mut gossiped_keys: HashMap<String, SignedPublicKey> = Default::default();
let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
for value in &gossip_headers {
let header = match value.parse::<Aheader>() {
@@ -2033,7 +2040,12 @@ async fn parse_gossip_headers(
)
.await?;
gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
let gossiped_key = GossipedKey {
public_key: header.public_key,
verified: header.verified,
};
gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
}
Ok(gossiped_keys)

View File

@@ -13,8 +13,8 @@ use std::sync::LazyLock;
// 163.md: 163.com
static P_163: Provider = Provider {
id: "163",
status: Status::Ok,
before_login_hint: "",
status: Status::Preparation,
before_login_hint: "Enable \"POP3/SMTP/IMAP\" on the website, add a third-party auth code and use that as the login password",
after_login_hint: "",
overview_page: "https://providers.delta.chat/163",
server: &[
@@ -98,7 +98,7 @@ static P_ALIYUN: Provider = Provider {
static P_AOL: Provider = Provider {
id: "aol",
status: Status::Preparation,
before_login_hint: "To log in to AOL with Delta Chat, you need to set up an app password in the AOL web interface.",
before_login_hint: "To log in to AOL, you need to set up an app password in the AOL web interface.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/aol",
server: &[
@@ -432,7 +432,7 @@ static P_EXAMPLE_COM: Provider = Provider {
id: "example.com",
status: Status::Broken,
before_login_hint: "Hush this provider doesn't exist!",
after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider for Delta Chat, take a look at providers.delta.chat!",
after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider, take a look at providers.delta.chat!",
overview_page: "https://providers.delta.chat/example-com",
server: &[
Server {
@@ -459,7 +459,7 @@ static P_EXAMPLE_COM: Provider = Provider {
static P_FASTMAIL: Provider = Provider {
id: "fastmail",
status: Status::Preparation,
before_login_hint: "You must create an app-specific password for Delta Chat before you can log in.",
before_login_hint: "You must create an app-specific password before you can log in.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/fastmail",
server: &[
@@ -526,7 +526,7 @@ static P_FIVE_CHAT: Provider = Provider {
static P_FREENET_DE: Provider = Provider {
id: "freenet.de",
status: Status::Preparation,
before_login_hint: "Um deine freenet.de E-Mail-Adresse mit Delta Chat zu benutzen, musst du erst auf der freenet.de-Webseite \"POP3/IMAP/SMTP\" aktivieren.",
before_login_hint: "Um deine freenet.de E-Mail-Adresse zu benutzen, musst du erst auf der freenet.de-Webseite \"POP3/IMAP/SMTP\" aktivieren.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/freenet-de",
server: &[
@@ -647,10 +647,6 @@ static P_HERMES_RADIO: Provider = Provider {
key: Config::MdnsEnabled,
value: "0",
},
ConfigDefault {
key: Config::E2eeEnabled,
value: "0",
},
ConfigDefault {
key: Config::ShowEmails,
value: "2",
@@ -663,7 +659,7 @@ static P_HERMES_RADIO: Provider = Provider {
static P_HEY_COM: Provider = Provider {
id: "hey.com",
status: Status::Broken,
before_login_hint: "hey.com does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to hey.com.",
before_login_hint: "hey.com does not offer the standard IMAP e-mail protocol, so you cannot log in to hey.com.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/hey-com",
server: &[],
@@ -702,7 +698,7 @@ static P_I3_NET: Provider = Provider {
static P_ICLOUD: Provider = Provider {
id: "icloud",
status: Status::Preparation,
before_login_hint: "You must create an app-specific password for Delta Chat before login.",
before_login_hint: "You must create an app-specific password before login.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/icloud",
server: &[
@@ -787,7 +783,7 @@ static P_KONTENT_COM: Provider = Provider {
static P_MAIL_COM: Provider = Provider {
id: "mail.com",
status: Status::Preparation,
before_login_hint: "To log in with Delta Chat, you first need to activate POP3/IMAP in your mail.com settings. Note that this is a mail.com Premium feature only.",
before_login_hint: "To log in, you first need to activate POP3/IMAP in your mail.com settings. Note that this is a mail.com Premium feature only.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail-com",
server: &[],
@@ -828,7 +824,7 @@ static P_MAIL_DE: Provider = Provider {
static P_MAIL_RU: Provider = Provider {
id: "mail.ru",
status: Status::Preparation,
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с Delta Chat.",
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с chatmail.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail-ru",
server: &[
@@ -1222,8 +1218,8 @@ static P_NUBO_COOP: Provider = Provider {
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de
static P_OUTLOOK_COM: Provider = Provider {
id: "outlook.com",
status: Status::Ok,
before_login_hint: "",
status: Status::Broken,
before_login_hint: "Unfortunately, Outlook does not allow using passwords anymore, per-app-passwords are currently not working.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/outlook-com",
server: &[
@@ -1321,8 +1317,8 @@ static P_POSTEO: Provider = Provider {
static P_PROTONMAIL: Provider = Provider {
id: "protonmail",
status: Status::Broken,
before_login_hint: "Protonmail does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Protonmail.",
after_login_hint: "To use Delta Chat with Protonmail, the IMAP bridge must be running in the background. If you have connectivity issues, double check whether it works as expected.",
before_login_hint: "Protonmail does not offer the standard IMAP e-mail protocol, so you cannot log in with to Protonmail.",
after_login_hint: "To use Protonmail, the IMAP bridge must be running in the background. If you have connectivity issues, double check whether it works as expected.",
overview_page: "https://providers.delta.chat/protonmail",
server: &[],
opt: ProviderOptions::new(),
@@ -1362,7 +1358,7 @@ static P_PURELYMAIL_COM: Provider = Provider {
static P_QQ: Provider = Provider {
id: "qq",
status: Status::Preparation,
before_login_hint: "Manually enabling IMAP/SMTP and creating an app-specific password for Delta Chat are required.",
before_login_hint: "Manually enabling IMAP/SMTP and creating an app-specific password are required.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/qq",
server: &[
@@ -1390,7 +1386,7 @@ static P_QQ: Provider = Provider {
static P_RAMBLER_RU: Provider = Provider {
id: "rambler.ru",
status: Status::Preparation,
before_login_hint: "Чтобы войти в Рамблер/почта через Delta Chat, необходимо предварительно включить доступ с помощью почтовых клиентов на сайте mail.rambler.ru",
before_login_hint: "Чтобы войти в Рамблер/почта, необходимо предварительно включить доступ с помощью почтовых клиентов на сайте mail.rambler.ru",
after_login_hint: "",
overview_page: "https://providers.delta.chat/rambler-ru",
server: &[
@@ -1566,7 +1562,7 @@ static P_SYSTEMLI_ORG: Provider = Provider {
static P_T_ONLINE: Provider = Provider {
id: "t-online",
status: Status::Preparation,
before_login_hint: "To use Delta Chat with a T-Online email address, you need to create an app password in the web interface.",
before_login_hint: "To use a T-Online email address, you need to create an app password in the web interface.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/t-online",
server: &[
@@ -1677,7 +1673,7 @@ static P_TISCALI_IT: Provider = Provider {
static P_TUTANOTA: Provider = Provider {
id: "tutanota",
status: Status::Broken,
before_login_hint: "Tutanota does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Tutanota.",
before_login_hint: "Tutanota does not offer the standard IMAP e-mail protocol, so you cannot log in to Tutanota.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/tutanota",
server: &[],
@@ -1787,7 +1783,7 @@ static P_VIVALDI: Provider = Provider {
static P_VK_COM: Provider = Provider {
id: "vk.com",
status: Status::Preparation,
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru https://account.mail.ru/user/2-step-auth/passwords/ чтобы vk.com работал с Delta Chat.",
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru https://account.mail.ru/user/2-step-auth/passwords/ чтобы vk.com работал с chatmail.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/vk-com",
server: &[
@@ -1906,7 +1902,7 @@ static P_WKPB_DE: Provider = Provider {
static P_YAHOO: Provider = Provider {
id: "yahoo",
status: Status::Preparation,
before_login_hint: "To use Delta Chat with your Yahoo email address you have to create an \"App-Password\" in the account security screen.",
before_login_hint: "To use your Yahoo email address you have to create an \"App-Password\" in the account security screen.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/yahoo",
server: &[
@@ -2662,4 +2658,4 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
});
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2024, 9, 13).unwrap());
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2025, 9, 4).unwrap());

View File

@@ -74,7 +74,7 @@ fn pad_device_token(s: &str) -> String {
///
/// The result is base64-encoded and not ASCII armored to avoid dealing with newlines.
pub(crate) fn encrypt_device_token(device_token: &str) -> Result<String> {
let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?.0;
let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?;
let encryption_subkey = public_key
.public_subkeys
.first()

View File

@@ -808,19 +808,18 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
authcode,
..
} => {
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
token::delete(context, token::Namespace::Auth, &authcode).await?;
token::delete(context, "").await?;
context
.sync_qr_code_token_deletion(invitenumber, authcode)
.await?;
}
Qr::WithdrawVerifyGroup {
grpid,
invitenumber,
authcode,
..
} => {
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
token::delete(context, token::Namespace::Auth, &authcode).await?;
token::delete(context, &grpid).await?;
context
.sync_qr_code_token_deletion(invitenumber, authcode)
.await?;

View File

@@ -2,7 +2,7 @@ use super::*;
use crate::chat::{ProtectionStatus, create_group_chat};
use crate::config::Config;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{TestContext, TestContextManager};
use crate::test_utils::{TestContext, TestContextManager, sync};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_http() -> Result<()> {
@@ -509,6 +509,77 @@ async fn test_withdraw_verifygroup() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_withdraw_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice2 = &tcm.alice().await;
alice.set_config_bool(Config::SyncMsgs, true).await?;
alice2.set_config_bool(Config::SyncMsgs, true).await?;
// Alice creates two QR codes on the first device:
// group QR code and contact QR code.
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let chat2_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group 2").await?;
let contact_qr = get_securejoin_qr(alice, None).await?;
let group_qr = get_securejoin_qr(alice, Some(chat_id)).await?;
let group2_qr = get_securejoin_qr(alice, Some(chat2_id)).await?;
assert!(matches!(
check_qr(alice, &contact_qr).await?,
Qr::WithdrawVerifyContact { .. }
));
assert!(matches!(
check_qr(alice, &group_qr).await?,
Qr::WithdrawVerifyGroup { .. }
));
// Sync group QR codes.
sync(alice, alice2).await;
assert!(matches!(
check_qr(alice2, &group_qr).await?,
Qr::WithdrawVerifyGroup { .. }
));
assert!(matches!(
check_qr(alice2, &group2_qr).await?,
Qr::WithdrawVerifyGroup { .. }
));
// Alice creates a contact QR code on second device
// and withdraws it.
let contact_qr2 = get_securejoin_qr(alice2, None).await?;
set_config_from_qr(alice2, &contact_qr2).await?;
assert!(matches!(
check_qr(alice2, &contact_qr2).await?,
Qr::ReviveVerifyContact { .. }
));
// Alice also withdraws second group QR code on second device.
set_config_from_qr(alice2, &group2_qr).await?;
// Sync messages are sent from Alice's second device to first device.
sync(alice2, alice).await;
// Now first device has reset all contact QR codes
// and second group QR code,
// but first group QR code is still valid.
assert!(matches!(
check_qr(alice, &contact_qr2).await?,
Qr::ReviveVerifyContact { .. }
));
assert!(matches!(
check_qr(alice, &group_qr).await?,
Qr::WithdrawVerifyGroup { .. }
));
assert!(matches!(
check_qr(alice, &group2_qr).await?,
Qr::ReviveVerifyGroup { .. }
));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_and_apply_dclogin() -> Result<()> {
let ctx = TestContext::new().await;

View File

@@ -1,6 +1,6 @@
//! Internet Message Format reception pipeline.
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, HashSet};
use std::iter;
use std::sync::LazyLock;
@@ -27,7 +27,7 @@ use crate::ephemeral::{Timer as EphemeralTimer, stock_ephemeral_timer_changed};
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::key::{DcKey, Fingerprint};
use crate::key::{self_fingerprint, self_fingerprint_opt};
use crate::log::LogExt;
use crate::log::{info, warn};
@@ -35,7 +35,7 @@ use crate::logged_debug_assert;
use crate::message::{
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
};
use crate::mimeparser::{AvatarAction, MimeMessage, SystemMessage, parse_message_ids};
use crate::mimeparser::{AvatarAction, GossipedKey, MimeMessage, SystemMessage, parse_message_ids};
use crate::param::{Param, Params};
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
use crate::reaction::{Reaction, set_msg_reaction};
@@ -838,7 +838,7 @@ pub(crate) async fn receive_imf_inner(
context
.sql
.transaction(move |transaction| {
let fingerprint = gossiped_key.dc_fingerprint().hex();
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
transaction.execute(
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
VALUES (?, ?, ?)
@@ -1003,8 +1003,10 @@ pub(crate) async fn receive_imf_inner(
}
}
if mime_parser.is_system_message == SystemMessage::IncomingCall {
context.handle_call_msg(&mime_parser, insert_msg_id).await?;
if mime_parser.is_call() {
context
.handle_call_msg(insert_msg_id, &mime_parser, from_id)
.await?;
} else if received_msg.hidden {
// No need to emit an event about the changed message
} else if let Some(replace_chat_id) = replace_chat_id {
@@ -1156,6 +1158,11 @@ async fn decide_chat_assignment(
{
info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
true
} else if mime_parser.is_system_message == SystemMessage::CallAccepted
|| mime_parser.is_system_message == SystemMessage::CallEnded
{
info!(context, "Call state changed (TRASH).");
true
} else if mime_parser.decrypting_failed && !mime_parser.incoming {
// Outgoing undecryptable message.
let last_time = context
@@ -1998,7 +2005,9 @@ async fn add_parts(
if let Some(call) =
message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
{
context.handle_call_msg(mime_parser, call.get_id()).await?;
context
.handle_call_msg(call.get_id(), mime_parser, from_id)
.await?;
} else {
warn!(context, "Call: Cannot load parent.")
}
@@ -2931,7 +2940,7 @@ async fn apply_group_changes(
// just like we have ChatGroupMemberRemovedFpr.
// The result of the error is that info message
// may contain display name of the wrong contact.
let fingerprint = key.dc_fingerprint().hex();
let fingerprint = key.public_key.dc_fingerprint().hex();
if let Some(contact_id) =
lookup_key_contact_by_fingerprint(context, &fingerprint).await?
{
@@ -3750,10 +3759,28 @@ async fn mark_recipients_as_verified(
to_ids: &[Option<ContactId>],
mimeparser: &MimeMessage,
) -> Result<()> {
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
for gossiped_key in mimeparser
.gossiped_keys
.values()
.filter(|gossiped_key| gossiped_key.verified)
{
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
continue;
};
if to_id == ContactId::SELF || to_id == from_id {
continue;
}
mark_contact_id_as_verified(context, to_id, verifier_id).await?;
ChatId::set_protection_for_contact(context, to_id, mimeparser.timestamp_sent).await?;
}
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
return Ok(());
}
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
for to_id in to_ids.iter().filter_map(|&x| x) {
if to_id == ContactId::SELF || to_id == from_id {
continue;
@@ -3846,7 +3873,7 @@ async fn add_or_lookup_contacts_by_address_list(
async fn add_or_lookup_key_contacts(
context: &Context,
address_list: &[SingleInfo],
gossiped_keys: &HashMap<String, SignedPublicKey>,
gossiped_keys: &BTreeMap<String, GossipedKey>,
fingerprints: &[Fingerprint],
origin: Origin,
) -> Result<Vec<Option<ContactId>>> {
@@ -3862,7 +3889,7 @@ async fn add_or_lookup_key_contacts(
// Iterator has not ran out of fingerprints yet.
fp.hex()
} else if let Some(key) = gossiped_keys.get(addr) {
key.dc_fingerprint().hex()
key.public_key.dc_fingerprint().hex()
} else if context.is_self_addr(addr).await? {
contact_ids.push(Some(ContactId::SELF));
continue;

View File

@@ -308,7 +308,9 @@ pub(crate) async fn handle_securejoin_handshake(
let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
for (addr, key) in &mime_message.gossiped_keys {
if key.dc_fingerprint() == self_fingerprint && context.is_self_addr(addr).await? {
if key.public_key.dc_fingerprint() == self_fingerprint
&& context.is_self_addr(addr).await?
{
self_found = true;
break;
}
@@ -596,7 +598,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Ok(HandshakeMessage::Ignore);
};
if key.dc_fingerprint() != contact_fingerprint {
if key.public_key.dc_fingerprint() != contact_fingerprint {
// Fingerprint does not match, ignore.
warn!(context, "Fingerprint does not match.");
return Ok(HandshakeMessage::Ignore);

View File

@@ -5,6 +5,7 @@ use crate::chat::{CantSendReason, remove_contact_from_chat};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::key::self_fingerprint;
use crate::mimeparser::GossipedKey;
use crate::receive_imf::receive_imf;
use crate::stock_str::{self, messages_e2e_encrypted};
use crate::test_utils::{
@@ -186,7 +187,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
);
if case == SetupContactCase::WrongAliceGossip {
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
let wrong_pubkey = GossipedKey {
public_key: load_self_public_key(&bob).await.unwrap(),
verified: false,
};
let alice_pubkey = msg
.gossiped_keys
.insert(alice_addr.to_string(), wrong_pubkey)

View File

@@ -97,7 +97,7 @@ impl Summary {
let prefix = if msg.state == MessageState::OutDraft {
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
} else if msg.from_id == ContactId::SELF {
if msg.is_info() {
if msg.is_info() || msg.viewtype == Viewtype::Call {
None
} else {
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
@@ -233,6 +233,16 @@ impl Message {
type_file = self.param.get(Param::Summary1).map(|s| s.to_string());
append_text = true;
}
Viewtype::Call => {
emoji = Some("📞");
type_name = Some(if self.from_id == ContactId::SELF {
"Outgoing call".to_string()
} else {
"Incoming call".to_string()
});
type_file = None;
append_text = false
}
Viewtype::Text | Viewtype::Unknown => {
emoji = None;
if self.param.get_cmd() == SystemMessage::LocationOnly {

View File

@@ -71,9 +71,6 @@ pub(crate) enum SyncData {
DeleteMessages {
msgs: Vec<String>, // RFC724 id (i.e. "Message-Id" header)
},
RejectIncomingCall {
msg: String, // RFC724 id (i.e. "Message-Id" header)
},
}
#[derive(Debug, Serialize, Deserialize)]
@@ -267,7 +264,6 @@ impl Context {
SyncData::Config { key, val } => self.sync_config(key, val).await,
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
SyncData::RejectIncomingCall { msg } => self.sync_call_rejection(msg).await,
},
SyncDataOrUnknown::Unknown(data) => {
warn!(self, "Ignored unknown sync item: {data}.");
@@ -297,8 +293,15 @@ impl Context {
}
async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> {
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
token::delete(self, Namespace::Auth, &token.auth).await?;
self.sql
.execute(
"DELETE FROM tokens
WHERE foreign_key IN
(SELECT foreign_key FROM tokens
WHERE token=? OR token=?)",
(&token.invitenumber, &token.auth),
)
.await?;
Ok(())
}
@@ -569,8 +572,8 @@ mod tests {
.await?
.is_none()
);
assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await?);
assert!(token::exists(&t, Namespace::Auth, "yip-auth").await?);
assert!(!token::exists(&t, Namespace::InviteNumber, "yip-in").await?);
assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await?);
assert!(!token::exists(&t, Namespace::Auth, "non-existent").await?);
assert!(!token::exists(&t, Namespace::Auth, "directly deleted").await?);

View File

@@ -1282,9 +1282,8 @@ impl SentMessage<'_> {
///
/// The keypair was created using the crate::key::tests::gen_key test.
pub fn alice_keypair() -> KeyPair {
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc"))
.unwrap()
.0;
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc")).unwrap();
let public = secret.split_public_key().unwrap();
KeyPair { public, secret }
}
@@ -1293,9 +1292,8 @@ pub fn alice_keypair() -> KeyPair {
///
/// Like [alice_keypair] but a different key and identity.
pub fn bob_keypair() -> KeyPair {
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc"))
.unwrap()
.0;
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
let public = secret.split_public_key().unwrap();
KeyPair { public, secret }
}
@@ -1306,8 +1304,7 @@ pub fn bob_keypair() -> KeyPair {
pub fn charlie_keypair() -> KeyPair {
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/charlie-secret.asc"))
.unwrap()
.0;
.unwrap();
let public = secret.split_public_key().unwrap();
KeyPair { public, secret }
}
@@ -1316,9 +1313,8 @@ pub fn charlie_keypair() -> KeyPair {
///
/// Like [alice_keypair] but a different key and identity.
pub fn dom_keypair() -> KeyPair {
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/dom-secret.asc"))
.unwrap()
.0;
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/dom-secret.asc")).unwrap();
let public = secret.split_public_key().unwrap();
KeyPair { public, secret }
}
@@ -1327,9 +1323,8 @@ pub fn dom_keypair() -> KeyPair {
///
/// Like [alice_keypair] but a different key and identity.
pub fn elena_keypair() -> KeyPair {
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/elena-secret.asc"))
.unwrap()
.0;
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/elena-secret.asc")).unwrap();
let public = secret.split_public_key().unwrap();
KeyPair { public, secret }
}
@@ -1338,9 +1333,8 @@ pub fn elena_keypair() -> KeyPair {
///
/// Like [alice_keypair] but a different key and identity.
pub fn fiona_keypair() -> KeyPair {
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc"))
.unwrap()
.0;
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap();
let public = secret.split_public_key().unwrap();
KeyPair { public, secret }
}
@@ -1476,8 +1470,7 @@ pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) {
alice0.send_sync_msg().await.unwrap();
let sync_msg = alice0.pop_sent_sync_msg().await;
let no_msg = alice1.recv_msg_opt(&sync_msg).await;
assert!(no_msg.is_none());
alice1.recv_msg_trash(&sync_msg).await;
}
/// Pretty-print an event to stdout

View File

@@ -119,13 +119,14 @@ pub async fn auth_foreign_key(context: &Context, token: &str) -> Result<Option<S
.await
}
pub async fn delete(context: &Context, namespace: Namespace, token: &str) -> Result<()> {
/// Resets all tokens corresponding to the `foreign_key`.
///
/// `foreign_key` is a group ID to reset all group tokens
/// or empty string to reset all setup contact tokens.
pub async fn delete(context: &Context, foreign_key: &str) -> Result<()> {
context
.sql
.execute(
"DELETE FROM tokens WHERE namespc=? AND token=?;",
(namespace, token),
)
.execute("DELETE FROM tokens WHERE foreign_key=?", (foreign_key,))
.await?;
Ok(())
}