Compare commits

...

25 Commits

Author SHA1 Message Date
WofWca
781de46e26 WIP: api!: add LIMIT arg to get_chat_media
In Delta Chat desktop we show the 3 recently used WebXDC apps,
which relies on `get_chat_media`, which is quite expensive.
Hopefully adding `LIMIT 3` makes it faster.

Marking this as a breaking change
because it's breaking TypeScript-wise,
but shouldn't be breaking behavior-wise,
because not providing the argument in JSON-RPC
should be equivalent to providing `null`
(which gets converted to `None`).

TODO:
- [ ] Add to CFFI?
- [ ] Docs. Both the core fn and the JSON-RPC.
2025-10-20 19:18:28 +04:00
iequidoo
fc81cef113 refactor: Rename chat::create_group_chat() to create_group()
If we use modules (which are actually namespaces), we can use shorter names. Another approach is to
only use modules for internal code incapsulation and use full names like deltachat-ffi does.
2025-10-20 04:19:22 -03:00
iequidoo
04c2585c27 feat: Synchronize encrypted groups creation across devices (#7001)
Unencrypted groups don't have grpid since key-contacts were merged, so we don't sync them for now.
2025-10-20 04:19:22 -03:00
Simon Laux
59fac54f7b test: Add unique offsets to ids generated by TestContext to increase test correctness (#7297)
and fix the mistakes in tests that get discovered by this.

closes #6799
2025-10-19 17:08:23 +00:00
Simon Laux
65b61efb31 build: ignore configuration for the zed editor (#7322) 2025-10-19 17:03:39 +00:00
link2xt
afc74b0829 fix: do not allow sync item timestamps to be in the future 2025-10-19 11:35:10 +00:00
link2xt
2481a0f48e feat: reset all indirect verifications 2025-10-19 11:35:09 +00:00
link2xt
6c24edb40d feat: do not mark Bob as verified if auth token is old 2025-10-19 11:35:09 +00:00
link2xt
e4178789da refactor: remove ProtectionStatus 2025-10-19 11:35:09 +00:00
link2xt
b417ba86bc api!: remove Chat.is_protected() 2025-10-19 11:35:09 +00:00
link2xt
498a831873 api!: remove APIs to create protected chats
Create unprotected group in test_create_protected_grp_multidev
The test is renamed accordingly.

SystemMessage::ChatE2ee is added in encrypted groups
regardless of whether they are protected or not.
Previously new encrypted unprotected groups
had no message saying that messages are end-to-end encrypted
at all.
2025-10-19 11:35:09 +00:00
link2xt
c6722d36de api!: remove public APIs to check if the chat is protected 2025-10-19 11:35:09 +00:00
link2xt
90f0d5c060 api: make dc_chat_is_protected always return 0 2025-10-19 11:35:09 +00:00
link2xt
90ec2f2518 docs: document Autocrypt-Gossip _verified attribute 2025-10-19 11:35:09 +00:00
link2xt
5b66535134 feat: verify contacts via Autocrypt-Gossip
This mechanism replaces `Chat-Verified` header.
New parameter `_verified=1` in `Autocrypt-Gossip`
header marks that the sender has the gossiped key
verified.

Using `_verified=1` instead of `_verified`
because it is less likely to cause troubles
with existing Autocrypt header parsers.
This is also how https://www.rfc-editor.org/rfc/rfc2045
defines parameter syntax.
2025-10-19 11:35:09 +00:00
link2xt
eea848f72b fix: don't ignore QR token timestamp from sync messages 2025-10-19 11:35:09 +00:00
link2xt
214a1d3e2d feat: do not resolve MX records during configuration
MX record lookup was only used to detect Google Workspace domains.
They can still be configured manually.
We anyway do not want to encourage creating new profiles
with Google Workspace as we don't have Gmail OAUTH2 token anymore
and new users can more easily onboard with a chatmail relay.
2025-10-18 18:09:56 +00:00
link2xt
e270a502d1 refactor: remove invalid Gmail OAuth2 tokens
They were already unused since
<https://github.com/chatmail/provider-db/pull/310>
2025-10-18 18:09:56 +00:00
iequidoo
b863345600 test(rpc-client): vCard color is the same as the contact color (#7294)
This tests "fix(jsonrpc): Use Core's logic for computing VcardContact.color".
2025-10-17 14:46:57 -03:00
link2xt
61b49a9339 Merge tag 'v2.22.0'
Release 2.22.0
2025-10-17 10:53:12 +00:00
l
6fd3645360 Merge pull request #7320 from chatmail/link2xt/dont-notify-contact-request-calls 2025-10-17 10:45:41 +00:00
link2xt
5256013615 feat: protect Autocrypt header 2025-10-16 23:34:44 +00:00
link2xt
9826c28581 feat: anonymize OpenPGP recipients 2025-10-16 23:34:03 +00:00
link2xt
9ceceebdc3 ci: set 7 days cooldown on Dependabot updates
This fixes the warning
<https://docs.zizmor.sh/audits/#dependabot-cooldown>
and avoids updating to freshly published dependencies
that are more likely to be unpublished.
2025-10-16 20:50:24 +00:00
link2xt
187d913f84 ci: pin GitHub action astral-sh/setup-uv
Pinned version corresponds to the current v7.1.0 tag.
2025-10-16 20:50:03 +00:00
69 changed files with 1186 additions and 1747 deletions

View File

@@ -7,6 +7,8 @@ updates:
commit-message:
prefix: "chore(cargo)"
open-pull-requests-limit: 50
cooldown:
default-days: 7
# Keep GitHub Actions up to date.
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
@@ -14,3 +16,5 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7

View File

@@ -19,7 +19,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d
- name: Run zizmor
run: uvx zizmor --format sarif . > results.sarif

1
.gitignore vendored
View File

@@ -36,6 +36,7 @@ deltachat-ffi/xml
coverage/
.DS_Store
.vscode
.zed
python/accounts.txt
python/all-testaccounts.txt
tmp/

1
Cargo.lock generated
View File

@@ -1316,7 +1316,6 @@ dependencies = [
"futures",
"futures-lite",
"hex",
"hickory-resolver",
"http-body-util",
"humansize",
"hyper",

View File

@@ -61,7 +61,6 @@ fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
hex = "0.4.0"
hickory-resolver = "0.25.2"
http-body-util = "0.1.3"
humansize = "2"
hyper = "1"

View File

@@ -1763,9 +1763,7 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
*
* @memberof dc_context_t
* @param context The context object.
* @param protect If set to 1 the function creates group with protection initially enabled.
* Only verified members are allowed in these groups
* and end-to-end-encryption is always enabled.
* @param protect Deprecated 2025-08-31, ignored.
* @param name The name of the group chat to create.
* The name may be changed later using dc_set_chat_name().
* To find out the name of a group later, see dc_chat_get_name()
@@ -3890,18 +3888,12 @@ int dc_chat_can_send (const dc_chat_t* chat);
/**
* Check if a chat is protected.
*
* Only verified contacts
* as determined by dc_contact_is_verified()
* can be added to protected chats.
*
* Protected chats are created using dc_create_group_chat()
* by setting the 'protect' parameter to 1.
* Deprecated, always returns 0.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protected, 0=chat is not protected.
* @return Always 0.
* @deprecated 2025-09-09
*/
int dc_chat_is_protected (const dc_chat_t* chat);
@@ -5350,11 +5342,9 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
/**
* Create a provider struct for the given e-mail address by local and DNS lookup.
* Create a provider struct for the given e-mail address by local lookup.
*
* First lookup is done from the local database as of dc_provider_new_from_email().
* If the first lookup fails, an additional DNS lookup is done,
* trying to figure out the provider belonging to custom domains.
* DNS lookup is not used anymore and this function is deprecated.
*
* @memberof dc_provider_t
* @param context The context object.
@@ -5362,6 +5352,7 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
* @deprecated 2025-10-17 use dc_provider_new_from_email() instead.
*/
dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email);

View File

@@ -22,7 +22,7 @@ use std::sync::{Arc, LazyLock};
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::{Context, ContextBuilder};
@@ -1532,6 +1532,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
msg_type: libc::c_int,
or_msg_type2: libc::c_int,
or_msg_type3: libc::c_int,
// limit: u32,
) -> *mut dc_array::dc_array_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_chat_media()");
@@ -1551,7 +1552,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
block_on(async move {
Box::into_raw(Box::new(
chat::get_chat_media(ctx, chat_id, msg_type, or_msg_type2, or_msg_type3)
chat::get_chat_media(ctx, chat_id, msg_type, or_msg_type2, or_msg_type3, None)
.await
.unwrap_or_log_default(ctx, "Failed get_chat_media")
.into(),
@@ -1721,7 +1722,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
#[no_mangle]
pub unsafe extern "C" fn dc_create_group_chat(
context: *mut dc_context_t,
protect: libc::c_int,
_protect: libc::c_int,
name: *const libc::c_char,
) -> u32 {
if context.is_null() || name.is_null() {
@@ -1729,22 +1730,12 @@ pub unsafe extern "C" fn dc_create_group_chat(
return 0;
}
let ctx = &*context;
let Some(protect) = ProtectionStatus::from_i32(protect)
.context("Bad protect-value for dc_create_group_chat()")
.log_err(ctx)
.ok()
else {
return 0;
};
block_on(async move {
chat::create_group_chat(ctx, protect, &to_string_lossy(name))
.await
.context("Failed to create group chat")
.log_err(ctx)
.map(|id| id.to_u32())
.unwrap_or(0)
})
block_on(chat::create_group(ctx, &to_string_lossy(name)))
.context("Failed to create group chat")
.log_err(ctx)
.map(|id| id.to_u32())
.unwrap_or(0)
}
#[no_mangle]
@@ -3206,13 +3197,8 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protected()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protected() as libc::c_int
pub extern "C" fn dc_chat_is_protected(_chat: *mut dc_chat_t) -> libc::c_int {
0
}
#[no_mangle]
@@ -4661,13 +4647,9 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
let ctx = &*context;
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
true,
))
.log_err(ctx)
.unwrap_or_default()
match provider::get_provider_info_by_addr(addr.as_str())
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
@@ -4686,25 +4668,13 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
let addr = to_string_lossy(addr);
let ctx = &*context;
let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled))
.context("Can't get config")
.log_err(ctx);
match proxy_enabled {
Ok(proxy_enabled) => {
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
proxy_enabled,
))
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
}
Err(_) => ptr::null_mut(),
match provider::get_provider_info_by_addr(addr.as_str())
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
}

View File

@@ -12,7 +12,6 @@ use deltachat::calls::ice_servers;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::Config;
@@ -336,21 +335,10 @@ impl CommandApi {
/// instead of the domain.
async fn get_provider_info(
&self,
account_id: u32,
_account_id: u32,
email: String,
) -> Result<Option<ProviderInfo>> {
let ctx = self.get_context(account_id).await?;
let proxy_enabled = ctx
.get_config_bool(deltachat::config::Config::ProxyEnabled)
.await?;
let provider_info = get_provider_info(
&ctx,
email.split('@').next_back().unwrap_or(""),
proxy_enabled,
)
.await;
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
Ok(ProviderInfo::from_dc_type(provider_info))
}
@@ -978,18 +966,9 @@ impl CommandApi {
///
/// To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
/// This may be useful if you want to show some help for just created groups.
///
/// @param protect If set to 1 the function creates group with protection initially enabled.
/// Only verified members are allowed in these groups
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
async fn create_group_chat(&self, account_id: u32, name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let protect = match protect {
true => ProtectionStatus::Protected,
false => ProtectionStatus::Unprotected,
};
chat::create_group_ex(&ctx, Some(protect), &name)
.await
.map(|id| id.to_u32())
chat::create_group(&ctx, &name).await.map(|id| id.to_u32())
}
/// Create a new unencrypted group chat.
@@ -998,7 +977,7 @@ impl CommandApi {
/// address-contacts.
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_group_ex(&ctx, None, &name)
chat::create_group_unencrypted(&ctx, &name)
.await
.map(|id| id.to_u32())
}
@@ -1755,6 +1734,7 @@ impl CommandApi {
message_type: MessageViewtype,
or_message_type2: Option<MessageViewtype>,
or_message_type3: Option<MessageViewtype>,
limit: Option<u32>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
@@ -1766,7 +1746,8 @@ impl CommandApi {
let or_msg_type2 = or_message_type2.map_or(Viewtype::Unknown, |v| v.into());
let or_msg_type3 = or_message_type3.map_or(Viewtype::Unknown, |v| v.into());
let media = get_chat_media(&ctx, chat_id, msg_type, or_msg_type2, or_msg_type3).await?;
let media =
get_chat_media(&ctx, chat_id, msg_type, or_msg_type2, or_msg_type3, limit).await?;
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
}

View File

@@ -19,18 +19,6 @@ pub struct FullChat {
id: u32,
name: String,
/// True if the chat is protected.
///
/// Only verified contacts
/// as determined by [`ContactObject::is_verified`] / `Contact.isVerified`
/// can be added to protected chats.
///
/// Protected chats are created using [`create_group_chat`] / `createGroupChat()`
/// by setting the 'protect' parameter to true.
///
/// [`create_group_chat`]: crate::api::CommandApi::create_group_chat
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
@@ -131,7 +119,6 @@ impl FullChat {
Ok(FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
@@ -172,18 +159,6 @@ pub struct BasicChat {
id: u32,
name: String,
/// True if the chat is protected.
///
/// UI should display a green checkmark
/// in the chat title,
/// in the chat profile title and
/// in the chatlist item
/// if chat protection is enabled.
/// UI should also display a green checkmark
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
@@ -234,7 +209,6 @@ impl BasicChat {
Ok(BasicChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,

View File

@@ -30,7 +30,6 @@ pub enum ChatListItemFetchResult {
summary_status: u32,
/// showing preview if last chat message is image
summary_preview_image: Option<String>,
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
@@ -161,7 +160,6 @@ pub(crate) async fn get_chat_list_item_by_id(
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
summary_preview_image,
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(ctx).await?,
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,

View File

@@ -532,7 +532,6 @@ pub struct MessageSearchResult {
chat_color: String,
chat_name: String,
chat_type: u32,
is_chat_protected: bool,
is_chat_contact_request: bool,
is_chat_archived: bool,
message: String,
@@ -572,7 +571,6 @@ impl MessageSearchResult {
chat_color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
chat_profile_image,
is_chat_protected: chat.is_protected(),
is_chat_contact_request: chat.is_contact_request(),
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
message: message.get_text(),

View File

@@ -6,9 +6,7 @@ use std::str::FromStr;
use std::time::Duration;
use anyhow::{bail, ensure, Result};
use deltachat::chat::{
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
};
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
@@ -347,7 +345,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
createchat <contact-id>\n\
creategroup <name>\n\
createbroadcast <name>\n\
createprotected <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
groupname <name>\n\
@@ -563,7 +560,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
println!(
"{}#{}: {} [{} fresh] {}{}{}{}",
"{}#{}: {} [{} fresh] {}{}{}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
@@ -574,7 +571,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
if chat.is_protected() { "🛡️" } else { "" },
if chat.is_contact_request() {
"🆕"
} else {
@@ -689,7 +685,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
format!("{} member(s)", members.len())
};
println!(
"{}#{}: {} [{}]{}{}{} {}",
"{}#{}: {} [{}]{}{}{}",
chat_prefix(sel_chat),
sel_chat.get_id(),
sel_chat.get_name(),
@@ -707,11 +703,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
_ => "".to_string(),
},
if sel_chat.is_protected() {
"🛡️"
} else {
""
},
);
log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
@@ -740,8 +731,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
let chat_id = chat::create_group(&context, arg1).await?;
println!("Group#{chat_id} created successfully.");
}
@@ -751,13 +741,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Broadcast#{chat_id} created successfully.");
}
"createprotected" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
println!("Group#{chat_id} created and protected successfully.");
}
"addmember" => {
ensure!(sel_chat.is_some(), "No chat selected");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
@@ -1033,6 +1016,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Viewtype::Image,
Viewtype::Gif,
Viewtype::Video,
None,
)
.await?;
println!("{} images or videos: ", images.len());
@@ -1266,10 +1250,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
let proxy_enabled = context
.get_config_bool(config::Config::ProxyEnabled)
.await?;
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {arg1}:");
println!("status: {}", info.status as u32);

View File

@@ -300,7 +300,7 @@ class Account:
chats.append(AttrDict(item))
return chats
def create_group(self, name: str, protect: bool = False) -> Chat:
def create_group(self, name: str) -> Chat:
"""Create a new group chat.
After creation,
@@ -317,12 +317,8 @@ class Account:
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
(see `get_full_snapshot()` / `get_basic_snapshot()`).
This may be useful if you want to show some help for just created groups.
:param protect: If set to 1 the function creates group with protection initially enabled.
Only verified members are allowed in these groups
and end-to-end-encryption is always enabled.
"""
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
return Chat(self, self._rpc.create_group_chat(self.id, name))
def create_broadcast(self, name: str) -> Chat:
"""Create a new **broadcast channel**

View File

@@ -58,8 +58,7 @@ def test_qr_setup_contact_svg(acfactory) -> None:
assert "Alice" in svg
@pytest.mark.parametrize("protect", [True, False])
def test_qr_securejoin(acfactory, protect):
def test_qr_securejoin(acfactory):
alice, bob, fiona = acfactory.get_online_accounts(3)
# Setup second device for Alice
@@ -67,8 +66,7 @@ def test_qr_securejoin(acfactory, protect):
alice2 = alice.clone()
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group", protect=protect)
assert alice_chat.get_basic_snapshot().is_protected == protect
alice_chat = alice.create_group("Group")
logging.info("Bob joins the group")
qr_code = alice_chat.get_qr_code()
@@ -89,7 +87,6 @@ def test_qr_securejoin(acfactory, protect):
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected == protect
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice)
@@ -125,8 +122,8 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
bob_chat_alice = snapshot.chat
assert bob_chat_alice.get_basic_snapshot().is_contact_request
alice_chat = alice.create_group("Verified group", protect=True)
logging.info("Bob joins verified group")
alice_chat = alice.create_group("Group")
logging.info("Bob joins the group")
qr_code = alice_chat.get_qr_code()
bob.secure_join(qr_code)
while True:
@@ -150,8 +147,8 @@ def test_qr_readreceipt(acfactory) -> None:
for joiner in [bob, charlie]:
joiner.wait_for_securejoin_joiner_success()
logging.info("Alice creates a verified group")
group = alice.create_group("Group", protect=True)
logging.info("Alice creates a group")
group = alice.create_group("Group")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
@@ -216,11 +213,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying then removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac1 creates a group")
chat = ac1.create_group("Group")
logging.info("ac2 joins verified group")
logging.info("ac2 joins the group")
qr_code = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
@@ -302,8 +298,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
# we first create a fully joined verified group, and then start
# joining a second time but interrupt it, to create pending bob state
logging.info("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group("Group", protect=True)
logging.info("ac1: create a group that ac2 fully joins")
ch1 = ac1.create_group("Group")
qr_code = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
@@ -313,7 +309,6 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
while 1:
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if snapshot.text == "ac1 says hello":
assert snapshot.chat.get_basic_snapshot().is_protected
break
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
@@ -327,7 +322,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
assert ac2.create_contact(ac3).get_snapshot().is_verified
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created", protect=True)
vg = ac3.create_group("ac3-created")
vg.add_contact(ac3.create_contact(ac2))
# ensure ac2 receives message in VG
@@ -335,7 +330,6 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
while 1:
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if msg.text == "hello":
assert msg.chat.get_basic_snapshot().is_protected
break
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
@@ -359,7 +353,7 @@ def test_qr_new_group_unblocked(acfactory):
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining", protect=True)
ac1_chat = ac1.create_group("Group for joining")
qr_code = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
@@ -384,8 +378,7 @@ def test_aeap_flow_verified(acfactory):
addr, password = acfactory.get_credentials()
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
assert chat.get_basic_snapshot().is_protected
chat = ac1.create_group("hello")
qr_code = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
ac2.secure_join(qr_code)
@@ -439,7 +432,6 @@ def test_gossip_verification(acfactory) -> None:
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
assert not bob_group_chat.get_basic_snapshot().is_protected
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Autocrypt group")
@@ -448,13 +440,12 @@ def test_gossip_verification(acfactory) -> None:
assert snapshot.text == "Hello Autocrypt group"
assert snapshot.show_padlock
# Autocrypt group does not propagate verification.
# Group propagates verification using Autocrypt-Gossip header.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
assert not carol_contact_alice_snapshot.is_verified
assert carol_contact_alice_snapshot.is_verified
logging.info("Bob creates a Securejoin group")
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
assert bob_group_chat.get_basic_snapshot().is_protected
bob_group_chat = bob.create_group("Securejoin Group")
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Securejoin group")
@@ -477,7 +468,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
# ac3 creates protected group with ac1.
ac3_chat = ac3.create_group("Verified group", protect=True)
ac3_chat = ac3.create_group("Group")
# ac1 joins ac3 group.
ac3_qr_code = ac3_chat.get_qr_code()
@@ -525,7 +516,6 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.is_info
ac2_chat = snapshot.chat
assert ac2_chat.get_basic_snapshot().is_protected
assert len(ac2_chat.get_contacts()) == 3
# ac1 is still "not verified" for ac2 due to inconsistent state.
@@ -535,9 +525,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
def test_withdraw_securejoin_qr(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Alice creates a group")
alice_chat = alice.create_group("Group")
logging.info("Bob joins verified group")
qr_code = alice_chat.get_qr_code()
@@ -548,7 +537,6 @@ def test_withdraw_securejoin_qr(acfactory):
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
bob_chat.leave()
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()

View File

@@ -570,8 +570,11 @@ def test_provider_info(rpc) -> None:
assert provider_info is None
# Test MX record resolution.
# This previously resulted in Gmail provider
# because MX record pointed to google.com domain,
# but MX record resolution has been removed.
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info["id"] == "gmail"
assert provider_info is None
# Disable MX record resolution.
rpc.set_config(account_id, "proxy_enabled", "1")

View File

@@ -1,8 +1,11 @@
def test_vcard(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice, bob, fiona = acfactory.get_online_accounts(3)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
alice_contact_charlie_snapshot = alice_contact_charlie.get_snapshot()
alice_contact_fiona = alice.create_contact(fiona, "Fiona")
alice_contact_fiona_snapshot = alice_contact_fiona.get_snapshot()
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_contact(alice_contact_charlie)
@@ -12,3 +15,12 @@ def test_vcard(acfactory) -> None:
snapshot = message.get_snapshot()
assert snapshot.vcard_contact
assert snapshot.vcard_contact.addr == "charlie@example.org"
assert snapshot.vcard_contact.color == alice_contact_charlie_snapshot.color
alice_chat_bob.send_contact(alice_contact_fiona)
event = bob.wait_for_incoming_msg_event()
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.vcard_contact
assert snapshot.vcard_contact.key
assert snapshot.vcard_contact.color == alice_contact_fiona_snapshot.color

View File

@@ -404,18 +404,16 @@ class Account:
self,
name: str,
contacts: Optional[List[Contact]] = None,
verified: bool = False,
) -> Chat:
"""create a new group chat object.
Chats are unpromoted until the first message is sent.
:param contacts: list of contacts to add
:param verified: if true only verified contacts can be added.
:returns: a :class:`deltachat.chat.Chat` object.
"""
bytes_name = name.encode("utf8")
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
chat_id = lib.dc_create_group_chat(self._dc_context, 0, bytes_name)
chat = Chat(self, chat_id)
if contacts is not None:
for contact in contacts:

View File

@@ -142,13 +142,6 @@ class Chat:
"""
return bool(lib.dc_chat_can_send(self._dc_chat))
def is_protected(self) -> bool:
"""return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise.
"""
return bool(lib.dc_chat_is_protected(self._dc_chat))
def get_name(self) -> Optional[str]:
"""return name of this chat.

View File

@@ -604,20 +604,6 @@ class ACFactory:
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def get_protected_chat(self, ac1: Account, ac2: Account):
chat = ac1.create_group_chat("Protected Group", verified=True)
qr = chat.get_join_qr()
ac2.qr_join_chat(qr)
ac2._evtracker.wait_securejoin_joiner_progress(1000)
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg is not None
assert msg.text == "Messages are end-to-end encrypted."
msg = ac2._evtracker.wait_next_incoming_message()
assert msg is not None
assert "Member Me " in msg.text and " added by " in msg.text
return chat
def introduce_each_other(self, accounts, sending=True):
to_wait = []
for i, acc in enumerate(accounts):

View File

@@ -118,8 +118,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
ac1_addr = ac1.get_self_contact().addr
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
chat1 = ac1.create_group_chat("hello")
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -142,7 +141,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: read message and check that it's a verified chat")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert msg.chat.is_protected()
assert msg.is_encrypted()
lp.sec("ac2: Check that ac2 verified ac1")
@@ -173,8 +171,10 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
ac2_ac3_contact = ac2.get_contacts()[1]
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
for ac2_contact in chat2.get_contacts():
if ac2_contact == ac2_ac1_contact or ac2_contact.id == dc.const.DC_CONTACT_ID_SELF:
continue
assert ac2.get_self_contact().get_verifier(ac2_contact).addr == ac1_addr
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")
@@ -266,8 +266,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
ac1_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -321,8 +320,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
ac1.set_avatar(avatar_path)
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
ac2.qr_join_chat(qr)
@@ -336,7 +334,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
assert msg_in.is_system_message()
assert contact.addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
@@ -376,8 +373,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
ac2_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
chat1 = ac1.create_group_chat("hello")
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -402,28 +398,17 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
assert not ac2_offl_ac1_contact.is_verified()
chat2_offl = msg_in.chat
assert not chat2_offl.is_protected()
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
chat2.send_text("hi2")
lp.sec("ac2_offl: receiving message")
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert msg_in.is_system_message()
assert msg_in.text == "Messages are end-to-end encrypted."
# We need to consume one event that has data2=0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
assert ev.data2 == 0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert not msg_in.is_system_message()
assert msg_in.text == "hi2"
assert msg_in.chat == chat2_offl
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert msg_in.chat.is_protected()
assert ac2_offl_ac1_contact.is_verified()

View File

@@ -1204,7 +1204,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group
that resulted in failure to propagate verification
when the database already contained the contact with a different email address capitalization.
"""
@@ -1215,17 +1215,17 @@ def test_qr_email_capitalization(acfactory, lp):
lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})")
ac1.create_contact(ac2_addr_uppercase)
lp.sec("ac3 creates a verified group with a QR code")
chat = ac3.create_group_chat("hello", verified=True)
lp.sec("ac3 creates a group with a QR code")
chat = ac3.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac1 joins a verified group via a QR code")
lp.sec("ac1 joins a group via a QR code")
ac1_chat = ac1.qr_join_chat(qr)
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr"))
assert len(ac1_chat.get_contacts()) == 2
lp.sec("ac2 joins a verified group via a QR code")
lp.sec("ac2 joins a group via a QR code")
ac2.qr_join_chat(qr)
ac1._evtracker.wait_next_incoming_message()

View File

@@ -271,10 +271,9 @@ class TestOfflineChat:
chat.set_name("Homework")
assert chat.get_messages()[-1].text == "abc homework xyz Homework"
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
def test_group_chat_qr(self, acfactory, ac1):
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_group_chat(name="title1", verified=verified)
chat = ac1.create_group_chat(name="title1")
assert chat.is_group()
qr = chat.get_join_qr()
assert ac2.check_qr(qr).is_ask_verifygroup

20
spec.md
View File

@@ -1,6 +1,6 @@
# Chatmail Specification
Version: 0.36.0
Version: 0.37.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
@@ -582,6 +582,24 @@ and e.g. simply search for the line starting with `EMAIL`
in order to get the email address.
# Verifications
Keys obtained using [SecureJoin](https://securejoin.readthedocs.io) protocol
and corresponding contacts
are considered "verified".
As an extension to `Autocrypt-Gossip` header,
chatmail clients can add `_verified=1` attribute
(underscore marks the attribute as non-critical)
to indicate that they have the gossiped key
and the corresponding contact marked as verified.
When receiving such `Autocrypt-Gossip` header
in a message signed by a verified key,
chatmail clients mark the gossiped key
as indirectly verified.
# Transitioning to a new e-mail address (AEAP)
When receiving a message:

View File

@@ -12,7 +12,6 @@ use std::time::Duration;
use anyhow::{Context as _, Result, anyhow, bail, ensure};
use chrono::TimeZone;
use deltachat_contact_tools::{ContactAddress, sanitize_bidi_characters, sanitize_single_line};
use deltachat_derive::{FromSql, ToSql};
use mail_builder::mime::MimePart;
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
@@ -31,6 +30,7 @@ use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
use crate::events::EventType;
use crate::key::self_fingerprint;
use crate::location;
use crate::log::{LogExt, error, info, warn};
use crate::logged_debug_assert;
@@ -67,41 +67,6 @@ pub enum ChatItem {
},
}
/// Chat protection status.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum ProtectionStatus {
/// Chat is not protected.
#[default]
Unprotected = 0,
/// Chat is protected.
///
/// All members of the chat must be verified.
Protected = 1,
// `2` was never used as a value.
// Chats don't break in Core v2 anymore. Chats with broken protection existing before the
// key-contacts migration are treated as `Unprotected`.
//
// ProtectionBroken = 3,
}
/// The reason why messages cannot be sent to the chat.
///
/// The reason is mainly for logging and displaying in debug REPL, thus not translated.
@@ -306,14 +271,12 @@ impl ChatId {
/// Create a group or mailinglist raw database record with the given parameters.
/// The function does not add SELF nor checks if the record already exists.
#[expect(clippy::too_many_arguments)]
pub(crate) async fn create_multiuser_record(
context: &Context,
chattype: Chattype,
grpid: &str,
grpname: &str,
create_blocked: Blocked,
create_protected: ProtectionStatus,
param: Option<String>,
timestamp: i64,
) -> Result<Self> {
@@ -321,31 +284,27 @@ impl ChatId {
let timestamp = cmp::min(timestamp, smeared_time(context));
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, 0, ?);",
(
chattype,
&grpname,
grpid,
create_blocked,
timestamp,
create_protected,
param.unwrap_or_default(),
),
).await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
let chat = Chat::load_from_db(context, chat_id).await?;
if create_protected == ProtectionStatus::Protected {
chat_id
.add_protection_msg(context, ProtectionStatus::Protected, None, timestamp)
.await?;
} else {
chat_id.maybe_add_encrypted_msg(context, timestamp).await?;
if chat.is_encrypted(context).await? {
chat_id.add_encrypted_msg(context, timestamp).await?;
}
info!(
context,
"Created group/mailinglist '{}' grpid={} as {}, blocked={}, protected={create_protected}.",
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
&grpname,
grpid,
chat_id,
@@ -500,111 +459,8 @@ impl ChatId {
Ok(())
}
/// Sets protection without sending a message.
///
/// Returns whether the protection status was actually modified.
pub(crate) async fn inner_set_protection(
self,
context: &Context,
protect: ProtectionStatus,
) -> Result<bool> {
ensure!(!self.is_special(), "Invalid chat-id {self}.");
let chat = Chat::load_from_db(context, self).await?;
if protect == chat.protected {
info!(context, "Protection status unchanged for {}.", self);
return Ok(false);
}
match protect {
ProtectionStatus::Protected => match chat.typ {
Chattype::Single
| Chattype::Group
| Chattype::OutBroadcast
| Chattype::InBroadcast => {}
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
},
ProtectionStatus::Unprotected => {}
};
context
.sql
.execute("UPDATE chats SET protected=? WHERE id=?;", (protect, self))
.await?;
context.emit_event(EventType::ChatModified(self));
chatlist_events::emit_chatlist_item_changed(context, self);
// make sure, the receivers will get all keys
self.reset_gossiped_timestamp(context).await?;
Ok(true)
}
/// Adds an info message to the chat, telling the user that the protection status changed.
///
/// Params:
///
/// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id.
/// * `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
pub(crate) async fn add_protection_msg(
self,
context: &Context,
protect: ProtectionStatus,
contact_id: Option<ContactId>,
timestamp_sort: i64,
) -> Result<()> {
if contact_id == Some(ContactId::SELF) {
// Do not add protection messages to Saved Messages chat.
// This chat never gets protected and unprotected,
// we do not want the first message
// to be a protection message with an arbitrary timestamp.
return Ok(());
}
let text = context.stock_protection_msg(protect, contact_id).await;
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
};
add_info_msg_with_cmd(
context,
self,
&text,
cmd,
timestamp_sort,
None,
None,
None,
None,
)
.await?;
Ok(())
}
/// Adds message "Messages are end-to-end encrypted" if appropriate.
///
/// This function is rather slow because it does a lot of database queries,
/// but this is fine because it is only called on chat creation.
async fn maybe_add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> {
let chat = Chat::load_from_db(context, self).await?;
// as secure-join adds its own message on success (after some other messasges),
// we do not want to add "Messages are end-to-end encrypted" on chat creation.
// we detect secure join by `can_send` (for Bob, scanner side) and by `blocked` (for Alice, inviter side) below.
if !chat.is_encrypted(context).await?
|| self <= DC_CHAT_ID_LAST_SPECIAL
|| chat.is_device_talk()
|| chat.is_self_talk()
|| (!chat.can_send(context).await? && !chat.is_contact_request())
|| chat.blocked == Blocked::Yes
{
return Ok(());
}
/// Adds message "Messages are end-to-end encrypted".
async fn add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> {
let text = stock_str::messages_e2e_encrypted(context).await;
add_info_msg_with_cmd(
context,
@@ -621,74 +477,6 @@ impl ChatId {
Ok(())
}
/// Sets protection and adds a message.
///
/// `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
async fn set_protection_for_timestamp_sort(
self,
context: &Context,
protect: ProtectionStatus,
timestamp_sort: i64,
contact_id: Option<ContactId>,
) -> Result<()> {
let protection_status_modified = self
.inner_set_protection(context, protect)
.await
.with_context(|| format!("Cannot set protection for {self}"))?;
if protection_status_modified {
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
.await?;
chatlist_events::emit_chatlist_item_changed(context, self);
}
Ok(())
}
/// Sets protection and sends or adds a message.
///
/// `timestamp_sent` is the "sent" timestamp of a message caused the protection state change.
pub(crate) async fn set_protection(
self,
context: &Context,
protect: ProtectionStatus,
timestamp_sent: i64,
contact_id: Option<ContactId>,
) -> Result<()> {
let sort_to_bottom = true;
let (received, incoming) = (false, false);
let ts = self
.calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, received, incoming)
.await?
// Always sort protection messages below `SystemMessage::SecurejoinWait{,Timeout}` ones
// in case of race conditions.
.saturating_add(1);
self.set_protection_for_timestamp_sort(context, protect, ts, contact_id)
.await
}
/// Sets the 1:1 chat with the given address to ProtectionStatus::Protected,
/// and posts a `SystemMessage::ChatProtectionEnabled` into it.
///
/// If necessary, creates a hidden chat for this.
pub(crate) async fn set_protection_for_contact(
context: &Context,
contact_id: ContactId,
timestamp: i64,
) -> Result<()> {
let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes)
.await
.with_context(|| format!("can't create chat for {contact_id}"))?;
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
timestamp,
Some(contact_id),
)
.await?;
Ok(())
}
/// Archives or unarchives a chat.
pub async fn set_visibility(self, context: &Context, visibility: ChatVisibility) -> Result<()> {
self.set_visibility_ex(context, Sync, visibility).await
@@ -1396,16 +1184,6 @@ impl ChatId {
Ok(())
}
/// Returns true if the chat is protected.
pub async fn is_protected(self, context: &Context) -> Result<ProtectionStatus> {
let protection_status = context
.sql
.query_get_value("SELECT protected FROM chats WHERE id=?", (self,))
.await?
.unwrap_or_default();
Ok(protection_status)
}
/// Returns the sort timestamp for a new message in the chat.
///
/// `message_timestamp` should be either the message "sent" timestamp or a timestamp of the
@@ -1560,9 +1338,6 @@ pub struct Chat {
/// Duration of the chat being muted.
pub mute_duration: MuteDuration,
/// If the chat is protected (verified).
pub(crate) protected: ProtectionStatus,
}
impl Chat {
@@ -1572,7 +1347,7 @@ impl Chat {
.sql
.query_row(
"SELECT c.type, c.name, c.grpid, c.param, c.archived,
c.blocked, c.locations_send_until, c.muted_until, c.protected
c.blocked, c.locations_send_until, c.muted_until
FROM chats c
WHERE c.id=?;",
(chat_id,),
@@ -1587,7 +1362,6 @@ impl Chat {
blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
is_sending_locations: row.get(6)?,
mute_duration: row.get(7)?,
protected: row.get(8)?,
};
Ok(c)
},
@@ -1868,53 +1642,38 @@ impl Chat {
!self.is_unpromoted()
}
/// Returns true if chat protection is enabled.
///
/// UI should display a green checkmark
/// in the chat title,
/// in the chat profile title and
/// in the chatlist item
/// if chat protection is enabled.
/// UI should also display a green checkmark
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
pub fn is_protected(&self) -> bool {
self.protected == ProtectionStatus::Protected
}
/// Returns true if the chat is encrypted.
pub async fn is_encrypted(&self, context: &Context) -> Result<bool> {
let is_encrypted = self.is_protected()
|| match self.typ {
Chattype::Single => {
match context
.sql
.query_row_optional(
"SELECT cc.contact_id, c.fingerprint<>''
let is_encrypted = match self.typ {
Chattype::Single => {
match context
.sql
.query_row_optional(
"SELECT cc.contact_id, c.fingerprint<>''
FROM chats_contacts cc LEFT JOIN contacts c
ON c.id=cc.contact_id
WHERE cc.chat_id=?
",
(self.id,),
|row| {
let id: ContactId = row.get(0)?;
let is_key: bool = row.get(1)?;
Ok((id, is_key))
},
)
.await?
{
Some((id, is_key)) => is_key || id == ContactId::DEVICE,
None => true,
}
(self.id,),
|row| {
let id: ContactId = row.get(0)?;
let is_key: bool = row.get(1)?;
Ok((id, is_key))
},
)
.await?
{
Some((id, is_key)) => is_key || id == ContactId::DEVICE,
None => true,
}
Chattype::Group => {
// Do not encrypt ad-hoc groups.
!self.grpid.is_empty()
}
Chattype::Mailinglist => false,
Chattype::OutBroadcast | Chattype::InBroadcast => true,
};
}
Chattype::Group => {
// Do not encrypt ad-hoc groups.
!self.grpid.is_empty()
}
Chattype::Mailinglist => false,
Chattype::OutBroadcast | Chattype::InBroadcast => true,
};
Ok(is_encrypted)
}
@@ -2248,17 +2007,21 @@ impl Chat {
/// Sends a `SyncAction` synchronising chat contacts to other devices.
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
if self.is_encrypted(context).await? {
let self_fp = self_fingerprint(context).await?;
let fingerprint_addrs = context
.sql
.query_map(
"SELECT c.fingerprint, c.addr
"SELECT c.id, c.fingerprint, c.addr
FROM contacts c INNER JOIN chats_contacts cc
ON c.id=cc.contact_id
WHERE cc.chat_id=? AND cc.add_timestamp >= cc.remove_timestamp",
(self.id,),
|row| {
let fingerprint = row.get(0)?;
let addr = row.get(1)?;
if row.get::<_, ContactId>(0)? == ContactId::SELF {
return Ok((self_fp.to_string(), String::new()));
}
let fingerprint = row.get(1)?;
let addr = row.get(2)?;
Ok((fingerprint, addr))
},
|addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
@@ -2625,7 +2388,6 @@ impl ChatIdBlocked {
_ => (),
}
let protected = contact_id == ContactId::SELF || contact.is_verified(context).await?;
let smeared_time = create_smeared_timestamp(context);
let chat_id = context
@@ -2633,19 +2395,14 @@ impl ChatIdBlocked {
.transaction(move |transaction| {
transaction.execute(
"INSERT INTO chats
(type, name, param, blocked, created_timestamp, protected)
VALUES(?, ?, ?, ?, ?, ?)",
(type, name, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
(
Chattype::Single,
chat_name,
params.to_string(),
create_blocked as u8,
smeared_time,
if protected {
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
},
),
)?;
let chat_id = ChatId::new(
@@ -2666,19 +2423,12 @@ impl ChatIdBlocked {
})
.await?;
if protected {
chat_id
.add_protection_msg(
context,
ProtectionStatus::Protected,
Some(contact_id),
smeared_time,
)
.await?;
} else {
chat_id
.maybe_add_encrypted_msg(context, smeared_time)
.await?;
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.is_encrypted(context).await?
&& !chat.param.exists(Param::Devicetalk)
&& !chat.param.exists(Param::Selftalk)
{
chat_id.add_encrypted_msg(context, smeared_time).await?;
}
Ok(Self {
@@ -3542,6 +3292,7 @@ pub async fn get_chat_media(
msg_type: Viewtype,
msg_type2: Viewtype,
msg_type3: Viewtype,
limit: Option<u32>,
) -> Result<Vec<MsgId>> {
let list = if msg_type == Viewtype::Webxdc
&& msg_type2 == Viewtype::Unknown
@@ -3556,12 +3307,14 @@ pub async fn get_chat_media(
AND chat_id != ?
AND type = ?
AND hidden=0
ORDER BY max(timestamp, timestamp_rcvd), id;",
ORDER BY max(timestamp, timestamp_rcvd), id
LIMIT ?;",
(
chat_id.is_none(),
chat_id.unwrap_or_else(|| ChatId::new(0)),
DC_CHAT_ID_TRASH,
Viewtype::Webxdc,
limit.map(|l| l.to_string()).unwrap_or("ALL".to_string()),
),
|row| row.get::<_, MsgId>(0),
|ids| Ok(ids.flatten().collect()),
@@ -3577,7 +3330,8 @@ pub async fn get_chat_media(
AND chat_id != ?
AND type IN (?, ?, ?)
AND hidden=0
ORDER BY timestamp, id;",
ORDER BY timestamp, id
LIMIT ?;",
(
chat_id.is_none(),
chat_id.unwrap_or_else(|| ChatId::new(0)),
@@ -3593,6 +3347,7 @@ pub async fn get_chat_media(
} else {
msg_type
},
limit.map(|l| l.to_string()).unwrap_or("ALL".to_string()),
),
|row| row.get::<_, MsgId>(0),
|ids| Ok(ids.flatten().collect()),
@@ -3650,23 +3405,26 @@ pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Resul
Ok(list)
}
/// Creates a group chat with a given `name`.
/// Deprecated on 2025-06-21, use `create_group_ex()`.
pub async fn create_group_chat(
context: &Context,
protect: ProtectionStatus,
name: &str,
) -> Result<ChatId> {
create_group_ex(context, Some(protect), name).await
/// Creates an encrypted group chat.
pub async fn create_group(context: &Context, name: &str) -> Result<ChatId> {
create_group_ex(context, Sync, create_id(), name).await
}
/// Creates an unencrypted group chat.
pub async fn create_group_unencrypted(context: &Context, name: &str) -> Result<ChatId> {
create_group_ex(context, Sync, String::new(), name).await
}
/// Creates a group chat.
///
/// * `encryption` - If `Some`, the chat is encrypted (with key-contacts) and can be protected.
/// * `sync` - Whether a multi-device synchronization message should be sent. Ignored for
/// unencrypted chats currently.
/// * `grpid` - Group ID. Iff nonempty, the chat is encrypted (with key-contacts).
/// * `name` - Chat name.
pub async fn create_group_ex(
pub(crate) async fn create_group_ex(
context: &Context,
encryption: Option<ProtectionStatus>,
sync: sync::Sync,
grpid: String,
name: &str,
) -> Result<ChatId> {
let mut chat_name = sanitize_single_line(name);
@@ -3677,11 +3435,6 @@ pub async fn create_group_ex(
chat_name = "".to_string();
}
let grpid = match encryption {
Some(_) => create_id(),
None => String::new(),
};
let timestamp = create_smeared_timestamp(context);
let row_id = context
.sql
@@ -3689,7 +3442,7 @@ pub async fn create_group_ex(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(Chattype::Group, chat_name, grpid, timestamp),
(Chattype::Group, &chat_name, &grpid, timestamp),
)
.await?;
@@ -3700,19 +3453,9 @@ pub async fn create_group_ex(
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, chat_id);
match encryption {
Some(ProtectionStatus::Protected) => {
let protect = ProtectionStatus::Protected;
chat_id
.set_protection_for_timestamp_sort(context, protect, timestamp, None)
.await?;
}
Some(ProtectionStatus::Unprotected) => {
// Add "Messages are end-to-end encrypted." message
// even to unprotected chats.
chat_id.maybe_add_encrypted_msg(context, timestamp).await?;
}
None => {}
if !grpid.is_empty() {
// Add "Messages are end-to-end encrypted." message.
chat_id.add_encrypted_msg(context, timestamp).await?;
}
if !context.get_config_bool(Config::Bot).await?
@@ -3721,7 +3464,11 @@ pub async fn create_group_ex(
let text = stock_str::new_group_send_first_message(context).await;
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
}
if let (true, true) = (sync.into(), !grpid.is_empty()) {
let id = SyncId::Grpid(grpid);
let action = SyncAction::CreateGroupEncrypted(chat_name);
self::sync(context, id, action).await.log_err(context).ok();
}
Ok(chat_id)
}
@@ -3737,7 +3484,7 @@ pub async fn create_group_ex(
/// which would make it hard to grep for it.
///
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
/// see [`create_group_chat`] for more information on the unpromoted state.
/// see [`create_group`] for more information on the unpromoted state.
///
/// Returns the created chat's id.
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
@@ -3961,13 +3708,6 @@ pub(crate) async fn add_contact_to_chat_ex(
}
} else {
// else continue and send status mail
if chat.is_protected() && !contact.is_verified(context).await? {
error!(
context,
"Cannot add non-bidirectionally verified contact {contact_id} to protected chat {chat_id}."
);
return Ok(false);
}
if is_contact_in_chat(context, chat_id, contact_id).await? {
return Ok(false);
}
@@ -4627,24 +4367,21 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> Result<usize> {
}
}
/// Returns a tuple of `(chatid, is_protected, blocked)`.
/// Returns a tuple of `(chatid, blocked)`.
pub(crate) async fn get_chat_id_by_grpid(
context: &Context,
grpid: &str,
) -> Result<Option<(ChatId, bool, Blocked)>> {
) -> Result<Option<(ChatId, Blocked)>> {
context
.sql
.query_row_optional(
"SELECT id, blocked, protected FROM chats WHERE grpid=?;",
"SELECT id, blocked FROM chats WHERE grpid=?;",
(grpid,),
|row| {
let chat_id = row.get::<_, ChatId>(0)?;
let b = row.get::<_, Option<Blocked>>(1)?.unwrap_or_default();
let p = row
.get::<_, Option<ProtectionStatus>>(2)?
.unwrap_or_default();
Ok((chat_id, p == ProtectionStatus::Protected, b))
Ok((chat_id, b))
},
)
.await
@@ -4947,16 +4684,14 @@ async fn set_contacts_by_fingerprints(
"Cannot add key-contacts to unencrypted chat {id}"
);
ensure!(
chat.typ == Chattype::OutBroadcast,
"{id} is not a broadcast list",
matches!(chat.typ, Chattype::Group | Chattype::OutBroadcast),
"{id} is not a group or broadcast",
);
let mut contacts = HashSet::new();
for (fingerprint, addr) in fingerprint_addrs {
let contact_addr = ContactAddress::new(addr)?;
let contact =
Contact::add_or_lookup_ex(context, "", &contact_addr, fingerprint, Origin::Hidden)
.await?
.0;
let contact = Contact::add_or_lookup_ex(context, "", addr, fingerprint, Origin::Hidden)
.await?
.0;
contacts.insert(contact);
}
let contacts_old = HashSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
@@ -4995,7 +4730,7 @@ pub(crate) enum SyncId {
/// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups.
Msgids(Vec<String>),
// Special id for device chat.
/// Special id for device chat.
Device,
}
@@ -5009,6 +4744,8 @@ pub(crate) enum SyncAction {
SetMuted(MuteDuration),
/// Create broadcast channel with the given name.
CreateBroadcast(String),
/// Create encrypted group chat with the given name.
CreateGroupEncrypted(String),
Rename(String),
/// Set chat contacts by their addresses.
SetContacts(Vec<String>),
@@ -5074,6 +4811,9 @@ impl Context {
if let SyncAction::CreateBroadcast(name) = action {
create_broadcast_ex(self, Nosync, grpid.clone(), name.clone()).await?;
return Ok(());
} else if let SyncAction::CreateGroupEncrypted(name) = action {
create_group_ex(self, Nosync, grpid.clone(), name).await?;
return Ok(());
}
get_chat_id_by_grpid(self, grpid)
.await?
@@ -5095,7 +4835,7 @@ impl Context {
SyncAction::Accept => chat_id.accept_ex(self, Nosync).await,
SyncAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
SyncAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
SyncAction::CreateBroadcast(_) => {
SyncAction::CreateBroadcast(_) | SyncAction::CreateGroupEncrypted(..) => {
Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
}
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,

View File

@@ -96,7 +96,7 @@ async fn test_get_draft() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_draft() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let chat_id = create_group(&t, "abc").await?;
let mut msg = Message::new_text("hi!".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await?;
@@ -120,7 +120,7 @@ async fn test_forwarding_draft_failing() -> Result<()> {
chat_id.set_draft(&t, Some(&mut msg)).await?;
assert_eq!(msg.id, chat_id.get_draft(&t).await?.unwrap().id);
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id2 = create_group(&t, "foo").await?;
assert!(forward_msgs(&t, &[msg.id], chat_id2).await.is_err());
Ok(())
}
@@ -169,7 +169,7 @@ async fn test_draft_stable_ids() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_only_one_draft_per_chat() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let chat_id = create_group(&t, "abc").await?;
let msgs: Vec<message::Message> = (1..=1000)
.map(|i| Message::new_text(i.to_string()))
@@ -196,7 +196,7 @@ async fn test_only_one_draft_per_chat() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
let chat_id = create_group(&t, "chat").await?;
let quote1 =
Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote1".to_string()).await?).await?;
let quote2 =
@@ -247,7 +247,7 @@ async fn test_quote_replies() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let grp_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
let grp_chat_id = create_group(&alice, "grp").await?;
let grp_msg_id = send_text_msg(&alice, grp_chat_id, "bar".to_string()).await?;
let grp_msg = Message::load_from_db(&alice, grp_msg_id).await?;
@@ -295,9 +295,7 @@ async fn test_quote_replies() -> Result<()> {
async fn test_add_contact_to_chat_ex_add_self() {
// Adding self to a contact should succeed, even though it's pointless.
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let chat_id = create_group(&t, "foo").await.unwrap();
let added = add_contact_to_chat_ex(&t, Nosync, chat_id, ContactId::SELF, false)
.await
.unwrap();
@@ -336,8 +334,7 @@ async fn test_member_add_remove() -> Result<()> {
}
tcm.section("Create and promote a group.");
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_chat_id = create_group(&alice, "Group chat").await?;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
let sent = alice
@@ -399,8 +396,7 @@ async fn test_parallel_member_remove() -> Result<()> {
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await;
tcm.section("Alice creates and promotes a group");
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_chat_id = create_group(&alice, "Group chat").await?;
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
let alice_sent_msg = alice
@@ -457,8 +453,7 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> {
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(&fiona).await;
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_chat_id = create_group(&alice, "Group chat").await?;
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent_msg = alice.send_text(alice_chat_id, "I created a group").await;
let bob_received_msg = bob.recv_msg(&sent_msg).await;
@@ -488,7 +483,7 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> {
// If Bob sends a message to Alice now, Fiona is removed.
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
let sent_msg = bob
.send_text(alice_chat_id, "I have removed Fiona some time ago.")
.send_text(bob_chat_id, "I have removed Fiona some time ago.")
.await;
alice.recv_msg(&sent_msg).await;
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
@@ -504,7 +499,7 @@ async fn test_modify_chat_multi_device() -> Result<()> {
a1.set_config_bool(Config::BccSelf, true).await?;
// create group and sync it to the second device
let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?;
let a1_chat_id = create_group(&a1, "foo").await?;
let sent = a1.send_text(a1_chat_id, "ho!").await;
let a1_msg = a1.get_last_msg().await;
let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?;
@@ -602,7 +597,7 @@ async fn test_modify_chat_disordered() -> Result<()> {
let fiona = tcm.fiona().await;
let fiona_id = alice.add_or_lookup_contact_id(&fiona).await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let alice_chat_id = create_group(&alice, "foo").await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
@@ -649,9 +644,7 @@ async fn test_lost_member_added() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
.await;
let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await;
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
@@ -681,7 +674,7 @@ async fn test_modify_chat_lost() -> Result<()> {
let fiona = tcm.fiona().await;
let fiona_id = alice.add_or_lookup_contact_id(&fiona).await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let alice_chat_id = create_group(&alice, "foo").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(&alice, alice_chat_id, charlie_id).await?;
add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?;
@@ -722,7 +715,7 @@ async fn test_leave_group() -> Result<()> {
let bob = tcm.bob().await;
tcm.section("Alice creates group chat with Bob.");
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let alice_chat_id = create_group(&alice, "foo").await?;
let bob_contact = alice.add_or_lookup_contact(&bob).await.id;
add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?;
@@ -1381,9 +1374,7 @@ async fn test_pinned() {
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
let chat_id2 = t.get_self_chat().await.id;
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let chat_id3 = create_group(&t, "foo").await.unwrap();
let chatlist = get_chats_from_chat_list(&t, DC_GCL_NO_SPECIALS).await;
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
@@ -1473,9 +1464,7 @@ async fn test_set_chat_name() {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let chat_id = create_group(alice, "foo").await.unwrap();
assert_eq!(
Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
"foo"
@@ -1547,7 +1536,7 @@ async fn test_shall_attach_selfavatar() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(alice, "foo").await?;
assert!(!shall_attach_selfavatar(alice, chat_id).await?);
let contact_id = alice.add_or_lookup_contact_id(bob).await;
@@ -1569,7 +1558,7 @@ async fn test_profile_data_on_group_leave() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(t, "foo").await?;
let contact_id = t.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(t, chat_id, contact_id).await?;
@@ -1594,9 +1583,7 @@ async fn test_profile_data_on_group_leave() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_mute_duration() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let chat_id = create_group(&t, "foo").await.unwrap();
// Initial
assert_eq!(
Chat::load_from_db(&t, chat_id).await.unwrap().is_muted(),
@@ -1645,7 +1632,7 @@ async fn test_set_mute_duration() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_info_msg() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
add_info_msg(&t, chat_id, "foo info", time()).await?;
let msg = t.get_last_msg_in(chat_id).await;
@@ -1662,7 +1649,7 @@ async fn test_add_info_msg() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_info_msg_with_cmd() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let msg_id = add_info_msg_with_cmd(
&t,
chat_id,
@@ -1931,14 +1918,14 @@ async fn test_classic_email_chat() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_get_color() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group_ex(&t, None, "a chat").await?;
let chat_id = create_group_unencrypted(&t, "a chat").await?;
let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
assert_eq!(color1, 0x6239dc);
// upper-/lowercase makes a difference for the colors, these are different groups
// (in contrast to email addresses, where upper-/lowercase is ignored in practise)
let t = TestContext::new().await;
let chat_id = create_group_ex(&t, None, "A CHAT").await?;
let chat_id = create_group_unencrypted(&t, "A CHAT").await?;
let color2 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
assert_ne!(color2, color1);
Ok(())
@@ -1948,7 +1935,7 @@ async fn test_chat_get_color() -> Result<()> {
async fn test_chat_get_color_encrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let chat_id = create_group_ex(t, Some(ProtectionStatus::Unprotected), "a chat").await?;
let chat_id = create_group(t, "a chat").await?;
let color1 = Chat::load_from_db(t, chat_id).await?.get_color(t).await?;
set_chat_name(t, chat_id, "A CHAT").await?;
let color2 = Chat::load_from_db(t, chat_id).await?.get_color(t).await?;
@@ -2137,7 +2124,7 @@ async fn test_forward_info_msg() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat_id1 = create_group_chat(alice, ProtectionStatus::Unprotected, "a").await?;
let chat_id1 = create_group(alice, "a").await?;
send_text_msg(alice, chat_id1, "msg one".to_string()).await?;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(alice, chat_id1, bob_id).await?;
@@ -2204,8 +2191,7 @@ async fn test_forward_group() -> Result<()> {
let bob_chat = bob.create_chat(&alice).await;
// Alice creates a group with Bob.
let alice_group_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_group_chat_id = create_group(&alice, "Group").await?;
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?;
@@ -2257,8 +2243,7 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> {
.set_config(Config::Displayname, Some("secretname"))
.await?;
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
let group_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "secretgrpname").await?;
let group_id = create_group(&alice, "secretgrpname").await?;
add_contact_to_chat(&alice, group_id, bob_id).await?;
let mut msg = Message::new_text("bla foo".to_owned());
let sent_msg = alice.send_msg(group_id, &mut msg).await;
@@ -2273,7 +2258,7 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> {
let orig_msg = bob.recv_msg(&sent_msg).await;
let charlie_id = bob.add_or_lookup_contact_id(&charlie).await;
let single_id = ChatId::create_for_contact(&bob, charlie_id).await?;
let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?;
let group_id = create_group(&bob, "group2").await?;
add_contact_to_chat(&bob, group_id, charlie_id).await?;
let broadcast_id = create_broadcast(&bob, "Channel".to_string()).await?;
add_contact_to_chat(&bob, broadcast_id, charlie_id).await?;
@@ -2371,7 +2356,7 @@ async fn test_save_msgs_order() -> Result<()> {
for a in [alice, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let chat_id = create_group_chat(alice, ProtectionStatus::Protected, "grp").await?;
let chat_id = create_group(alice, "grp").await?;
let sent = [
alice.send_text(chat_id, "0").await,
alice.send_text(chat_id, "1").await,
@@ -2427,14 +2412,14 @@ async fn test_forward_from_saved_to_saved() -> Result<()> {
let bob = TestContext::new_bob().await;
let sent = alice.send_text(alice.create_chat(&bob).await.id, "k").await;
bob.recv_msg(&sent).await;
let received_message = bob.recv_msg(&sent).await;
let orig = bob.get_last_msg().await;
let self_chat = bob.get_self_chat().await;
save_msgs(&bob, &[orig.id]).await?;
let saved1 = bob.get_last_msg().await;
assert_eq!(
saved1.get_original_msg_id(&bob).await?.unwrap(),
sent.sender_msg_id
received_message.id
);
assert_ne!(saved1.from_id, ContactId::SELF);
@@ -2492,7 +2477,7 @@ async fn test_resend_own_message() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let fiona = TestContext::new_fiona().await;
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
let alice_grp = create_group(&alice, "grp").await?;
add_contact_to_chat(
&alice,
alice_grp,
@@ -2579,7 +2564,7 @@ async fn test_resend_foreign_message_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
let alice_grp = create_group(alice, "grp").await?;
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
@@ -2596,7 +2581,7 @@ async fn test_resend_info_message_fails() -> Result<()> {
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
let alice_grp = create_group(alice, "grp").await?;
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
alice.send_text(alice_grp, "alice->bob").await;
@@ -2619,7 +2604,7 @@ async fn test_can_send_group() -> Result<()> {
let chat_id = ChatId::create_for_contact(&alice, bob).await?;
let chat = Chat::load_from_db(&alice, chat_id).await?;
assert!(chat.can_send(&alice).await?);
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&alice, "foo").await?;
assert_eq!(
Chat::load_from_db(&alice, chat_id)
.await?
@@ -2659,7 +2644,7 @@ async fn test_broadcast() -> Result<()> {
add_contact_to_chat(
&alice,
broadcast_id,
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
get_chat_contacts(&alice, msg.chat_id).await?.pop().unwrap(),
)
.await?;
let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
@@ -3115,7 +3100,7 @@ async fn test_chat_get_encryption_info() -> Result<()> {
let contact_bob = alice.add_or_lookup_contact_id(bob).await;
let contact_fiona = alice.add_or_lookup_contact_id(fiona).await;
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let chat_id = create_group(alice, "Group").await?;
assert_eq!(
chat_id.get_encryption_info(alice).await?,
"End-to-end encryption available"
@@ -3177,9 +3162,7 @@ async fn test_out_failed_on_all_keys_missing() -> Result<()> {
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_chat_id = bob
.create_group_with_members(ProtectionStatus::Unprotected, "", &[alice, fiona])
.await;
let bob_chat_id = bob.create_group_with_members("", &[alice, fiona]).await;
bob.send_text(bob_chat_id, "Gossiping Fiona's key").await;
alice
.recv_msg(&bob.send_text(bob_chat_id, "No key gossip").await)
@@ -3197,8 +3180,8 @@ async fn test_out_failed_on_all_keys_missing() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_chat_media() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "bar").await?;
let chat_id1 = create_group(&t, "foo").await?;
let chat_id2 = create_group(&t, "bar").await?;
assert_eq!(
get_chat_media(
@@ -3206,7 +3189,8 @@ async fn test_get_chat_media() -> Result<()> {
Some(chat_id1),
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Unknown
Viewtype::Unknown,
None,
)
.await?
.len(),
@@ -3267,6 +3251,7 @@ async fn test_get_chat_media() -> Result<()> {
Viewtype::Image,
Viewtype::Unknown,
Viewtype::Unknown,
None,
)
.await?
.len(),
@@ -3279,6 +3264,7 @@ async fn test_get_chat_media() -> Result<()> {
Viewtype::Sticker,
Viewtype::Unknown,
Viewtype::Unknown,
None,
)
.await?
.len(),
@@ -3291,11 +3277,25 @@ async fn test_get_chat_media() -> Result<()> {
Viewtype::Sticker,
Viewtype::Image,
Viewtype::Unknown,
None,
)
.await?
.len(),
2
);
assert_eq!(
get_chat_media(
&t,
Some(chat_id1),
Viewtype::Sticker,
Viewtype::Image,
Viewtype::Unknown,
Some(1),
)
.await?
.len(),
1
);
assert_eq!(
get_chat_media(
&t,
@@ -3303,6 +3303,7 @@ async fn test_get_chat_media() -> Result<()> {
Viewtype::Webxdc,
Viewtype::Unknown,
Viewtype::Unknown,
None,
)
.await?
.len(),
@@ -3315,6 +3316,7 @@ async fn test_get_chat_media() -> Result<()> {
Viewtype::Image,
Viewtype::Unknown,
Viewtype::Unknown,
None,
)
.await?
.len(),
@@ -3327,6 +3329,7 @@ async fn test_get_chat_media() -> Result<()> {
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Unknown,
None,
)
.await?
.len(),
@@ -3339,6 +3342,33 @@ async fn test_get_chat_media() -> Result<()> {
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Webxdc,
None,
)
.await?
.len(),
4
);
assert_eq!(
get_chat_media(
&t,
None,
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Webxdc,
Some(3),
)
.await?
.len(),
3
);
assert_eq!(
get_chat_media(
&t,
None,
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Webxdc,
Some(99),
)
.await?
.len(),
@@ -3354,6 +3384,7 @@ async fn test_get_chat_media() -> Result<()> {
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Webxdc,
None,
)
.await?
.len(),
@@ -3395,6 +3426,7 @@ async fn test_get_chat_media_webxdc_order() -> Result<()> {
Viewtype::Webxdc,
Viewtype::Unknown,
Viewtype::Unknown,
None,
)
.await?;
assert_eq!(media.first().unwrap(), &instance1_id);
@@ -3410,6 +3442,7 @@ async fn test_get_chat_media_webxdc_order() -> Result<()> {
Viewtype::Webxdc,
Viewtype::Unknown,
Viewtype::Unknown,
None,
)
.await?;
assert_eq!(media.first().unwrap(), &instance2_id);
@@ -3422,7 +3455,7 @@ async fn test_get_chat_media_webxdc_order() -> Result<()> {
async fn test_blob_renaming() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
let chat_id = create_group(&alice, "Group").await?;
add_contact_to_chat(&alice, chat_id, alice.add_or_lookup_contact_id(&bob).await).await?;
let file = alice.get_blobdir().join("harmless_file.\u{202e}txt.exe");
fs::write(&file, "aaa").await?;
@@ -3484,9 +3517,7 @@ async fn test_sync_blocked() -> Result<()> {
// - Group chats synchronisation.
// - That blocking a group deletes it on other devices.
let fiona = TestContext::new_fiona().await;
let fiona_grp_chat_id = fiona
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0])
.await;
let fiona_grp_chat_id = fiona.create_group_with_members("grp", &[alice0]).await;
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
@@ -3619,9 +3650,7 @@ async fn test_sync_delete_chat() -> Result<()> {
.get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. }))
.await;
let bob_grp_chat_id = bob
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0])
.await;
let bob_grp_chat_id = bob.create_group_with_members("grp", &[alice0]).await;
let sent_msg = bob.send_text(bob_grp_chat_id, "hi").await;
let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
@@ -3854,6 +3883,61 @@ async fn test_sync_name() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_create_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = &tcm.bob().await;
let a0_bob_contact_id = alice0.add_or_lookup_contact_id(bob).await;
let a1_bob_contact_id = alice1.add_or_lookup_contact_id(bob).await;
let a0_chat_id = create_group(alice0, "grp").await?;
sync(alice0, alice1).await;
let a0_chat = Chat::load_from_db(alice0, a0_chat_id).await?;
let a1_chat_id = get_chat_id_by_grpid(alice1, &a0_chat.grpid)
.await?
.unwrap()
.0;
let a1_chat = Chat::load_from_db(alice1, a1_chat_id).await?;
assert_eq!(a1_chat.get_type(), Chattype::Group);
assert_eq!(a1_chat.is_promoted(), false);
assert_eq!(a1_chat.get_name(), "grp");
set_chat_name(alice0, a0_chat_id, "renamed").await?;
sync(alice0, alice1).await;
let a1_chat = Chat::load_from_db(alice1, a1_chat_id).await?;
assert_eq!(a1_chat.is_promoted(), false);
assert_eq!(a1_chat.get_name(), "renamed");
add_contact_to_chat(alice0, a0_chat_id, a0_bob_contact_id).await?;
sync(alice0, alice1).await;
let a1_chat = Chat::load_from_db(alice1, a1_chat_id).await?;
assert_eq!(a1_chat.is_promoted(), false);
assert_eq!(
get_chat_contacts(alice1, a1_chat_id).await?,
[a1_bob_contact_id, ContactId::SELF]
);
// Let's test a contact removal from another device.
remove_contact_from_chat(alice1, a1_chat_id, a1_bob_contact_id).await?;
sync(alice1, alice0).await;
let a0_chat = Chat::load_from_db(alice0, a0_chat_id).await?;
assert_eq!(a0_chat.is_promoted(), false);
assert_eq!(
get_chat_contacts(alice0, a0_chat_id).await?,
[ContactId::SELF]
);
let sent_msg = alice0.send_text(a0_chat_id, "hi").await;
let msg = alice1.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, a1_chat_id);
assert_eq!(a1_chat_id.is_promoted(alice1).await?, true);
Ok(())
}
/// Tests sending JPEG image with .png extension.
///
/// This is a regression test, previously sending failed
@@ -3988,9 +4072,7 @@ async fn test_info_contact_id() -> Result<()> {
}
// Alice creates group, Bob receives group
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "play", &[bob])
.await;
let alice_chat_id = alice.create_group_with_members("play", &[bob]).await;
let sent_msg1 = alice.send_text(alice_chat_id, "moin").await;
let msg = bob.recv_msg(&sent_msg1).await;
@@ -4036,26 +4118,27 @@ async fn test_info_contact_id() -> Result<()> {
)
.await?;
let fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await; // contexts are in sync, fiona_id is same everywhere
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
let alice_fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await;
let bob_fiona_id = bob.add_or_lookup_contact_id(&tcm.fiona().await).await;
add_contact_to_chat(alice, alice_chat_id, alice_fiona_id).await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::MemberAddedToGroup,
fiona_id,
fiona_id,
alice_fiona_id,
bob_fiona_id,
)
.await?;
remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?;
remove_contact_from_chat(alice, alice_chat_id, alice_fiona_id).await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::MemberRemovedFromGroup,
fiona_id,
fiona_id,
alice_fiona_id,
bob_fiona_id,
)
.await?;
@@ -4063,11 +4146,12 @@ async fn test_info_contact_id() -> Result<()> {
// We raw delete in db as Contact::delete() leaves a tombstone (which is great as the tap works longer then)
alice
.sql
.execute("DELETE FROM contacts WHERE id=?", (fiona_id,))
.execute("DELETE FROM contacts WHERE id=?", (alice_fiona_id,))
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_info_type(), SystemMessage::MemberRemovedFromGroup);
assert!(msg.get_info_contact_id(alice).await?.is_none());
assert!(msg.get_info_contact_id(bob).await?.is_none());
Ok(())
}
@@ -4085,8 +4169,7 @@ async fn test_add_member_bug() -> Result<()> {
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
// Create a group.
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_chat_id = create_group(alice, "Group chat").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
@@ -4130,8 +4213,7 @@ async fn test_past_members() -> Result<()> {
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
tcm.section("Alice creates a chat.");
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_chat_id = create_group(alice, "Group chat").await?;
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
alice
.send_text(alice_chat_id, "Hi! I created a group.")
@@ -4165,8 +4247,7 @@ async fn test_non_member_cannot_modify_member_list() -> Result<()> {
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_chat_id = create_group(alice, "Group chat").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let alice_sent_msg = alice
.send_text(alice_chat_id, "Hi! I created a group.")
@@ -4209,8 +4290,7 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_chat_id = create_group(alice, "Group chat").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 3);
@@ -4241,8 +4321,7 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
let fiona = &tcm.fiona().await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_chat_id = create_group(alice, "Group chat").await?;
add_contact_to_chat(alice, alice_chat_id, alice_fiona_contact_id).await?;
alice
.send_text(alice_chat_id, "Hi! I created a group.")
@@ -4279,7 +4358,7 @@ async fn test_past_members_order() -> Result<()> {
let fiona = tcm.fiona().await;
let fiona_contact_id = t.add_or_lookup_contact_id(&fiona).await;
let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "Group chat").await?;
let chat_id = create_group(t, "Group chat").await?;
add_contact_to_chat(t, chat_id, bob_contact_id).await?;
add_contact_to_chat(t, chat_id, charlie_contact_id).await?;
add_contact_to_chat(t, chat_id, fiona_contact_id).await?;
@@ -4341,8 +4420,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_chat_id = create_group(alice, "Group chat").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
add_contact_to_chat(alice, alice_chat_id, alice_charlie_contact_id).await?;
@@ -4530,9 +4608,7 @@ async fn test_cannot_send_edit_request() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[bob])
.await;
let chat_id = alice.create_group_with_members("My Group", &[bob]).await;
// Alice can edit her message
let sent1 = alice.send_text(chat_id, "foo").await;
@@ -4703,7 +4779,7 @@ async fn test_no_address_contacts_in_group_chats() -> Result<()> {
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
let chat_id = create_group(alice, "Group chat").await?;
let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await;
let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await;
@@ -4762,7 +4838,7 @@ async fn test_create_unencrypted_group_chat() -> Result<()> {
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let chat_id = create_group_ex(alice, None, "Group chat").await?;
let chat_id = create_group_unencrypted(alice, "Group chat").await?;
let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await;
let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await;
@@ -4783,7 +4859,7 @@ async fn test_create_unencrypted_group_chat() -> Result<()> {
async fn test_create_group_invalid_name() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let chat_id = create_group_ex(alice, None, " ").await?;
let chat_id = create_group(alice, " ").await?;
let chat = Chat::load_from_db(alice, chat_id).await?;
assert_eq!(chat.get_name(), "");
Ok(())
@@ -4829,7 +4905,7 @@ async fn test_long_group_name() -> Result<()> {
let bob = &tcm.bob().await;
let group_name = "δδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδδ";
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, group_name).await?;
let alice_chat_id = create_group(alice, group_name).await?;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice

View File

@@ -481,8 +481,8 @@ mod tests {
use super::*;
use crate::chat::save_msgs;
use crate::chat::{
ProtectionStatus, add_contact_to_chat, create_group_chat, get_chat_contacts,
remove_contact_from_chat, send_text_msg,
add_contact_to_chat, create_group, get_chat_contacts, remove_contact_from_chat,
send_text_msg,
};
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
@@ -495,15 +495,9 @@ mod tests {
async fn test_try_load() {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
let chat_id1 = create_group(bob, "a chat").await.unwrap();
let chat_id2 = create_group(bob, "b chat").await.unwrap();
let chat_id3 = create_group(bob, "c chat").await.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
@@ -536,9 +530,7 @@ mod tests {
// receive a message from alice
let alice = &tcm.alice().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat")
.await
.unwrap();
let alice_chat_id = create_group(alice, "alice chat").await.unwrap();
add_contact_to_chat(
alice,
alice_chat_id,
@@ -576,9 +568,7 @@ mod tests {
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new_alice().await;
t.update_device_chats().await.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
create_group(&t, "a chat").await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
@@ -765,9 +755,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chat_id1 = create_group(&t, "a chat").await.unwrap();
let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
@@ -783,9 +771,7 @@ mod tests {
async fn test_get_summary_deleted_draft() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chat_id = create_group(&t, "a chat").await.unwrap();
let mut msg = Message::new_text("Foobar".to_string());
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
@@ -824,15 +810,9 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
let chat_id1 = create_group(&t, "a chat").await.unwrap();
create_group(&t, "b chat").await.unwrap();
create_group(&t, "c chat").await.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();

View File

@@ -389,12 +389,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"))]

View File

@@ -300,8 +300,6 @@ async fn get_configured_param(
param.smtp.password.clone()
};
let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
let mut addr = param.addr.clone();
if param.oauth2 {
// the used oauth2 addr may differ, check this.
@@ -343,7 +341,7 @@ async fn get_configured_param(
"checking internal provider-info for offline autoconfig"
);
provider = provider::get_provider_info(ctx, &param_domain, proxy_enabled).await;
provider = provider::get_provider_info(&param_domain);
if let Some(provider) = provider {
if provider.server.is_empty() {
info!(ctx, "Offline autoconfig found, but no servers defined.");

View File

@@ -118,7 +118,7 @@ pub enum Chattype {
/// Group chat.
///
/// Created by [`crate::chat::create_group_chat`].
/// Created by [`crate::chat::create_group`].
Group = 120,
/// An (unencrypted) mailing list,

View File

@@ -1790,9 +1790,7 @@ WHERE type=? AND id IN (
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Some((chat_id, _, _)) =
chat::get_chat_id_by_grpid(context, &contact.addr).await?
{
if let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await? {
chat_id.unblock_ex(context, Nosync).await?;
}
}

View File

@@ -1,7 +1,7 @@
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr};
use super::*;
use crate::chat::{Chat, ProtectionStatus, get_chat_contacts, send_text_msg};
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
@@ -1320,9 +1320,6 @@ async fn test_self_is_verified() -> Result<()> {
assert!(contact.get_verifier_id(&alice).await?.is_none());
assert!(contact.is_key_contact());
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
Ok(())
}

View File

@@ -14,7 +14,7 @@ use pgp::types::PublicKeyTrait;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt};
use crate::chat::{ChatId, get_chat_cnt};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
@@ -1035,12 +1035,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(),
@@ -1089,7 +1083,6 @@ impl Context {
async fn get_self_report(&self) -> Result<String> {
#[derive(Default)]
struct ChatNumbers {
protected: u32,
opportunistic_dc: u32,
opportunistic_mua: u32,
unencrypted_dc: u32,
@@ -1124,7 +1117,6 @@ impl Context {
res += &format!("key_created {key_created}\n");
// how many of the chats active in the last months are:
// - protected
// - opportunistic-encrypted and the contact uses Delta Chat
// - opportunistic-encrypted and the contact uses a classical MUA
// - unencrypted and the contact uses Delta Chat
@@ -1133,7 +1125,7 @@ impl Context {
let chats = self
.sql
.query_map(
"SELECT c.protected, m.param, m.msgrmsg
"SELECT m.param, m.msgrmsg
FROM chats c
JOIN msgs m
ON c.id=m.chat_id
@@ -1151,23 +1143,20 @@ impl Context {
GROUP BY c.id",
(DownloadState::Done, ContactId::INFO, three_months_ago),
|row| {
let protected: ProtectionStatus = row.get(0)?;
let message_param: Params =
row.get::<_, String>(1)?.parse().unwrap_or_default();
let is_dc_message: bool = row.get(2)?;
Ok((protected, message_param, is_dc_message))
Ok((message_param, is_dc_message))
},
|rows| {
let mut chats = ChatNumbers::default();
for row in rows {
let (protected, message_param, is_dc_message) = row?;
let (message_param, is_dc_message) = row?;
let encrypted = message_param
.get_bool(Param::GuaranteeE2ee)
.unwrap_or(false);
if protected == ProtectionStatus::Protected {
chats.protected += 1;
} else if encrypted {
if encrypted {
if is_dc_message {
chats.opportunistic_dc += 1;
} else {
@@ -1183,7 +1172,6 @@ impl Context {
},
)
.await?;
res += &format!("chats_protected {}\n", chats.protected);
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
@@ -1216,9 +1204,6 @@ impl Context {
mark_contact_id_as_verified(self, contact_id, Some(ContactId::SELF)).await?;
let chat_id = ChatId::create_for_contact(self, contact_id).await?;
chat_id
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
.await?;
let mut msg = Message::new_text(self.get_self_report().await?);

View File

@@ -604,15 +604,12 @@ async fn test_draft_self_report() -> Result<()> {
let chat_id = alice.draft_self_report().await?;
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
let chat = Chat::load_from_db(&alice, chat_id).await?;
assert!(chat.is_protected());
assert_eq!(msg.get_info_type(), SystemMessage::ChatE2ee);
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
assert!(draft.text.starts_with("core_version"));
// Test that sending into the protected chat works:
// Test that sending into the chat works:
let _sent = alice.send_msg(chat_id, &mut draft).await;
Ok(())

View File

@@ -12,7 +12,7 @@ use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
use crate::{
chat::{self, Chat, ChatItem, ProtectionStatus, create_group_chat, send_text_msg},
chat::{self, Chat, ChatItem, create_group, send_text_msg},
tools::IsNoneOrEmpty,
};
@@ -164,7 +164,7 @@ async fn test_ephemeral_enable_disable() -> Result<()> {
async fn test_ephemeral_unpromoted() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
let chat_id = create_group(&alice, "Group name").await?;
// Group is unpromoted, the timer can be changed without sending a message.
assert!(chat_id.is_unpromoted(&alice).await?);
@@ -799,8 +799,7 @@ async fn test_ephemeral_timer_non_member() -> Result<()> {
let bob = &tcm.bob().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group name").await?;
let alice_chat_id = create_group(alice, "Group name").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
send_text_msg(alice, alice_chat_id, "Hi!".to_string()).await?;

View File

@@ -66,8 +66,7 @@ mod test_chatlist_events {
use crate::{
EventType,
chat::{
self, ChatId, ChatVisibility, MuteDuration, ProtectionStatus, create_broadcast,
create_group_chat, set_muted,
self, ChatId, ChatVisibility, MuteDuration, create_broadcast, create_group, set_muted,
},
config::Config,
constants::*,
@@ -138,12 +137,7 @@ mod test_chatlist_events {
async fn test_change_chat_visibility() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat_id = create_group_chat(
&alice,
crate::chat::ProtectionStatus::Unprotected,
"my_group",
)
.await?;
let chat_id = create_group(&alice, "my_group").await?;
chat_id
.set_visibility(&alice, ChatVisibility::Pinned)
@@ -289,7 +283,7 @@ mod test_chatlist_events {
async fn test_delete_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let chat = create_group(&alice, "My Group").await?;
alice.evtracker.clear_events();
chat.delete(&alice).await?;
@@ -299,11 +293,11 @@ mod test_chatlist_events {
/// Create group chat
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_group_chat() -> Result<()> {
async fn test_create_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
alice.evtracker.clear_events();
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let chat = create_group(&alice, "My Group").await?;
wait_for_chatlist_and_specific_item(&alice, chat).await;
Ok(())
}
@@ -324,7 +318,7 @@ mod test_chatlist_events {
async fn test_mute_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let chat = create_group(&alice, "My Group").await?;
alice.evtracker.clear_events();
chat::set_muted(&alice, chat, MuteDuration::Forever).await?;
@@ -343,7 +337,7 @@ mod test_chatlist_events {
async fn test_mute_chat_expired() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let chat = create_group(&alice, "My Group").await?;
let mute_duration = MuteDuration::Until(
std::time::SystemTime::now()
@@ -363,7 +357,7 @@ mod test_chatlist_events {
async fn test_change_chat_name() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let chat = create_group(&alice, "My Group").await?;
alice.evtracker.clear_events();
chat::set_chat_name(&alice, chat, "New Name").await?;
@@ -377,7 +371,7 @@ mod test_chatlist_events {
async fn test_change_chat_profile_image() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let chat = create_group(&alice, "My Group").await?;
alice.evtracker.clear_events();
let file = alice.dir.path().join("avatar.png");
@@ -395,9 +389,7 @@ mod test_chatlist_events {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
@@ -419,9 +411,7 @@ mod test_chatlist_events {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
@@ -438,9 +428,7 @@ mod test_chatlist_events {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
@@ -456,7 +444,7 @@ mod test_chatlist_events {
async fn test_delete_message() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let chat = create_group(&alice, "My Group").await?;
let message = chat::send_text_msg(&alice, chat, "Hello World".to_owned()).await?;
alice.evtracker.clear_events();
@@ -473,9 +461,7 @@ mod test_chatlist_events {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let chat = alice.create_group_with_members("My Group", &[&bob]).await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
chat_id_for_bob.accept(&bob).await?;
@@ -516,7 +502,7 @@ mod test_chatlist_events {
async fn test_update_after_ephemeral_messages() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let chat = create_group(&alice, "My Group").await?;
chat.set_ephemeral_timer(&alice, crate::ephemeral::Timer::Enabled { duration: 60 })
.await?;
alice
@@ -560,8 +546,7 @@ First thread."#;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_chatid =
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
let alice_chatid = chat::create_group(&alice.ctx, "the chat").await?;
// Step 1: Generate QR-code, secure-join implied by chatid
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid)).await?;
@@ -608,7 +593,7 @@ First thread."#;
async fn test_resend_message() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let chat = create_group(&alice, "My Group").await?;
let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
let _ = alice.pop_sent_msg().await;
@@ -628,7 +613,7 @@ First thread."#;
async fn test_reaction() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let chat = create_group(&alice, "My Group").await?;
let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
let _ = alice.pop_sent_msg().await;

View File

@@ -964,10 +964,6 @@ impl MimeFactory {
hidden_headers.push(header.clone());
} else if is_hidden(&header_name) {
hidden_headers.push(header.clone());
} else if header_name == "autocrypt"
&& !context.get_config_bool(Config::ProtectAutocrypt).await?
{
unprotected_headers.push(header.clone());
} else if header_name == "from" {
// Unencrypted securejoin messages should _not_ include the display name:
if is_encrypted || !is_securejoin_message {
@@ -1085,6 +1081,17 @@ impl MimeFactory {
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
};
let verifier_id: Option<u32> = context
.sql
.query_get_value(
"SELECT verifier FROM contacts WHERE fingerprint=?",
(&fingerprint,),
)
.await?;
let is_verified =
verifier_id.is_some_and(|verifier_id| verifier_id != 0);
if !should_do_gossip {
continue;
}
@@ -1095,7 +1102,7 @@ impl MimeFactory {
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included.
prefer_encrypt: EncryptPreference::NoPreference,
verified: false,
verified: is_verified,
}
.to_string();
@@ -1320,20 +1327,6 @@ impl MimeFactory {
let command = msg.param.get_cmd();
let mut placeholdertext = None;
let send_verified_headers = match chat.typ {
Chattype::Single => true,
Chattype::Group => true,
// Mailinglists and broadcast channels can actually never be verified:
Chattype::Mailinglist => false,
Chattype::OutBroadcast | Chattype::InBroadcast => false,
};
if chat.is_protected() && send_verified_headers {
headers.push((
"Chat-Verified",
mail_builder::headers::raw::Raw::new("1").into(),
));
}
if chat.typ == Chattype::Group {
// Send group ID unless it is an ad hoc group that has no ID.
if !chat.grpid.is_empty() {

View File

@@ -6,8 +6,7 @@ use std::time::Duration;
use super::*;
use crate::chat::{
self, ChatId, ProtectionStatus, add_contact_to_chat, create_group_chat,
remove_contact_from_chat, send_text_msg,
self, ChatId, add_contact_to_chat, create_group, remove_contact_from_chat, send_text_msg,
};
use crate::chatlist::Chatlist;
use crate::constants;
@@ -352,9 +351,7 @@ async fn test_subject_in_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = tcm.alice().await;
let bob = tcm.bob().await;
let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname")
.await
.unwrap();
let group_id = chat::create_group(&t, "groupname").await.unwrap();
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?;
@@ -666,7 +663,7 @@ async fn test_selfavatar_unencrypted_signed() {
assert_eq!(part.match_indices("From:").count(), 1);
assert_eq!(part.match_indices("Message-ID:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 0);
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
@@ -717,7 +714,7 @@ async fn test_selfavatar_unencrypted_signed() {
assert_eq!(part.match_indices("From:").count(), 1);
assert_eq!(part.match_indices("Message-ID:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 0);
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
@@ -756,7 +753,7 @@ async fn test_remove_member_bcc() -> Result<()> {
let charlie_contact = Contact::get_by_id(alice, charlie_id).await?;
let charlie_addr = charlie_contact.get_addr();
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?;
let alice_chat_id = create_group(alice, "foo").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, charlie_id).await?;
send_text_msg(alice, alice_chat_id, "Creating a group".to_string()).await?;
@@ -846,16 +843,12 @@ async fn test_dont_remove_self() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let first_group = alice
.create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob])
.await;
let first_group = alice.create_group_with_members("First group", &[bob]).await;
alice.send_text(first_group, "Hi! I created a group.").await;
remove_contact_from_chat(alice, first_group, ContactId::SELF).await?;
alice.pop_sent_msg().await;
let second_group = alice
.create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob])
.await;
let second_group = alice.create_group_with_members("First group", &[bob]).await;
let sent = alice
.send_text(second_group, "Hi! I created another group.")
.await;
@@ -883,9 +876,7 @@ async fn test_new_member_is_first_recipient() -> Result<()> {
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
let group = alice
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
.await;
let group = alice.create_group_with_members("Group", &[bob]).await;
alice.send_text(group, "Hi! I created a group.").await;
SystemTime::shift(Duration::from_secs(60));

View File

@@ -1817,39 +1817,6 @@ async fn test_take_last_header() {
);
}
async fn test_protect_autocrypt(enabled: bool) -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat = alice.create_chat(bob).await;
alice
.set_config_bool(Config::ProtectAutocrypt, enabled)
.await?;
let sent = alice.send_text(chat.id, "Hello!").await;
assert_eq!(sent.payload().contains("Autocrypt: "), !enabled);
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.get_showpadlock(), true);
Ok(())
}
/// Tests that if `protect_autocrypt` is enabled,
/// `Autocrypt` header does not appear in the outer headers
/// of encrypted messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protect_autocrypt_enabled() -> Result<()> {
test_protect_autocrypt(true).await
}
/// Tests that if `protect_autocrypt` is disabled,
/// `Autocrypt` header appears in the outer headers
/// of encrypted messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protect_autocrypt_false() -> Result<()> {
test_protect_autocrypt(false).await
}
/// Tests that CRLF before MIME boundary
/// is not treated as the part body.
///

View File

@@ -14,17 +14,6 @@ use crate::provider;
use crate::provider::Oauth2Authorizer;
use crate::tools::time;
const OAUTH2_GMAIL: Oauth2 = Oauth2 {
// see <https://developers.google.com/identity/protocols/OAuth2InstalledApp>
client_id: "959970109878-4mvtgf6feshskf7695nfln6002mom908.apps.googleusercontent.com",
get_code: "https://accounts.google.com/o/oauth2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline",
init_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&code=$CODE&grant_type=authorization_code",
refresh_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&refresh_token=$REFRESH_TOKEN&grant_type=refresh_token",
get_userinfo: Some(
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=$ACCESS_TOKEN",
),
};
const OAUTH2_YANDEX: Oauth2 = Oauth2 {
// see <https://tech.yandex.com/oauth/doc/dg/reference/auto-code-client-docpage/>
client_id: "c4d0b6735fc8420a816d7e1303469341",
@@ -64,7 +53,7 @@ pub async fn get_oauth2_url(
addr: &str,
redirect_uri: &str,
) -> Result<Option<String>> {
if let Some(oauth2) = Oauth2::from_address(context, addr).await {
if let Some(oauth2) = Oauth2::from_address(addr) {
context
.sql
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri))
@@ -84,7 +73,7 @@ pub(crate) async fn get_oauth2_access_token(
code: &str,
regenerate: bool,
) -> Result<Option<String>> {
if let Some(oauth2) = Oauth2::from_address(context, addr).await {
if let Some(oauth2) = Oauth2::from_address(addr) {
let lock = context.oauth2_mutex.lock().await;
// read generated token
@@ -232,7 +221,7 @@ pub(crate) async fn get_oauth2_addr(
addr: &str,
code: &str,
) -> Result<Option<String>> {
let oauth2 = match Oauth2::from_address(context, addr).await {
let oauth2 = match Oauth2::from_address(addr) {
Some(o) => o,
None => return Ok(None),
};
@@ -267,19 +256,16 @@ pub(crate) async fn get_oauth2_addr(
}
impl Oauth2 {
async fn from_address(context: &Context, addr: &str) -> Option<Self> {
fn from_address(addr: &str) -> Option<Self> {
let addr_normalized = normalize_addr(addr);
let skip_mx = true;
if let Some(domain) = addr_normalized
.find('@')
.map(|index| addr_normalized.split_at(index + 1).1)
{
if let Some(oauth2_authorizer) = provider::get_provider_info(context, domain, skip_mx)
.await
if let Some(oauth2_authorizer) = provider::get_provider_info(domain)
.and_then(|provider| provider.oauth2_authorizer.as_ref())
{
return Some(match oauth2_authorizer {
Oauth2Authorizer::Gmail => OAUTH2_GMAIL,
Oauth2Authorizer::Yandex => OAUTH2_YANDEX,
});
}
@@ -366,21 +352,16 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_oauth_from_address() {
let t = TestContext::new().await;
// Delta Chat does not have working Gmail client ID anymore.
assert_eq!(Oauth2::from_address(&t, "hello@gmail.com").await, None);
assert_eq!(Oauth2::from_address(&t, "hello@googlemail.com").await, None);
assert_eq!(Oauth2::from_address("hello@gmail.com"), None);
assert_eq!(Oauth2::from_address("hello@googlemail.com"), None);
assert_eq!(
Oauth2::from_address(&t, "hello@yandex.com").await,
Oauth2::from_address("hello@yandex.com"),
Some(OAUTH2_YANDEX)
);
assert_eq!(
Oauth2::from_address(&t, "hello@yandex.ru").await,
Some(OAUTH2_YANDEX)
);
assert_eq!(Oauth2::from_address(&t, "hello@web.de").await, None);
assert_eq!(Oauth2::from_address("hello@yandex.ru"), Some(OAUTH2_YANDEX));
assert_eq!(Oauth2::from_address("hello@web.de"), None);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -574,7 +574,7 @@ mod tests {
use super::*;
use crate::{
EventType,
chat::{self, ChatId, ProtectionStatus, add_contact_to_chat, resend_msgs, send_msg},
chat::{self, ChatId, add_contact_to_chat, resend_msgs, send_msg},
message::{Message, Viewtype},
test_utils::{TestContext, TestContextManager},
};
@@ -616,7 +616,7 @@ mod tests {
loop {
let event = bob.evtracker.recv().await.unwrap();
if let EventType::WebxdcRealtimeAdvertisementReceived { msg_id } = event.typ {
assert!(msg_id == alice_webxdc.id);
assert!(msg_id == bob_webxdc.id);
break;
}
}
@@ -962,9 +962,7 @@ mod tests {
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
let group = chat::create_group_chat(alice, ProtectionStatus::Unprotected, "group chat")
.await
.unwrap();
let group = chat::create_group(alice, "group chat").await.unwrap();
// Alice sends webxdc to bob
let mut instance = Message::new(Viewtype::File);

View File

@@ -178,7 +178,7 @@ pub async fn pk_encrypt(
let msg = MessageBuilder::from_bytes("", plain);
let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
for pkey in pkeys {
msg.encrypt_to_key(&mut rng, &pkey)?;
msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
}
if let Some(ref skey) = private_key_for_signing {
@@ -347,6 +347,8 @@ mod tests {
use super::*;
use crate::test_utils::{alice_keypair, bob_keypair};
use pgp::composed::Esk;
use pgp::packet::PublicKeyEncryptedSessionKey;
fn pk_decrypt_and_validate<'a>(
ctext: &'a [u8],
@@ -543,4 +545,34 @@ mod tests {
assert_eq!(content, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
/// Tests that recipient key IDs and fingerprints
/// are omitted or replaced with wildcards.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_anonymous_recipients() -> Result<()> {
let ctext = ctext_signed().await.as_bytes();
let cursor = Cursor::new(ctext);
let (msg, _headers) = Message::from_armor(cursor)?;
let Message::Encrypted { esk, .. } = msg else {
unreachable!();
};
for encrypted_session_key in esk {
let Esk::PublicKeyEncryptedSessionKey(pkesk) = encrypted_session_key else {
unreachable!()
};
match pkesk {
PublicKeyEncryptedSessionKey::V3 { id, .. } => {
assert!(id.is_wildcard());
}
PublicKeyEncryptedSessionKey::V6 { fingerprint, .. } => {
assert!(fingerprint.is_none());
}
PublicKeyEncryptedSessionKey::Other { .. } => unreachable!(),
}
}
Ok(())
}
}

View File

@@ -4,13 +4,9 @@ pub(crate) mod data;
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;
use hickory_resolver::name_server::TokioConnectionProvider;
use hickory_resolver::{Resolver, TokioResolver, config};
use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::context::Context;
use crate::log::warn;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
/// Provider status according to manual testing.
@@ -82,13 +78,9 @@ pub enum UsernamePattern {
/// Type of OAuth 2 authorization.
#[derive(Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum Oauth2Authorizer {
/// Yandex.
Yandex = 1,
/// Gmail.
Gmail = 2,
Yandex,
}
/// Email server endpoint.
@@ -175,61 +167,17 @@ impl ProviderOptions {
}
}
/// Get resolver to query MX records.
///
/// We first try to read the system's resolver from `/etc/resolv.conf`.
/// This does not work at least on some Androids, therefore we fallback
/// to the default `ResolverConfig` which uses eg. to google's `8.8.8.8` or `8.8.4.4`.
fn get_resolver() -> Result<TokioResolver> {
if let Ok(resolver) = TokioResolver::builder_tokio() {
return Ok(resolver.build());
}
let resolver = Resolver::builder_with_config(
config::ResolverConfig::default(),
TokioConnectionProvider::default(),
);
Ok(resolver.build())
}
/// Returns provider for the given an e-mail address.
///
/// Returns an error if provided address is not valid.
pub async fn get_provider_info_by_addr(
context: &Context,
addr: &str,
skip_mx: bool,
) -> Result<Option<&'static Provider>> {
pub fn get_provider_info_by_addr(addr: &str) -> Result<Option<&'static Provider>> {
let addr = EmailAddress::new(addr)?;
let provider = get_provider_info(context, &addr.domain, skip_mx).await;
Ok(provider)
}
/// Returns provider for the given domain.
///
/// This function looks up domain in offline database first. If not
/// found, it queries MX record for the domain and looks up offline
/// database for MX domains.
pub async fn get_provider_info(
context: &Context,
domain: &str,
skip_mx: bool,
) -> Option<&'static Provider> {
if let Some(provider) = get_provider_by_domain(domain) {
return Some(provider);
}
if !skip_mx {
if let Some(provider) = get_provider_by_mx(context, domain).await {
return Some(provider);
}
}
None
Ok(get_provider_info(&addr.domain))
}
/// Finds a provider in offline database based on domain.
pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> {
pub fn get_provider_info(domain: &str) -> Option<&'static Provider> {
let domain = domain.to_lowercase();
for (pattern, provider) in PROVIDER_DATA {
if let Some(suffix) = pattern.strip_prefix('*') {
@@ -247,51 +195,6 @@ pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> {
None
}
/// Finds a provider based on MX record for the given domain.
///
/// For security reasons, only Gmail can be configured this way.
pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'static Provider> {
let Ok(resolver) = get_resolver() else {
warn!(context, "Cannot get a resolver to check MX records.");
return None;
};
let mut fqdn: String = domain.to_string();
if !fqdn.ends_with('.') {
fqdn.push('.');
}
let Ok(mx_domains) = resolver.mx_lookup(fqdn).await else {
warn!(context, "Cannot resolve MX records for {domain:?}.");
return None;
};
for (provider_domain_pattern, provider) in PROVIDER_DATA {
if provider.id != "gmail" {
// MX lookup is limited to Gmail for security reasons
continue;
}
if provider_domain_pattern.starts_with('*') {
// Skip wildcard patterns.
continue;
}
let provider_fqdn = provider_domain_pattern.to_string() + ".";
let provider_fqdn_dot = ".".to_string() + &provider_fqdn;
for mx_domain in mx_domains.iter() {
let mx_domain = mx_domain.exchange().to_lowercase().to_utf8();
if mx_domain == provider_fqdn || mx_domain.ends_with(&provider_fqdn_dot) {
return Some(provider);
}
}
}
None
}
/// Returns a provider with the given ID from the database.
pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
if let Some(provider) = PROVIDER_IDS.get(id) {
@@ -304,24 +207,23 @@ pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
#[test]
fn test_get_provider_by_domain_unexistant() {
let provider = get_provider_by_domain("unexistant.org");
let provider = get_provider_info("unexistant.org");
assert!(provider.is_none());
}
#[test]
fn test_get_provider_by_domain_mixed_case() {
let provider = get_provider_by_domain("nAUta.Cu").unwrap();
let provider = get_provider_info("nAUta.Cu").unwrap();
assert!(provider.status == Status::Ok);
}
#[test]
fn test_get_provider_by_domain() {
fn test_get_provider_info() {
let addr = "nauta.cu";
let provider = get_provider_by_domain(addr).unwrap();
let provider = get_provider_info(addr).unwrap();
assert!(provider.status == Status::Ok);
let server = &provider.server[0];
assert_eq!(server.protocol, Protocol::Imap);
@@ -336,13 +238,17 @@ mod tests {
assert_eq!(server.port, 25);
assert_eq!(server.username_pattern, UsernamePattern::Email);
let provider = get_provider_by_domain("gmail.com").unwrap();
let provider = get_provider_info("gmail.com").unwrap();
assert!(provider.status == Status::Preparation);
assert!(!provider.before_login_hint.is_empty());
assert!(!provider.overview_page.is_empty());
let provider = get_provider_by_domain("googlemail.com").unwrap();
let provider = get_provider_info("googlemail.com").unwrap();
assert!(provider.status == Status::Preparation);
assert!(get_provider_info("").is_none());
assert!(get_provider_info("google.com").unwrap().id == "gmail");
assert!(get_provider_info("example@google.com").is_none());
}
#[test]
@@ -351,39 +257,10 @@ mod tests {
assert!(provider.id == "gmail");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_provider_info() {
let t = TestContext::new().await;
assert!(get_provider_info(&t, "", false).await.is_none());
assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
assert!(
get_provider_info(&t, "example@google.com", false)
.await
.is_none()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_provider_info_by_addr() -> Result<()> {
let t = TestContext::new().await;
assert!(
get_provider_info_by_addr(&t, "google.com", false)
.await
.is_err()
);
assert!(
get_provider_info_by_addr(&t, "example@google.com", false)
.await?
.unwrap()
.id
== "gmail"
);
Ok(())
}
#[test]
fn test_get_resolver() -> Result<()> {
assert!(get_resolver().is_ok());
assert!(get_provider_info_by_addr("google.com").is_err());
assert!(get_provider_info_by_addr("example@google.com")?.unwrap().id == "gmail");
Ok(())
}
}

View File

@@ -19,7 +19,7 @@ use crate::key::Fingerprint;
use crate::net::http::post_empty;
use crate::net::proxy::{DEFAULT_SOCKS_PORT, ProxyConfig};
use crate::token;
use crate::tools::validate_id;
use crate::tools::{time, validate_id};
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
@@ -741,8 +741,16 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
authcode,
..
} => {
token::save(context, token::Namespace::InviteNumber, None, &invitenumber).await?;
token::save(context, token::Namespace::Auth, None, &authcode).await?;
let timestamp = time();
token::save(
context,
token::Namespace::InviteNumber,
None,
&invitenumber,
timestamp,
)
.await?;
token::save(context, token::Namespace::Auth, None, &authcode, timestamp).await?;
context.sync_qr_code_tokens(None).await?;
context.scheduler.interrupt_inbox().await;
}
@@ -752,14 +760,23 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
grpid,
..
} => {
let timestamp = time();
token::save(
context,
token::Namespace::InviteNumber,
Some(&grpid),
&invitenumber,
timestamp,
)
.await?;
token::save(
context,
token::Namespace::Auth,
Some(&grpid),
&authcode,
timestamp,
)
.await?;
token::save(context, token::Namespace::Auth, Some(&grpid), &authcode).await?;
context.sync_qr_code_tokens(Some(&grpid)).await?;
context.scheduler.interrupt_inbox().await;
}

View File

@@ -1,5 +1,5 @@
use super::*;
use crate::chat::{ProtectionStatus, create_group_chat};
use crate::chat::create_group;
use crate::config::Config;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{TestContext, TestContextManager, sync};
@@ -479,7 +479,7 @@ async fn test_withdraw_verifycontact() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_withdraw_verifygroup() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&alice, "foo").await?;
let qr = get_securejoin_qr(&alice, Some(chat_id)).await?;
// scanning own verify-group code offers withdrawing
@@ -520,8 +520,8 @@ async fn test_withdraw_multidevice() -> Result<()> {
// 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 chat_id = create_group(alice, "Group").await?;
let chat2_id = create_group(alice, "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?;

View File

@@ -14,9 +14,7 @@ use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use regex::Regex;
use crate::chat::{
self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, remove_from_chat_contacts_table,
};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, remove_from_chat_contacts_table};
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
use crate::contact::{Contact, ContactId, Origin, mark_contact_id_as_verified};
@@ -248,9 +246,7 @@ async fn get_to_and_past_contact_ids(
let chat_id = match chat_assignment {
ChatAssignment::Trash => None,
ChatAssignment::GroupChat { grpid } => {
if let Some((chat_id, _protected, _blocked)) =
chat::get_chat_id_by_grpid(context, grpid).await?
{
if let Some((chat_id, _blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
Some(chat_id)
} else {
None
@@ -742,7 +738,7 @@ pub(crate) async fn receive_imf_inner(
let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?;
if verified_encryption == VerifiedEncryption::Verified {
mark_recipients_as_verified(context, from_id, &to_ids, &mime_parser).await?;
mark_recipients_as_verified(context, from_id, &mime_parser).await?;
}
let received_msg = if let Some(received_msg) = received_msg {
@@ -794,7 +790,6 @@ pub(crate) async fn receive_imf_inner(
allow_creation,
&mut mime_parser,
is_partial_download,
&verified_encryption,
parent_message,
)
.await?;
@@ -812,7 +807,6 @@ pub(crate) async fn receive_imf_inner(
is_partial_download,
replace_msg_id,
prevent_rename,
verified_encryption,
chat_id,
chat_id_blocked,
is_dc_message,
@@ -860,7 +854,9 @@ pub(crate) async fn receive_imf_inner(
if let Some(ref sync_items) = mime_parser.sync_items {
if from_id == ContactId::SELF {
if mime_parser.was_encrypted() {
context.execute_sync_items(sync_items).await;
context
.execute_sync_items(sync_items, mime_parser.timestamp_sent)
.await;
} else {
warn!(context, "Sync items are not encrypted.");
}
@@ -1319,7 +1315,6 @@ async fn do_chat_assignment(
allow_creation: bool,
mime_parser: &mut MimeMessage,
is_partial_download: Option<u32>,
verified_encryption: &VerifiedEncryption,
parent_message: Option<Message>,
) -> Result<(ChatId, Blocked)> {
let is_bot = context.get_config_bool(Config::Bot).await?;
@@ -1362,9 +1357,7 @@ async fn do_chat_assignment(
}
ChatAssignment::GroupChat { grpid } => {
// Try to assign to a chat based on Chat-Group-ID.
if let Some((id, _protected, blocked)) =
chat::get_chat_id_by_grpid(context, grpid).await?
{
if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
chat_id = Some(id);
chat_id_blocked = blocked;
} else if allow_creation || test_normal_chat.is_some() {
@@ -1376,7 +1369,6 @@ async fn do_chat_assignment(
from_id,
to_ids,
past_ids,
verified_encryption,
grpid,
)
.await?
@@ -1478,45 +1470,6 @@ async fn do_chat_assignment(
);
}
}
// Check if the message was sent with verified encryption and set the protection of
// the 1:1 chat accordingly.
let chat = match is_partial_download.is_none()
&& mime_parser.get_header(HeaderDef::SecureJoin).is_none()
{
true => Some(Chat::load_from_db(context, chat_id).await?)
.filter(|chat| chat.typ == Chattype::Single),
false => None,
};
if let Some(chat) = chat {
ensure_and_debug_assert!(
chat.typ == Chattype::Single,
"Chat {chat_id} is not Single",
);
let new_protection = match verified_encryption {
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
};
ensure_and_debug_assert!(
chat.protected == ProtectionStatus::Unprotected
|| new_protection == ProtectionStatus::Protected,
"Chat {chat_id} can't downgrade to Unprotected",
);
if chat.protected != new_protection {
// The message itself will be sorted under the device message since the device
// message is `MessageState::InNoticed`, which means that all following
// messages are sorted under it.
chat_id
.set_protection(
context,
new_protection,
mime_parser.timestamp_sent,
Some(from_id),
)
.await?;
}
}
}
}
} else {
@@ -1533,9 +1486,7 @@ async fn do_chat_assignment(
chat_id = Some(DC_CHAT_ID_TRASH);
}
ChatAssignment::GroupChat { grpid } => {
if let Some((id, _protected, blocked)) =
chat::get_chat_id_by_grpid(context, grpid).await?
{
if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
chat_id = Some(id);
chat_id_blocked = blocked;
} else if allow_creation {
@@ -1547,7 +1498,6 @@ async fn do_chat_assignment(
from_id,
to_ids,
past_ids,
verified_encryption,
grpid,
)
.await?
@@ -1603,7 +1553,7 @@ async fn do_chat_assignment(
if chat_id.is_none() && allow_creation {
let to_contact = Contact::get_by_id(context, to_id).await?;
if let Some(list_id) = to_contact.param.get(Param::ListId) {
if let Some((id, _, blocked)) =
if let Some((id, blocked)) =
chat::get_chat_id_by_grpid(context, list_id).await?
{
chat_id = Some(id);
@@ -1669,7 +1619,6 @@ async fn add_parts(
is_partial_download: Option<u32>,
mut replace_msg_id: Option<MsgId>,
prevent_rename: bool,
verified_encryption: VerifiedEncryption,
chat_id: ChatId,
chat_id_blocked: Blocked,
is_dc_message: MessengerMessage,
@@ -1718,16 +1667,7 @@ async fn add_parts(
apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
}
Chattype::Group => {
apply_group_changes(
context,
mime_parser,
&mut chat,
from_id,
to_ids,
past_ids,
&verified_encryption,
)
.await?
apply_group_changes(context, mime_parser, &mut chat, from_id, to_ids, past_ids).await?
}
Chattype::InBroadcast => {
apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
@@ -2224,6 +2164,7 @@ RETURNING id
if !chat_id.is_trash() && !hidden {
let mut chat = Chat::load_from_db(context, chat_id).await?;
let mut update_param = false;
// In contrast to most other update-timestamps,
// use `sort_timestamp` instead of `sent_timestamp` for the subject-timestamp comparison.
@@ -2237,6 +2178,14 @@ RETURNING id
let subject = mime_parser.get_subject().unwrap_or_default();
chat.param.set(Param::LastSubject, subject);
update_param = true;
}
if chat.is_unpromoted() {
chat.param.remove(Param::Unpromoted);
update_param = true;
}
if update_param {
chat.update_param(context).await?;
}
}
@@ -2632,27 +2581,12 @@ async fn create_group(
from_id: ContactId,
to_ids: &[Option<ContactId>],
past_ids: &[Option<ContactId>],
verified_encryption: &VerifiedEncryption,
grpid: &str,
) -> Result<Option<(ChatId, Blocked)>> {
let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
let mut chat_id = None;
let mut chat_id_blocked = Default::default();
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(
context,
"Creating unprotected group because of the verification problem: {err:#}."
);
ProtectionStatus::Unprotected
} else {
ProtectionStatus::Protected
}
} else {
ProtectionStatus::Unprotected
};
async fn self_explicitly_added(
context: &Context,
mime_parser: &&mut MimeMessage,
@@ -2688,7 +2622,6 @@ async fn create_group(
grpid,
grpname,
create_blocked,
create_protected,
None,
mime_parser.timestamp_sent,
)
@@ -2860,7 +2793,6 @@ async fn apply_group_changes(
from_id: ContactId,
to_ids: &[Option<ContactId>],
past_ids: &[Option<ContactId>],
verified_encryption: &VerifiedEncryption,
) -> Result<GroupChangesInfo> {
let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
ensure!(chat.typ == Chattype::Group);
@@ -2875,24 +2807,6 @@ async fn apply_group_changes(
let is_from_in_chat =
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() && !chat.is_protected() {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(
context,
"Not marking chat {} as protected due to verification problem: {err:#}.", chat.id,
);
} else {
chat.id
.set_protection(
context,
ProtectionStatus::Protected,
mime_parser.timestamp_sent,
Some(from_id),
)
.await?;
}
}
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
// TODO: if address "alice@example.org" is a member of the group twice,
// with old and new key,
@@ -3280,7 +3194,7 @@ async fn create_or_lookup_mailinglist_or_broadcast(
) -> Result<Option<(ChatId, Blocked)>> {
let listid = mailinglist_header_listid(list_id_header)?;
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
if let Some((chat_id, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
return Ok(Some((chat_id, blocked)));
}
@@ -3318,7 +3232,6 @@ async fn create_or_lookup_mailinglist_or_broadcast(
&listid,
name,
blocked,
ProtectionStatus::Unprotected,
param,
mime_parser.timestamp_sent,
)
@@ -3599,7 +3512,6 @@ async fn create_adhoc_group(
"", // Ad hoc groups have no ID.
grpname,
create_blocked,
ProtectionStatus::Unprotected,
None,
mime_parser.timestamp_sent,
)
@@ -3678,7 +3590,6 @@ async fn has_verified_encryption(
async fn mark_recipients_as_verified(
context: &Context,
from_id: ContactId,
to_ids: &[Option<ContactId>],
mimeparser: &MimeMessage,
) -> Result<()> {
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
@@ -3697,19 +3608,6 @@ async fn mark_recipients_as_verified(
}
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(());
}
for to_id in to_ids.iter().filter_map(|&x| x) {
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?;
}
Ok(())

View File

@@ -5,7 +5,7 @@ use tokio::fs;
use super::*;
use crate::chat::{
ChatItem, ChatVisibility, add_contact_to_chat, add_to_chat_contacts_table, create_group_chat,
ChatItem, ChatVisibility, add_contact_to_chat, add_to_chat_contacts_table, create_group,
get_chat_contacts, get_chat_msgs, is_contact_in_chat, remove_contact_from_chat, send_text_msg,
};
use crate::chatlist::Chatlist;
@@ -1000,7 +1000,7 @@ async fn test_other_device_writes_to_mailinglist() -> Result<()> {
chat::get_chat_id_by_grpid(&t, "delta.codespeak.net")
.await?
.unwrap(),
(first_chat.id, false, Blocked::Request)
(first_chat.id, Blocked::Request)
);
receive_imf(
@@ -2901,9 +2901,9 @@ async fn test_accept_outgoing() -> Result<()> {
let bob1_chat = bob1.create_chat(&alice1).await;
let sent = bob1.send_text(bob1_chat.id, "Hello!").await;
alice1.recv_msg(&sent).await;
let alice1_msg = alice1.recv_msg(&sent).await;
alice2.recv_msg(&sent).await;
let alice1_msg = bob2.recv_msg(&sent).await;
bob2.recv_msg(&sent).await;
assert_eq!(alice1_msg.text, "Hello!");
let alice1_chat = chat::Chat::load_from_db(&alice1, alice1_msg.chat_id).await?;
assert!(alice1_chat.is_contact_request());
@@ -2944,7 +2944,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
let charlie = tcm.charlie().await;
// =============== Bob creates a group ===============
let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
let group_id = chat::create_group(&bob, "Group").await?;
chat::add_to_chat_contacts_table(
&bob,
time(),
@@ -3042,9 +3042,7 @@ async fn test_auto_accept_protected_group_for_bots() -> Result<()> {
bob.set_config(Config::Bot, Some("1")).await.unwrap();
mark_as_verified(alice, bob).await;
mark_as_verified(bob, alice).await;
let group_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
.await;
let group_id = alice.create_group_with_members("Group", &[bob]).await;
let sent = alice.send_text(group_id, "Hello!").await;
let msg = bob.recv_msg(&sent).await;
let chat = chat::Chat::load_from_db(bob, msg.chat_id).await?;
@@ -3092,13 +3090,11 @@ async fn test_bot_accepts_another_group_after_qr_scan() -> Result<()> {
let bob = &tcm.bob().await;
bob.set_config(Config::Bot, Some("1")).await?;
let group_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let group_id = chat::create_group(alice, "Group").await?;
let qr = get_securejoin_qr(alice, Some(group_id)).await?;
tcm.exec_securejoin_qr(bob, alice, &qr).await;
let group_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
.await;
let group_id = alice.create_group_with_members("Group", &[bob]).await;
let sent = alice.send_text(group_id, "Hello!").await;
let msg = bob.recv_msg(&sent).await;
let chat = chat::Chat::load_from_db(bob, msg.chat_id).await?;
@@ -3152,7 +3148,7 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
let bob = tcm.bob().await;
tcm.section("Bob creates a group");
let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
let group_id = chat::create_group(&bob, "Group").await?;
chat::add_to_chat_contacts_table(
&bob,
time(),
@@ -3228,11 +3224,7 @@ async fn test_blocked_contact_creates_group() -> Result<()> {
chat.id.block(&alice).await?;
let group_id = bob
.create_group_with_members(
ProtectionStatus::Unprotected,
"group name",
&[&alice, &fiona],
)
.create_group_with_members("group name", &[&alice, &fiona])
.await;
let sent = bob.send_text(group_id, "Heyho, I'm a spammer!").await;
@@ -3253,7 +3245,7 @@ async fn test_blocked_contact_creates_group() -> Result<()> {
assert_eq!(rcvd.chat_blocked, Blocked::Request);
// In order not to lose context, Bob's message should also be shown in the group
let msgs = chat::get_chat_msgs(&alice, rcvd.chat_id).await?;
assert_eq!(msgs.len(), 2);
assert_eq!(msgs.len(), 3);
Ok(())
}
@@ -3728,7 +3720,7 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
let bob = &tcm.bob().await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
let alice_chat_id = create_group(alice, "foos").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
let sent_msg = alice.pop_sent_msg().await;
@@ -3774,7 +3766,7 @@ async fn test_sync_member_list_on_rejoin() -> Result<()> {
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
let alice_chat_id = create_group(alice, "foos").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
@@ -3812,7 +3804,7 @@ async fn test_ignore_outdated_membership_changes() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_bob_id = alice.add_or_lookup_contact_id(bob).await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
let alice_chat_id = create_group(alice, "grp").await?;
// Alice creates a group chat. Bob accepts it.
add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?;
@@ -3860,7 +3852,7 @@ async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
let fiona = &tcm.fiona().await;
let charlie = &tcm.charlie().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_chat_id = create_group(alice, "Group").await?;
add_contact_to_chat(
alice,
@@ -3911,7 +3903,7 @@ async fn test_delayed_removal_is_ignored() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let chat_id = create_group(alice, "Group").await?;
let alice_bob = alice.add_or_lookup_contact_id(bob).await;
let alice_fiona = alice.add_or_lookup_contact_id(fiona).await;
// create chat with three members
@@ -3964,7 +3956,7 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_chat_id = create_group(alice, "Group").await?;
add_contact_to_chat(
alice,
@@ -4202,7 +4194,7 @@ async fn test_member_left_does_not_create_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_chat_id = create_group(alice, "Group").await?;
add_contact_to_chat(
alice,
alice_chat_id,
@@ -4230,7 +4222,7 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_chat_id = create_group(alice, "Group").await?;
add_contact_to_chat(
alice,
alice_chat_id,
@@ -4274,7 +4266,7 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_chat_id = create_group(&alice, "Group").await?;
add_contact_to_chat(
&alice,
alice_chat_id,
@@ -4414,7 +4406,7 @@ async fn test_create_group_with_big_msg() -> Result<()> {
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
let bob_grp_id = create_group(&bob, "Group").await?;
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?;
@@ -4445,7 +4437,7 @@ async fn test_create_group_with_big_msg() -> Result<()> {
// Now Bob can send encrypted messages to Alice.
let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group1").await?;
let bob_grp_id = create_group(&bob, "Group1").await?;
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?;
@@ -4486,7 +4478,7 @@ async fn test_partial_group_consistency() -> Result<()> {
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
let alice_chat_id = create_group(&alice, "foos").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
@@ -4566,7 +4558,7 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
mark_as_verified(alice, bob).await;
let group_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let group_id = create_group(alice, "Group").await?;
let alice_bob_id = alice.add_or_lookup_contact(bob).await.id;
add_contact_to_chat(alice, group_id, alice_bob_id).await?;
alice.send_text(group_id, "Hello!").await;
@@ -4663,7 +4655,7 @@ async fn test_unarchive_on_member_removal() -> Result<()> {
let fiona = &tcm.fiona().await;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
let alice_chat_id = create_group(alice, "foos").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
@@ -4696,9 +4688,7 @@ async fn test_no_op_member_added_is_trash() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "foos", &[bob])
.await;
let alice_chat_id = alice.create_group_with_members("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;
@@ -4765,7 +4755,7 @@ async fn test_references() -> Result<()> {
let bob = &tcm.bob().await;
alice.set_config_bool(Config::BccSelf, true).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_chat_id = create_group(alice, "Group").await?;
alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
@@ -4809,7 +4799,7 @@ async fn test_prefer_references_to_downloaded_msgs() -> Result<()> {
let fiona = &tcm.fiona().await;
let alice_bob_id = tcm.send_recv(bob, alice, "hi").await.from_id;
let alice_fiona_id = tcm.send_recv(fiona, alice, "hi").await.from_id;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_chat_id = create_group(alice, "Group").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?;
// W/o fiona the test doesn't work -- the last message is assigned to the 1:1 chat due to
// `is_probably_private_reply()`.
@@ -5009,12 +4999,7 @@ async fn test_group_name_with_newline() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let chat_id = create_group_chat(
alice,
ProtectionStatus::Unprotected,
"Group\r\nwith\nnewlines",
)
.await?;
let chat_id = create_group(alice, "Group\r\nwith\nnewlines").await?;
add_contact_to_chat(alice, chat_id, alice.add_or_lookup_contact_id(bob).await).await?;
send_text_msg(alice, chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
@@ -5032,7 +5017,7 @@ async fn test_rename_chat_on_missing_message() -> Result<()> {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let charlie = tcm.charlie().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
let chat_id = create_group(&alice, "Group").await?;
add_to_chat_contacts_table(
&alice,
time(),
@@ -5068,7 +5053,7 @@ async fn test_rename_chat_after_creating_invite() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
for populate_before_securejoin in [false, true] {
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let alice_chat_id = create_group(alice, "Group").await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
SystemTime::shift(Duration::from_secs(60));
@@ -5098,7 +5083,7 @@ async fn test_two_group_securejoins() -> Result<()> {
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let group_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let group_id = chat::create_group(alice, "Group").await?;
let qr = get_securejoin_qr(alice, Some(group_id)).await?;
@@ -5123,8 +5108,7 @@ async fn test_unverified_member_msg() -> Result<()> {
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice_chat_id =
chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let alice_chat_id = chat::create_group(alice, "Group").await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
tcm.exec_securejoin_qr(bob, alice, &qr).await;
@@ -5150,12 +5134,15 @@ async fn test_dont_reverify_by_self_on_outgoing_msg() -> Result<()> {
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_chat_id = chat::create_group_chat(bob, ProtectionStatus::Protected, "Group").await?;
let bob_chat_id = chat::create_group(bob, "Group").await?;
let qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?;
tcm.exec_securejoin_qr(fiona, bob, &qr).await;
tcm.exec_securejoin_qr(a0, bob, &qr).await;
tcm.exec_securejoin_qr(a1, bob, &qr).await;
// Shift time by one week to trigger gossip.
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
let a0_chat_id = a0.get_last_msg().await.chat_id;
let a0_sent_msg = a0.send_text(a0_chat_id, "Hi").await;
a1.recv_msg(&a0_sent_msg).await;
@@ -5211,9 +5198,7 @@ async fn test_no_address_contact_added_into_group() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
.await;
let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await;
let bob_received_msg = bob
.recv_msg(&alice.send_text(alice_chat_id, "Message").await)
.await;
@@ -5493,7 +5478,7 @@ async fn test_small_unencrypted_group() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = chat::create_group_ex(alice, None, "Unencrypted group").await?;
let alice_chat_id = chat::create_group_unencrypted(alice, "Unencrypted group").await?;
let alice_bob_id = alice.add_or_lookup_address_contact_id(bob).await;
add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?;
send_text_msg(alice, alice_chat_id, "Hello!".to_string()).await?;

View File

@@ -4,8 +4,7 @@ use anyhow::{Context as _, Error, Result, bail, ensure};
use deltachat_contact_tools::ContactAddress;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, get_chat_id_by_grpid};
use crate::chatlist_events;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, get_chat_id_by_grpid};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
use crate::contact::mark_contact_id_as_verified;
@@ -23,6 +22,7 @@ use crate::qr::check_qr;
use crate::securejoin::bob::JoinerProgress;
use crate::sync::Sync::*;
use crate::token;
use crate::tools::{create_id, time};
mod bob;
mod qrinvite;
@@ -87,10 +87,21 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
let sync_token = token::lookup(context, Namespace::InviteNumber, grpid)
.await?
.is_none();
// invitenumber will be used to allow starting the handshake,
// auth will be used to verify the fingerprint
// Invite number is used to request the inviter key.
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
let auth = token::lookup_or_new(context, Namespace::Auth, grpid).await?;
// Auth token is used to verify the key-contact
// if the token is not old
// and add the contact to the group
// if there is an associated group ID.
//
// We always generate a new auth token
// because auth tokens "expire"
// and can only be used to join groups
// without verification afterwards.
let auth = create_id();
token::save(context, Namespace::Auth, grpid, &auth, time()).await?;
let self_addr = context.get_primary_self_addr().await?;
let self_name = context
.get_config(Config::Displayname)
@@ -378,7 +389,19 @@ pub(crate) async fn handle_securejoin_handshake(
);
return Ok(HandshakeMessage::Ignore);
};
let Some(grpid) = token::auth_foreign_key(context, auth).await? else {
let Some((grpid, timestamp)) = context
.sql
.query_row_optional(
"SELECT foreign_key, timestamp FROM tokens WHERE namespc=? AND token=?",
(Namespace::Auth, auth),
|row| {
let foreign_key: String = row.get(0)?;
let timestamp: i64 = row.get(1)?;
Ok((foreign_key, timestamp))
},
)
.await?
else {
warn!(
context,
"Ignoring {step} message because of invalid auth code."
@@ -396,7 +419,11 @@ pub(crate) async fn handle_securejoin_handshake(
}
};
if !verify_sender_by_fingerprint(context, &fingerprint, contact_id).await? {
let sender_contact = Contact::get_by_id(context, contact_id).await?;
if sender_contact
.fingerprint()
.is_none_or(|fp| fp != fingerprint)
{
warn!(
context,
"Ignoring {step} message because of fingerprint mismatch."
@@ -404,6 +431,11 @@ pub(crate) async fn handle_securejoin_handshake(
return Ok(HandshakeMessage::Ignore);
}
info!(context, "Fingerprint verified via Auth code.",);
// Mark the contact as verified if auth code is 600 seconds old.
if time() < timestamp + 600 {
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
}
contact_id.regossip_keys(context).await?;
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
// for setup-contact, make Alice's one-to-one chat with Bob visible
@@ -414,13 +446,6 @@ pub(crate) async fn handle_securejoin_handshake(
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
if let Some(group_chat_id) = group_chat_id {
// Join group.
secure_connection_established(
context,
contact_id,
group_chat_id,
mime_message.timestamp_sent,
)
.await?;
chat::add_contact_to_chat_ex(context, Nosync, group_chat_id, contact_id, true)
.await?;
let is_group = true;
@@ -431,13 +456,6 @@ pub(crate) async fn handle_securejoin_handshake(
} else {
let chat_id = info_chat_id(context, contact_id).await?;
// Setup verified contact.
secure_connection_established(
context,
contact_id,
chat_id,
mime_message.timestamp_sent,
)
.await?;
send_alice_handshake_msg(context, contact_id, "vc-contact-confirm")
.await
.context("failed sending vc-contact-confirm message")?;
@@ -560,8 +578,6 @@ pub(crate) async fn observe_securejoin_on_other_device(
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?;
if step == "vg-member-added" || step == "vc-contact-confirm" {
let is_group = mime_message
.get_header(HeaderDef::ChatGroupMemberAdded)
@@ -592,28 +608,6 @@ pub(crate) async fn observe_securejoin_on_other_device(
}
}
async fn secure_connection_established(
context: &Context,
contact_id: ContactId,
chat_id: ChatId,
timestamp: i64,
) -> Result<()> {
let private_chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
.await?
.id;
private_chat_id
.set_protection(
context,
ProtectionStatus::Protected,
timestamp,
Some(contact_id),
)
.await?;
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(())
}
/* ******************************************************************************
* Tools: Misc.
******************************************************************************/

View File

@@ -4,7 +4,7 @@ use anyhow::{Context as _, Result};
use super::HandshakeMessage;
use super::qrinvite::QrInvite;
use crate::chat::{self, ChatId, ProtectionStatus, is_contact_in_chat};
use crate::chat::{self, ChatId, is_contact_in_chat};
use crate::constants::{Blocked, Chattype};
use crate::contact::Origin;
use crate::context::Context;
@@ -74,16 +74,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
.await?;
// Mark 1:1 chat as verified already.
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(invite.contact_id()),
)
.await?;
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(),
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
@@ -123,21 +113,19 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
let ts_sort = chat_id
.calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
.await?;
if chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
let ts_start = time();
chat::add_info_msg_with_cmd(
context,
chat_id,
&stock_str::securejoin_wait(context).await,
SystemMessage::SecurejoinWait,
ts_sort,
Some(ts_start),
None,
None,
None,
)
.await?;
}
let ts_start = time();
chat::add_info_msg_with_cmd(
context,
chat_id,
&stock_str::securejoin_wait(context).await,
SystemMessage::SecurejoinWait,
ts_sort,
Some(ts_start),
None,
None,
None,
)
.await?;
Ok(chat_id)
}
}
@@ -215,15 +203,6 @@ pub(super) async fn handle_auth_required(
}
}
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
message.timestamp_sent,
Some(invite.contact_id()),
)
.await?;
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(),
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
@@ -348,7 +327,7 @@ async fn joining_chat_id(
QrInvite::Contact { .. } => Ok(alice_chat_id),
QrInvite::Group { grpid, name, .. } => {
let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
Some((chat_id, _protected, _blocked)) => {
Some((chat_id, _blocked)) => {
chat_id.unblock_ex(context, Nosync).await?;
chat_id
}
@@ -359,7 +338,6 @@ async fn joining_chat_id(
grpid,
name,
Blocked::Not,
ProtectionStatus::Unprotected, // protection is added later as needed
None,
create_smeared_timestamp(context),
)

View File

@@ -1,3 +1,5 @@
use std::time::Duration;
use deltachat_contact_tools::EmailAddress;
use super::*;
@@ -5,19 +7,17 @@ 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::mimeparser::{GossipedKey, SystemMessage};
use crate::receive_imf::receive_imf;
use crate::stock_str::{self, messages_e2e_encrypted};
use crate::test_utils::{
TestContext, TestContextManager, TimeShiftFalsePositiveNote, get_chat_msg,
};
use crate::tools::SystemTime;
use std::time::Duration;
#[derive(PartialEq)]
enum SetupContactCase {
Normal,
CheckProtectionTimestamp,
WrongAliceGossip,
AliceIsBot,
AliceHasName,
@@ -28,11 +28,6 @@ async fn test_setup_contact() {
test_setup_contact_ex(SetupContactCase::Normal).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact_protection_timestamp() {
test_setup_contact_ex(SetupContactCase::CheckProtectionTimestamp).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact_wrong_alice_gossip() {
test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await
@@ -164,10 +159,6 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
assert!(!sent.payload.contains("Bob Examplenet"));
let mut msg = alice.parse_msg(&sent).await;
let vc_request_with_auth_ts_sent = msg
.get_header(HeaderDef::Date)
.and_then(|value| mailparse::dateparse(value).ok())
.unwrap();
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
@@ -214,10 +205,6 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
assert_eq!(contact_bob.get_authname(), "");
if case == SetupContactCase::CheckProtectionTimestamp {
SystemTime::shift(Duration::from_secs(3600));
}
tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm");
alice.recv_msg_trash(&sent).await;
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true);
@@ -247,9 +234,6 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
assert!(msg.is_info());
let expected_text = messages_e2e_encrypted(&alice).await;
assert_eq!(msg.get_text(), expected_text);
if case == SetupContactCase::CheckProtectionTimestamp {
assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1);
}
}
// Make sure Alice hasn't yet sent their name to Bob.
@@ -292,10 +276,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
let mut i = 0..msg_cnt;
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -448,8 +432,7 @@ async fn test_secure_join() -> Result<()> {
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
let alice_chatid =
chat::create_group_chat(&alice, ProtectionStatus::Protected, "the chat").await?;
let alice_chatid = chat::create_group(&alice, "the chat").await?;
tcm.section("Step 1: Generate QR-code, secure-join implied by chatid");
let qr = get_securejoin_qr(&alice, Some(alice_chatid)).await.unwrap();
@@ -583,7 +566,7 @@ async fn test_secure_join() -> Result<()> {
Blocked::Yes,
"Alice's 1:1 chat with Bob is not hidden"
);
// There should be 3 messages in the chat:
// There should be 2 messages in the chat:
// - The ChatProtectionEnabled message
// - You added member bob@example.net
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
@@ -619,7 +602,6 @@ async fn test_secure_join() -> Result<()> {
}
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
assert!(bob_chat.is_protected());
assert!(bob_chat.typ == Chattype::Group);
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
@@ -667,7 +649,7 @@ async fn test_unknown_sender() -> Result<()> {
tcm.execute_securejoin(&alice, &bob).await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob])
.create_group_with_members("Group with Bob", &[&bob])
.await;
let sent = alice.send_text(alice_chat_id, "Hi!").await;
@@ -733,10 +715,8 @@ async fn test_parallel_securejoin() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat1_id =
chat::create_group_chat(alice, ProtectionStatus::Protected, "First chat").await?;
let alice_chat2_id =
chat::create_group_chat(alice, ProtectionStatus::Protected, "Second chat").await?;
let alice_chat1_id = chat::create_group(alice, "First chat").await?;
let alice_chat2_id = chat::create_group(alice, "Second chat").await?;
let qr1 = get_securejoin_qr(alice, Some(alice_chat1_id)).await?;
let qr2 = get_securejoin_qr(alice, Some(alice_chat2_id)).await?;
@@ -862,3 +842,120 @@ async fn test_wrong_auth_token() -> Result<()> {
Ok(())
}
/// Tests that scanning a QR code week later
/// allows Bob to establish a contact with Alice,
/// but does not mark Bob as verified for Alice.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_expired_contact_auth_token() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
// Alice creates a QR code.
let qr = get_securejoin_qr(alice, None).await?;
// One week passes, QR code expires.
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
// Bob scans the QR code.
join_securejoin(bob, &qr).await?;
// vc-request
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
// vc-auth-requried
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
// vc-request-with-auth
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
// Bob should not be verified for Alice.
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_expired_group_auth_token() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = chat::create_group(alice, "Group").await?;
// Alice creates a group QR code.
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
// One week passes, QR code expires.
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
// Bob scans the QR code.
join_securejoin(bob, &qr).await?;
// vg-request
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
// vg-auth-requried
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
// vg-request-with-auth
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
// vg-member-added
let bob_member_added_msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert!(bob_member_added_msg.is_info());
assert_eq!(
bob_member_added_msg.get_info_type(),
SystemMessage::MemberAddedToGroup
);
// Bob should not be verified for Alice.
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false);
Ok(())
}
/// Tests that old token is considered expired
/// even if sync message just arrived.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_expired_synced_auth_token() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let bob = &tcm.bob().await;
alice.set_config_bool(Config::SyncMsgs, true).await?;
alice2.set_config_bool(Config::SyncMsgs, true).await?;
// Alice creates a QR code on the second device.
let qr = get_securejoin_qr(alice2, None).await?;
alice2.send_sync_msg().await.unwrap();
let sync_msg = alice2.pop_sent_sync_msg().await;
// One week passes, QR code expires.
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
alice.recv_msg_trash(&sync_msg).await;
// Bob scans the QR code.
join_securejoin(bob, &qr).await?;
// vc-request
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
// vc-auth-requried
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
// vc-request-with-auth
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
// Bob should not be verified for Alice.
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false);
Ok(())
}

View File

@@ -19,7 +19,7 @@ use crate::key::DcKey;
use crate::log::{info, warn};
use crate::login_param::ConfiguredLoginParam;
use crate::message::MsgId;
use crate::provider::get_provider_by_domain;
use crate::provider::get_provider_info;
use crate::sql::Sql;
use crate::tools::{Time, inc_and_check, time_elapsed};
@@ -382,7 +382,7 @@ UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
context
.set_config_internal(
Config::ConfiguredProvider,
get_provider_by_domain(&domain).map(|provider| provider.id),
get_provider_info(&domain).map(|provider| provider.id),
)
.await?;
} else {
@@ -1261,6 +1261,16 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
.await?;
}
inc_and_check(&mut migration_version, 134)?;
if dbversion < migration_version {
// Reset all indirect verifications.
sql.execute_migration(
"UPDATE contacts SET verifier=0 WHERE verifier!=1",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?

View File

@@ -122,7 +122,7 @@ async fn test_key_contacts_migration_email2() -> Result<()> {
.await?
.is_empty()
);
let pgp_bob = Contact::get_by_id(&t, ContactId::new(11)).await?;
let pgp_bob = Contact::get_by_id(&t, ContactId::new(11001)).await?;
assert_eq!(pgp_bob.is_key_contact(), true);
assert_eq!(pgp_bob.origin, Origin::Hidden);
@@ -159,7 +159,10 @@ async fn test_key_contacts_migration_verified() -> Result<()> {
INSERT INTO chats_contacts VALUES(10,10,1745609547,0);
"#,
)?)).await?;
t.sql.run_migrations(&t).await?;
STOP_MIGRATIONS_AT
.scope(133, t.sql.run_migrations(&t))
.await?;
// Hidden address-contact can't be looked up.
assert!(

View File

@@ -11,7 +11,7 @@ use tokio::sync::RwLock;
use crate::accounts::Accounts;
use crate::blob::BlobObject;
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
@@ -1083,13 +1083,6 @@ pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
translated(context, StockMessage::ChatProtectionEnabled).await
}
/// Stock string: `%1$s sent a message from another device.`
pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String {
translated(context, StockMessage::ChatProtectionDisabled)
.await
.replace1(&contact_id.get_stock_name(context).await)
}
/// Stock string: `Reply`.
pub(crate) async fn reply_noun(context: &Context) -> String {
translated(context, StockMessage::ReplyNoun).await
@@ -1334,26 +1327,6 @@ impl Context {
Ok(())
}
/// Returns a stock message saying that protection status has changed.
pub(crate) async fn stock_protection_msg(
&self,
protect: ProtectionStatus,
contact_id: Option<ContactId>,
) -> String {
match protect {
ProtectionStatus::Unprotected => {
if let Some(contact_id) = contact_id {
chat_protection_disabled(self, contact_id).await
} else {
// In a group chat, it's not possible to downgrade verification.
// In a 1:1 chat, the `contact_id` always has to be provided.
"[Error] No contact_id given".to_string()
}
}
ProtectionStatus::Protected => messages_e2e_encrypted(self).await,
}
}
pub(crate) async fn update_device_chats(&self) -> Result<()> {
if self.get_config_bool(Config::Bot).await? {
return Ok(());

View File

@@ -253,13 +253,19 @@ impl Context {
/// If an error is returned, the caller shall not try over because some sync items could be
/// already executed. Sync items are considered independent and executed in the given order but
/// regardless of whether executing of the previous items succeeded.
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) {
pub(crate) async fn execute_sync_items(&self, items: &SyncItems, timestamp_sent: i64) {
info!(self, "executing {} sync item(s)", items.items.len());
for item in &items.items {
// Limit the timestamp to ensure it is not in the future.
//
// `sent_timestamp` should be already corrected
// if the `Date` header is in the future.
let timestamp = std::cmp::min(item.timestamp, timestamp_sent);
match &item.data {
SyncDataOrUnknown::SyncData(data) => match data {
AddQrToken(token) => self.add_qr_token(token).await,
DeleteQrToken(token) => self.delete_qr_token(token).await,
AddQrToken(token) => self.add_qr_token(token, timestamp).await,
DeleteQrToken(token) => self.delete_qr_token(token, timestamp).await,
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
SyncData::Config { key, val } => self.sync_config(key, val).await,
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
@@ -284,21 +290,28 @@ impl Context {
}
}
async fn add_qr_token(&self, token: &QrTokenData) -> Result<()> {
async fn add_qr_token(&self, token: &QrTokenData, timestamp: i64) -> Result<()> {
let grpid = token.grpid.as_deref();
token::save(self, Namespace::InviteNumber, grpid, &token.invitenumber).await?;
token::save(self, Namespace::Auth, grpid, &token.auth).await?;
token::save(
self,
Namespace::InviteNumber,
grpid,
&token.invitenumber,
timestamp,
)
.await?;
token::save(self, Namespace::Auth, grpid, &token.auth, timestamp).await?;
Ok(())
}
async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> {
async fn delete_qr_token(&self, token: &QrTokenData, timestamp: i64) -> Result<()> {
self.sql
.execute(
"DELETE FROM tokens
WHERE foreign_key IN
(SELECT foreign_key FROM tokens
WHERE token=? OR token=?)",
(&token.invitenumber, &token.auth),
WHERE token=? OR token=? AND timestamp <= ?)",
(&token.invitenumber, &token.auth, timestamp),
)
.await?;
Ok(())
@@ -339,7 +352,7 @@ mod tests {
use anyhow::bail;
use super::*;
use crate::chat::{Chat, ProtectionStatus, remove_contact_from_chat};
use crate::chat::{Chat, remove_contact_from_chat};
use crate::chatlist::Chatlist;
use crate::contact::{Contact, Origin};
use crate::securejoin::get_securejoin_qr;
@@ -550,6 +563,7 @@ mod tests {
assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await?);
let timestamp_sent = time();
let sync_items = t
.parse_sync_items(
r#"{"items":[
@@ -564,7 +578,7 @@ mod tests {
.to_string(),
)
?;
t.execute_sync_items(&sync_items).await;
t.execute_sync_items(&sync_items, timestamp_sent).await;
assert!(
Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)
@@ -714,8 +728,7 @@ mod tests {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
alice.set_config_bool(Config::SyncMsgs, true).await?;
let alice_chatid =
chat::create_group_chat(alice, ProtectionStatus::Protected, "the chat").await?;
let alice_chatid = chat::create_group(alice, "the chat").await?;
let qr = get_securejoin_qr(alice, Some(alice_chatid)).await?;
// alice2 syncs the QR code token.

View File

@@ -21,8 +21,7 @@ use tokio::runtime::Handle;
use tokio::{fs, task};
use crate::chat::{
self, Chat, ChatId, ChatIdBlocked, MessageListOptions, ProtectionStatus,
add_to_chat_contacts_table, create_group_chat,
self, Chat, ChatId, ChatIdBlocked, MessageListOptions, add_to_chat_contacts_table, create_group,
};
use crate::chatlist::Chatlist;
use crate::config::Config;
@@ -84,6 +83,7 @@ impl TestContextManager {
pub async fn alice(&mut self) -> TestContext {
TestContext::builder()
.configure_alice()
.with_id_offset(1000)
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.await
@@ -92,6 +92,7 @@ impl TestContextManager {
pub async fn bob(&mut self) -> TestContext {
TestContext::builder()
.configure_bob()
.with_id_offset(2000)
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.await
@@ -100,6 +101,7 @@ impl TestContextManager {
pub async fn charlie(&mut self) -> TestContext {
TestContext::builder()
.configure_charlie()
.with_id_offset(3000)
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.await
@@ -108,6 +110,7 @@ impl TestContextManager {
pub async fn dom(&mut self) -> TestContext {
TestContext::builder()
.configure_dom()
.with_id_offset(4000)
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.await
@@ -116,6 +119,7 @@ impl TestContextManager {
pub async fn elena(&mut self) -> TestContext {
TestContext::builder()
.configure_elena()
.with_id_offset(5000)
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.await
@@ -124,6 +128,7 @@ impl TestContextManager {
pub async fn fiona(&mut self) -> TestContext {
TestContext::builder()
.configure_fiona()
.with_id_offset(6000)
.with_log_sink(self.log_sink.clone())
.build(Some(&mut self.used_names))
.await
@@ -263,6 +268,11 @@ pub struct TestContextBuilder {
/// so the caller should store the LogSink elsewhere to
/// prevent it from being dropped immediately.
log_sink: Option<LogSink>,
/// Offset for chat-,message-,contact ids.
///
/// This makes tests fail where ids from different accounts were mixed up.
id_offset: Option<u32>,
}
impl TestContextBuilder {
@@ -328,6 +338,14 @@ impl TestContextBuilder {
self
}
/// Adds an offset for chat-, message-, contact IDs.
///
/// This makes it harder to accidentally mix up IDs from different accounts.
pub fn with_id_offset(mut self, offset: u32) -> Self {
self.id_offset = Some(offset);
self
}
/// Builds the [`TestContext`].
pub async fn build(self, used_names: Option<&mut BTreeSet<String>>) -> TestContext {
if let Some(key_pair) = self.key_pair {
@@ -360,6 +378,22 @@ impl TestContextBuilder {
key::store_self_keypair(&test_context, &key_pair)
.await
.expect("Failed to save key");
if let Some(offset) = self.id_offset {
test_context
.ctx
.sql
.execute(
"UPDATE sqlite_sequence SET seq = ?
WHERE name = 'contacts'
OR name = 'chats'
OR name = 'msgs'",
(offset,),
)
.await
.expect("Failed set id offset");
}
test_context
} else {
TestContext::new_internal(None, self.log_sink).await
@@ -409,21 +443,33 @@ impl TestContext {
///
/// This is a shortcut which configures alice@example.org with a fixed key.
pub async fn new_alice() -> Self {
Self::builder().configure_alice().build(None).await
Self::builder()
.configure_alice()
.with_id_offset(11000)
.build(None)
.await
}
/// Creates a new configured [`TestContext`].
///
/// This is a shortcut which configures bob@example.net with a fixed key.
pub async fn new_bob() -> Self {
Self::builder().configure_bob().build(None).await
Self::builder()
.configure_bob()
.with_id_offset(12000)
.build(None)
.await
}
/// Creates a new configured [`TestContext`].
///
/// This is a shortcut which configures fiona@example.net with a fixed key.
pub async fn new_fiona() -> Self {
Self::builder().configure_fiona().build(None).await
Self::builder()
.configure_fiona()
.with_id_offset(13000)
.build(None)
.await
}
/// Print current chat state.
@@ -1013,7 +1059,7 @@ impl TestContext {
};
writeln!(
res,
"{}#{}: {} [{}]{}{}{} {}",
"{}#{}: {} [{}]{}{}{}",
sel_chat.typ,
sel_chat.get_id(),
sel_chat.get_name(),
@@ -1031,11 +1077,6 @@ impl TestContext {
},
_ => "".to_string(),
},
if sel_chat.is_protected() {
"🛡️"
} else {
""
},
)
.unwrap();
@@ -1066,11 +1107,10 @@ impl TestContext {
pub async fn create_group_with_members(
&self,
protect: ProtectionStatus,
chat_name: &str,
members: &[&TestContext],
) -> ChatId {
let chat_id = create_group_chat(self, protect, chat_name).await.unwrap();
let chat_id = create_group(self, chat_name).await.unwrap();
let mut to_add = vec![];
for member in members {
let contact_id = self.add_or_lookup_contact_id(member).await;
@@ -1630,4 +1670,32 @@ mod tests {
let runtime = tokio::runtime::Runtime::new().expect("unable to create tokio runtime");
runtime.block_on(TestContext::new());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_id_offset() {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
// chat ids
let alice_bob_chat = alice.create_chat(&bob).await;
let bob_alice_chat = bob.create_chat(&alice).await;
assert_ne!(alice_bob_chat.id, bob_alice_chat.id);
// contact ids
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
let fiona_fiona_contact_id = bob.add_or_lookup_contact_id(&fiona).await;
assert_ne!(alice_fiona_contact_id, fiona_fiona_contact_id);
// message ids
let alice_group_id = alice
.create_group_with_members("test group", &[&bob, &fiona])
.await;
let alice_sent_msg = alice.send_text(alice_group_id, "testing").await;
let bob_received_id = bob.recv_msg(&alice_sent_msg).await;
assert_ne!(alice_sent_msg.sender_msg_id, bob_received_id.id);
let fiona_received_id = fiona.recv_msg(&alice_sent_msg).await;
assert_ne!(bob_received_id.id, fiona_received_id.id);
}
}

View File

@@ -9,7 +9,7 @@
use anyhow::Result;
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::chat::{self, Chat, ChatId};
use crate::contact::{Contact, ContactId};
use crate::message::Message;
use crate::receive_imf::receive_imf;
@@ -90,24 +90,12 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition, verified:
}
let mut groups = vec![
chat::create_group_chat(bob, chat::ProtectionStatus::Unprotected, "Group 0")
.await
.unwrap(),
chat::create_group_chat(bob, chat::ProtectionStatus::Unprotected, "Group 1")
.await
.unwrap(),
chat::create_group(bob, "Group 0").await.unwrap(),
chat::create_group(bob, "Group 1").await.unwrap(),
];
if verified {
groups.push(
chat::create_group_chat(bob, chat::ProtectionStatus::Protected, "Group 2")
.await
.unwrap(),
);
groups.push(
chat::create_group_chat(bob, chat::ProtectionStatus::Protected, "Group 3")
.await
.unwrap(),
);
groups.push(chat::create_group(bob, "Group 2").await.unwrap());
groups.push(chat::create_group(bob, "Group 3").await.unwrap());
}
let alice_contact = bob.add_or_lookup_contact_id(alice).await;
@@ -201,8 +189,7 @@ async fn test_aeap_replay_attack() -> Result<()> {
tcm.send_recv_accept(&alice, &bob, "Hi").await;
tcm.send_recv(&bob, &alice, "Hi back").await;
let group =
chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0").await?;
let group = chat::create_group(&bob, "Group 0").await?;
let bob_alice_contact = bob.add_or_lookup_contact_id(&alice).await;
let bob_fiona_contact = bob.add_or_lookup_contact_id(&fiona).await;
@@ -217,10 +204,12 @@ async fn test_aeap_replay_attack() -> Result<()> {
// Fiona gets the message, replaces the From addr...
let sent = sent
.payload()
.replace("From: <alice@example.org>", "From: <fiona@example.net>")
.replace("addr=alice@example.org;", "addr=fiona@example.net;");
.replace("From: <alice@example.org>", "From: <fiona@example.net>");
sent.find("From: <fiona@example.net>").unwrap(); // Assert that it worked
sent.find("addr=fiona@example.net;").unwrap(); // Assert that it worked
// Autocrypt header is protected, nothing to replace outside.
// In the signed part we cannot replace it without breaking the signature.
assert!(!sent.contains("addr=alice@example.org;"));
tcm.section("Fiona replaced the From addr and forwards the message to Bob");
receive_imf(&bob, sent.as_bytes(), false).await?.unwrap();
@@ -243,16 +232,13 @@ async fn test_write_to_alice_after_aeap() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_grp_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let alice_grp_id = chat::create_group(alice, "Group").await?;
let qr = get_securejoin_qr(alice, Some(alice_grp_id)).await?;
tcm.exec_securejoin_qr(bob, alice, &qr).await;
let bob_alice_contact = bob.add_or_lookup_contact(alice).await;
assert!(bob_alice_contact.is_verified(bob).await?);
let bob_alice_chat = bob.create_chat(alice).await;
assert!(bob_alice_chat.is_protected());
let bob_unprotected_grp_id = bob
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[alice])
.await;
let bob_unprotected_grp_id = bob.create_group_with_members("Group", &[alice]).await;
tcm.change_addr(alice, "alice@someotherdomain.xyz").await;
let sent = alice.send_text(alice_grp_id, "Hello!").await;
@@ -260,7 +246,6 @@ async fn test_write_to_alice_after_aeap() -> Result<()> {
assert!(bob_alice_contact.is_verified(bob).await?);
let bob_alice_chat = Chat::load_from_db(bob, bob_alice_chat.id).await?;
assert!(bob_alice_chat.is_protected());
let mut msg = Message::new_text("hi".to_string());
chat::send_msg(bob, bob_alice_chat.id, &mut msg).await?;

View File

@@ -2,9 +2,7 @@ use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::chat::resend_msgs;
use crate::chat::{
self, Chat, ProtectionStatus, add_contact_to_chat, remove_contact_from_chat, send_msg,
};
use crate::chat::{self, Chat, add_contact_to_chat, remove_contact_from_chat, send_msg};
use crate::config::Config;
use crate::constants::Chattype;
use crate::contact::{Contact, ContactId};
@@ -38,8 +36,8 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email:
tcm.execute_securejoin(&alice, &bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
assert_verified(&alice, &bob).await;
assert_verified(&bob, &alice).await;
if by_classical_email {
tcm.section("Bob uses a classical MUA to send a message to Alice");
@@ -59,7 +57,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email:
.unwrap();
let contact = alice.add_or_lookup_contact(&bob).await;
assert_eq!(contact.is_verified(&alice).await.unwrap(), true);
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&alice, &bob).await;
} else {
tcm.section("Bob sets up another Delta Chat device");
let bob2 = tcm.unconfigured().await;
@@ -71,7 +69,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email:
.await;
let contact = alice.add_or_lookup_contact(&bob2).await;
assert_eq!(contact.is_verified(&alice).await.unwrap(), false);
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&alice, &bob).await;
}
tcm.section("Bob sends another message from DC");
@@ -79,7 +77,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email:
tcm.send_recv(&bob, &alice, "Using DC again").await;
// Bob's chat is marked as verified again
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&alice, &bob).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -91,21 +89,17 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
tcm.execute_securejoin(&alice, &bob).await;
tcm.execute_securejoin(&bob, &fiona).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
assert_verified(&bob, &fiona, ProtectionStatus::Protected).await;
assert_verified(&fiona, &bob, ProtectionStatus::Protected).await;
assert_verified(&alice, &bob).await;
assert_verified(&bob, &alice).await;
assert_verified(&bob, &fiona).await;
assert_verified(&fiona, &bob).await;
let group_id = bob
.create_group_with_members(
ProtectionStatus::Protected,
"Group with everyone",
&[&alice, &fiona],
)
.create_group_with_members("Group with everyone", &[&alice, &fiona])
.await;
assert_eq!(
get_chat_msg(&bob, group_id, 0, 1).await.get_info_type(),
SystemMessage::ChatProtectionEnabled
SystemMessage::ChatE2ee
);
{
@@ -117,7 +111,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
get_chat_msg(&fiona, msg.chat_id, 0, 2)
.await
.get_info_type(),
SystemMessage::ChatProtectionEnabled
SystemMessage::ChatE2ee
);
}
@@ -125,26 +119,6 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await;
assert!(alice_fiona_contact.is_verified(&alice).await.unwrap(),);
// Alice should have a hidden protected chat with Fiona
{
let chat = alice.get_chat(&fiona).await;
assert!(chat.is_protected());
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
let expected_text = stock_str::messages_e2e_encrypted(&alice).await;
assert_eq!(msg.text, expected_text);
}
// Fiona should have a hidden protected chat with Alice
{
let chat = fiona.get_chat(&alice).await;
assert!(chat.is_protected());
let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await;
let expected_text = stock_str::messages_e2e_encrypted(&fiona).await;
assert_eq!(msg0.text, expected_text);
}
tcm.section("Fiona reinstalls DC");
drop(fiona);
@@ -155,19 +129,12 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
tcm.send_recv(&fiona_new, &alice, "I have a new device")
.await;
// Alice gets a new unprotected chat with new Fiona contact.
// Alice gets a new chat with new Fiona contact.
{
let chat = alice.get_chat(&fiona_new).await;
assert!(!chat.is_protected());
let msg = get_chat_msg(&alice, chat.id, 1, E2EE_INFO_MSGS + 1).await;
assert_eq!(msg.text, "I have a new device");
// After recreating the chat, it should still be unprotected
chat.id.delete(&alice).await?;
let chat = alice.create_chat(&fiona_new).await;
assert!(!chat.is_protected());
}
Ok(())
@@ -180,7 +147,7 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
let bob = &tcm.bob().await;
let chat_id = tcm.execute_securejoin(bob, alice).await;
let chat = Chat::load_from_db(bob, chat_id).await?;
assert!(chat.is_protected());
assert!(chat.can_send(bob).await?);
bob.sql
.execute(
"DELETE FROM public_keys WHERE fingerprint=?",
@@ -191,43 +158,12 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
.hex(),),
)
.await?;
let chat = Chat::load_from_db(bob, chat_id).await?;
assert!(!chat.can_send(bob).await?);
let chat_id = tcm.execute_securejoin(bob, alice).await;
let chat = Chat::load_from_db(bob, chat_id).await?;
assert!(chat.is_protected());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_unverified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// A chat with an unknown contact should be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2@example.org>\n\
\n\
hello\n",
false,
)
.await?;
chat.id.delete(&alice).await.unwrap();
// Now Bob is a known contact, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
tcm.send_recv(&bob, &alice, "hi").await;
chat.id.delete(&alice).await.unwrap();
// Now we have a public key, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(chat.can_send(bob).await?);
Ok(())
}
@@ -245,7 +181,6 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
mark_as_verified(&alice, &bob).await;
let alice_chat = alice.create_chat(&bob).await;
assert!(alice_chat.is_protected());
receive_imf(
&alice,
@@ -261,7 +196,7 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 1).await;
let enabled = stock_str::messages_e2e_encrypted(&alice).await;
assert_eq!(msg0.text, enabled);
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatE2ee);
let email_chat = alice.get_email_chat(&bob).await;
assert!(!email_chat.is_encrypted(&alice).await?);
@@ -369,7 +304,7 @@ async fn test_mdn_doesnt_disable_verification() -> Result<()> {
let body = rendered_msg.message;
receive_imf(&alice, body.as_bytes(), false).await.unwrap();
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&alice, &bob).await;
Ok(())
}
@@ -384,7 +319,7 @@ async fn test_outgoing_mua_msg() -> Result<()> {
mark_as_verified(&bob, &alice).await;
tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&alice, &bob).await;
let sent = receive_imf(
&alice,
@@ -497,7 +432,7 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
mark_as_verified(bob, alice).await;
tcm.send_recv(bob, alice, "Now i have it!").await;
assert_verified(alice, bob, ProtectionStatus::Protected).await;
assert_verified(alice, bob).await;
let msg = alice.recv_msg(&sent_old).await;
assert!(msg.get_showpadlock());
@@ -506,8 +441,6 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
// The outdated Bob's Autocrypt header isn't applied
// and the message goes to another chat, so the verification preserves.
assert!(contact.is_verified(alice).await.unwrap());
let chat = alice.get_chat(bob).await;
assert!(chat.is_protected());
Ok(())
}
@@ -528,7 +461,7 @@ async fn test_verify_then_verify_again() -> Result<()> {
mark_as_verified(&bob, &alice).await;
alice.create_chat(&bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&alice, &bob).await;
tcm.section("Bob reinstalls DC");
drop(bob);
@@ -537,42 +470,39 @@ async fn test_verify_then_verify_again() -> Result<()> {
e2ee::ensure_secret_key_exists(&bob_new).await?;
tcm.execute_securejoin(&bob_new, &alice).await;
assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await;
assert_verified(&alice, &bob_new).await;
Ok(())
}
/// Tests that on the second device of a protected group creator the first message is
/// `SystemMessage::ChatProtectionEnabled` and the second one is the message populating the group.
/// Tests that on the second device of a group creator the first message is
/// `SystemMessage::ChatE2ee` and the second one is the message populating the group.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_protected_grp_multidev() -> Result<()> {
async fn test_create_grp_multidev() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice1 = &tcm.alice().await;
let group_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[])
.await;
let group_id = alice.create_group_with_members("Group", &[]).await;
assert_eq!(
get_chat_msg(alice, group_id, 0, 1).await.get_info_type(),
SystemMessage::ChatProtectionEnabled
SystemMessage::ChatE2ee
);
let sent = alice.send_text(group_id, "Hey").await;
// This time shift is necessary to reproduce the bug when the original message is sorted over
// the "protection enabled" message so that these messages have different timestamps.
// the "Messages are end-to-end encrypted" message so that these messages have different timestamps.
SystemTime::shift(std::time::Duration::from_secs(3600));
let msg = alice1.recv_msg(&sent).await;
let group1 = Chat::load_from_db(alice1, msg.chat_id).await?;
assert_eq!(group1.get_type(), Chattype::Group);
assert!(group1.is_protected());
assert_eq!(
chat::get_chat_contacts(alice1, group1.id).await?,
vec![ContactId::SELF]
);
assert_eq!(
get_chat_msg(alice1, group1.id, 0, 2).await.get_info_type(),
SystemMessage::ChatProtectionEnabled
SystemMessage::ChatE2ee
);
assert_eq!(get_chat_msg(alice1, group1.id, 1, 2).await.id, msg.id);
@@ -592,10 +522,8 @@ async fn test_verified_member_added_reordering() -> Result<()> {
tcm.execute_securejoin(bob, alice).await;
tcm.execute_securejoin(fiona, alice).await;
// Alice creates protected group with Bob.
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
.await;
// Alice creates a group with Bob.
let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await;
let alice_sent_group_promotion = alice.send_text(alice_chat_id, "I created a group").await;
let msg = bob.recv_msg(&alice_sent_group_promotion).await;
let bob_chat_id = msg.chat_id;
@@ -614,15 +542,13 @@ async fn test_verified_member_added_reordering() -> Result<()> {
// "Member added" message, so unverified group is created.
let fiona_received_message = fiona.recv_msg(&bob_sent_message).await;
let fiona_chat = Chat::load_from_db(fiona, fiona_received_message.chat_id).await?;
assert!(!fiona_chat.can_send(fiona).await?);
assert_eq!(fiona_received_message.get_text(), "Hi");
assert_eq!(fiona_chat.is_protected(), false);
// Fiona receives late "Member added" message
// and the chat becomes protected.
fiona.recv_msg(&alice_sent_member_added).await;
let fiona_chat = Chat::load_from_db(fiona, fiona_received_message.chat_id).await?;
assert_eq!(fiona_chat.is_protected(), true);
Ok(())
}
@@ -665,9 +591,7 @@ async fn test_verified_lost_member_added() -> Result<()> {
tcm.execute_securejoin(bob, alice).await;
tcm.execute_securejoin(fiona, alice).await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
.await;
let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await;
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
assert_eq!(chat::get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
@@ -728,9 +652,7 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
tcm.execute_securejoin(alice, bob).await;
tcm.section("Alice creates a protected group with Bob");
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
.await;
let alice_chat_id = alice.create_group_with_members("Group", &[bob]).await;
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
@@ -811,7 +733,7 @@ async fn test_no_reverification() -> Result<()> {
tcm.section("Alice creates a protected group with Bob, Charlie and Fiona");
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob, charlie, fiona])
.create_group_with_members("Group", &[bob, charlie, fiona])
.await;
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_rcvd_msg = bob.recv_msg(&alice_sent).await;
@@ -859,15 +781,47 @@ async fn test_no_reverification() -> Result<()> {
Ok(())
}
/// Tests that if our second device observes
/// us gossiping a verification,
/// it is not treated as direct verification.
///
/// Direct verifications should only happen
/// as a result of SecureJoin.
/// If we see our second device gossiping
/// a verification of some contact,
/// it may be indirect verification,
/// so we should mark the contact as verified,
/// but with unknown verifier.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_direct_verification_via_bcc() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let bob = &tcm.bob().await;
mark_as_verified(alice, bob).await;
let alice_chat_id = alice.create_chat_id(bob).await;
let alice_sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
alice2.recv_msg(&alice_sent_msg).await;
// Alice 2 observes Alice 1 gossiping verification for Bob.
// Alice 2 does not know if Alice 1 has verified Bob directly though.
let alice2_bob_contact = alice2.add_or_lookup_contact(bob).await;
assert_eq!(alice2_bob_contact.is_verified(alice2).await?, true);
// There is some verifier, but it is unknown to Alice's second device.
assert_eq!(
alice2_bob_contact.get_verifier_id(alice2).await?,
Some(None)
);
Ok(())
}
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
async fn assert_verified(this: &TestContext, other: &TestContext) {
let contact = this.add_or_lookup_contact(other).await;
assert_eq!(contact.is_verified(this).await.unwrap(), true);
let chat = this.get_chat(other).await;
assert_eq!(
chat.is_protected(),
protected == ProtectionStatus::Protected
);
}

View File

@@ -28,12 +28,13 @@ pub async fn save(
namespace: Namespace,
foreign_key: Option<&str>,
token: &str,
timestamp: i64,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO tokens (namespc, foreign_key, token, timestamp) VALUES (?, ?, ?, ?)",
(namespace, foreign_key.unwrap_or(""), token, time()),
(namespace, foreign_key.unwrap_or(""), token, timestamp),
)
.await?;
Ok(())
@@ -71,7 +72,8 @@ pub async fn lookup_or_new(
}
let token = create_id();
save(context, namespace, foreign_key, &token).await?;
let timestamp = time();
save(context, namespace, foreign_key, &token, timestamp).await?;
Ok(token)
}
@@ -86,24 +88,6 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> Res
Ok(exists)
}
/// Looks up foreign key by auth token.
///
/// Returns None if auth token is not valid.
/// Returns an empty string if the token corresponds to "setup contact" rather than group join.
pub async fn auth_foreign_key(context: &Context, token: &str) -> Result<Option<String>> {
context
.sql
.query_row_optional(
"SELECT foreign_key FROM tokens WHERE namespc=? AND token=?",
(Namespace::Auth, token),
|row| {
let foreign_key: String = row.get(0)?;
Ok(foreign_key)
},
)
.await
}
/// Resets all tokens corresponding to the `foreign_key`.
///
/// `foreign_key` is a group ID to reset all group tokens

View File

@@ -169,7 +169,7 @@ pub(crate) async fn intercept_get_updates(
#[cfg(test)]
mod tests {
use crate::chat::{ChatId, ProtectionStatus, create_group_chat};
use crate::chat::{ChatId, create_group};
use crate::chatlist::Chatlist;
use crate::contact::Contact;
use crate::message::Message;
@@ -231,7 +231,7 @@ mod tests {
assert_eq!(msg.chat_id, bob_chat_id);
// Integrate Webxdc into another group
let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let group_id = create_group(&t, "foo").await?;
let integration_id = t.init_webxdc_integration(Some(group_id)).await?.unwrap();
let locations = location::get_range(&t, Some(group_id), None, 0, 0).await?;

View File

@@ -5,8 +5,8 @@ use serde_json::json;
use super::*;
use crate::chat::{
ChatId, ProtectionStatus, add_contact_to_chat, create_broadcast, create_group_chat,
forward_msgs, remove_contact_from_chat, resend_msgs, send_msg, send_text_msg,
ChatId, add_contact_to_chat, create_broadcast, create_group, forward_msgs,
remove_contact_from_chat, resend_msgs, send_msg, send_text_msg,
};
use crate::chatlist::Chatlist;
use crate::config::Config;
@@ -78,7 +78,7 @@ async fn send_webxdc_instance(t: &TestContext, chat_id: ChatId) -> Result<Messag
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_webxdc_instance() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
// send as .xdc file
let instance = send_webxdc_instance(&t, chat_id).await?;
@@ -97,7 +97,7 @@ async fn test_send_webxdc_instance() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_invalid_webxdc() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
// sending invalid .xdc as file is possible, but must not result in Viewtype::Webxdc
let mut instance = create_webxdc_instance(
@@ -126,7 +126,7 @@ async fn test_send_invalid_webxdc() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_draft_invalid_webxdc() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let mut instance = create_webxdc_instance(
&t,
@@ -143,7 +143,7 @@ async fn test_set_draft_invalid_webxdc() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_special_webxdc_format() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
// chess.xdc is failing for some zip-versions, see #3476, if we know more details about why, we can have a nicer name for the test :)
let mut instance = create_webxdc_instance(
@@ -164,7 +164,7 @@ async fn test_send_special_webxdc_format() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_webxdc_instance() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
t.send_webxdc_status_update(
instance.id,
@@ -213,7 +213,7 @@ async fn test_resend_webxdc_instance_and_info() -> Result<()> {
// Alice uses webxdc in a group
alice.set_config_bool(Config::BccSelf, false).await?;
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
let alice_grp = create_group(&alice, "grp").await?;
let alice_instance = send_webxdc_instance(&alice, alice_grp).await?;
assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 2);
alice
@@ -395,7 +395,7 @@ async fn test_webxdc_update_for_not_downloaded_instance() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_webxdc_instance() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
let now = tools::time();
t.receive_status_update(
@@ -428,7 +428,7 @@ async fn test_delete_webxdc_instance() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_chat_with_webxdc() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
let now = tools::time();
t.receive_status_update(
@@ -461,7 +461,7 @@ async fn test_delete_chat_with_webxdc() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_webxdc_draft() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let mut instance = create_webxdc_instance(
&t,
@@ -498,7 +498,7 @@ async fn test_delete_webxdc_draft() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_status_update_record() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
assert_eq!(
@@ -633,7 +633,7 @@ async fn test_create_status_update_record() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receive_status_update() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
let now = tools::time();
@@ -904,7 +904,7 @@ async fn test_send_big_webxdc_status_update() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_webxdc_status_update_object() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
let chat_id = create_group(&t, "a chat").await?;
let mut instance = create_webxdc_instance(
&t,
"minimal.xdc",
@@ -932,7 +932,7 @@ async fn test_render_webxdc_status_update_object() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_webxdc_status_update_object_range() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
let chat_id = create_group(&t, "a chat").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
t.send_webxdc_status_update(instance.id, r#"{"payload": 1}"#)
.await?;
@@ -979,7 +979,7 @@ async fn test_render_webxdc_status_update_object_range() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pop_status_update() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
let chat_id = create_group(&t, "a chat").await?;
let instance1 = send_webxdc_instance(&t, chat_id).await?;
let instance2 = send_webxdc_instance(&t, chat_id).await?;
let instance3 = send_webxdc_instance(&t, chat_id).await?;
@@ -1109,7 +1109,7 @@ async fn test_draft_and_send_webxdc_status_update() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_webxdc_status_update_to_non_webxdc() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let msg_id = send_text_msg(&t, chat_id, "ho!".to_string()).await?;
assert!(
t.send_webxdc_status_update(msg_id, r#"{"foo":"bar"}"#)
@@ -1122,7 +1122,7 @@ async fn test_send_webxdc_status_update_to_non_webxdc() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_webxdc_blob() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
let buf = instance.get_webxdc_blob(&t, "index.html").await?;
@@ -1141,7 +1141,7 @@ async fn test_get_webxdc_blob() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_webxdc_blob_default_icon() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
let buf = instance.get_webxdc_blob(&t, WEBXDC_DEFAULT_ICON).await?;
@@ -1153,7 +1153,7 @@ async fn test_get_webxdc_blob_default_icon() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_webxdc_blob_with_absolute_paths() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
let buf = instance.get_webxdc_blob(&t, "/index.html").await?;
@@ -1166,7 +1166,7 @@ async fn test_get_webxdc_blob_with_absolute_paths() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_webxdc_blob_with_subdirs() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let mut instance = create_webxdc_instance(
&t,
"some-files.xdc",
@@ -1265,7 +1265,7 @@ async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_min_api_too_large() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
let chat_id = create_group(&t, "chat").await?;
let mut instance = create_webxdc_instance(
&t,
"with-min-api-1001.xdc",
@@ -1283,7 +1283,7 @@ async fn test_webxdc_min_api_too_large() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_webxdc_info() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
let info = instance.get_webxdc_info(&t).await?;
@@ -1363,7 +1363,7 @@ async fn test_get_webxdc_info() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_webxdc_self_addr() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&t, "foo").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
let info1 = instance.get_webxdc_info(&t).await?;
@@ -1601,7 +1601,7 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_info_msg_no_cleanup_on_interrupted_series() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "c").await?;
let chat_id = create_group(&t, "c").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
t.send_webxdc_status_update(instance.id, r#"{"info":"i1", "payload":1}"#)
@@ -1623,7 +1623,7 @@ async fn test_webxdc_no_internet_access() -> Result<()> {
let t = TestContext::new_alice().await;
let self_id = t.get_self_chat().await.id;
let single_id = t.create_chat_with_contact("bob", "bob@e.com").await.id;
let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
let group_id = create_group(&t, "chat").await?;
let broadcast_id = create_broadcast(&t, "Channel".to_string()).await?;
for chat_id in [self_id, single_id, group_id, broadcast_id] {
@@ -1655,7 +1655,7 @@ async fn test_webxdc_no_internet_access() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_chatlist_summary() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
let chat_id = create_group(&t, "chat").await?;
let mut instance = create_webxdc_instance(
&t,
"with-minimal-manifest.xdc",
@@ -1727,7 +1727,7 @@ async fn test_webxdc_reject_updates_from_non_groupmembers() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let contact_bob = alice.add_or_lookup_contact_id(bob).await;
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let chat_id = create_group(alice, "Group").await?;
add_contact_to_chat(alice, chat_id, contact_bob).await?;
let instance = send_webxdc_instance(alice, chat_id).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
@@ -1758,7 +1758,7 @@ async fn test_webxdc_reject_updates_from_non_groupmembers() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_delete_event() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let chat_id = create_group(&alice, "foo").await?;
let instance = send_webxdc_instance(&alice, chat_id).await?;
message::delete_msgs(&alice, &[instance.id]).await?;
alice
@@ -1912,7 +1912,7 @@ async fn test_webxdc_notify_one() -> Result<()> {
let fiona = tcm.fiona().await;
let grp_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona])
.create_group_with_members("grp", &[&bob, &fiona])
.await;
let alice_instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
@@ -1958,7 +1958,7 @@ async fn test_webxdc_notify_multiple() -> Result<()> {
let fiona = tcm.fiona().await;
let grp_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona])
.create_group_with_members("grp", &[&bob, &fiona])
.await;
let alice_instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
@@ -2001,9 +2001,7 @@ async fn test_webxdc_no_notify_self() -> Result<()> {
let alice = tcm.alice().await;
let alice2 = tcm.alice().await;
let grp_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[])
.await;
let grp_id = alice.create_group_with_members("grp", &[]).await;
let alice_instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
let alice2_instance = alice2.recv_msg(&sent1).await;
@@ -2043,7 +2041,7 @@ async fn test_webxdc_notify_all() -> Result<()> {
let fiona = tcm.fiona().await;
let grp_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona])
.create_group_with_members("grp", &[&bob, &fiona])
.await;
let alice_instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
@@ -2083,7 +2081,7 @@ async fn test_webxdc_notify_bob_and_all() -> Result<()> {
let fiona = tcm.fiona().await;
let grp_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona])
.create_group_with_members("grp", &[&bob, &fiona])
.await;
let alice_instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
@@ -2117,7 +2115,7 @@ async fn test_webxdc_notify_all_and_bob() -> Result<()> {
let fiona = tcm.fiona().await;
let grp_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona])
.create_group_with_members("grp", &[&bob, &fiona])
.await;
let alice_instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
@@ -2149,9 +2147,7 @@ async fn test_webxdc_href() -> Result<()> {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let grp_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob])
.await;
let grp_id = alice.create_group_with_members("grp", &[&bob]).await;
let instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
@@ -2181,9 +2177,7 @@ async fn test_webxdc_href() -> Result<()> {
async fn test_self_addr_consistency() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice_chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "No friends :(", &[])
.await;
let alice_chat = alice.create_group_with_members("No friends :(", &[]).await;
let mut instance = create_webxdc_instance(
alice,
"minimal.xdc",

View File

@@ -1,8 +1,8 @@
Group#Chat#10: Group chat [3 member(s)]
Group#Chat#2001: Group chat [3 member(s)]
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#11🔒: (Contact#Contact#10): Hi! I created a group. [FRESH]
Msg#12🔒: Me (Contact#Contact#Self): You left the group. [INFO] √
Msg#13🔒: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO]
Msg#14🔒: (Contact#Contact#10): What a silence! [FRESH]
Msg#2001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#2002🔒: (Contact#Contact#2001): Hi! I created a group. [FRESH]
Msg#2003🔒: Me (Contact#Contact#Self): You left the group. [INFO] √
Msg#2004🔒: (Contact#Contact#2001): Member charlie@example.net added by alice@example.org. [FRESH][INFO]
Msg#2005🔒: (Contact#Contact#2001): What a silence! [FRESH]
--------------------------------------------------------------------------------

View File

@@ -1,10 +1,10 @@
Group#Chat#10: Group [5 member(s)]
Group#Chat#1001: Group [5 member(s)]
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#11🔒: Me (Contact#Contact#Self): populate √
Msg#12: info (Contact#Contact#Info): Member dom@example.net added. [NOTICED][INFO]
Msg#13: info (Contact#Contact#Info): Member fiona@example.net removed. [NOTICED][INFO]
Msg#14🔒: (Contact#Contact#10): Member elena@example.net added by bob@example.net. [FRESH][INFO]
Msg#15🔒: Me (Contact#Contact#Self): You added member fiona@example.net. [INFO] o
Msg#16🔒: (Contact#Contact#10): Member fiona@example.net removed by bob@example.net. [FRESH][INFO]
Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#1002🔒: Me (Contact#Contact#Self): populate √
Msg#1003: info (Contact#Contact#Info): Member dom@example.net added. [NOTICED][INFO]
Msg#1004: info (Contact#Contact#Info): Member fiona@example.net removed. [NOTICED][INFO]
Msg#1005🔒: (Contact#Contact#1001): Member elena@example.net added by bob@example.net. [FRESH][INFO]
Msg#1006🔒: Me (Contact#Contact#Self): You added member fiona@example.net. [INFO] o
Msg#1007🔒: (Contact#Contact#1001): Member fiona@example.net removed by bob@example.net. [FRESH][INFO]
--------------------------------------------------------------------------------

View File

@@ -1,5 +1,5 @@
Single#Chat#10: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
Single#Chat#1001: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
--------------------------------------------------------------------------------
Msg#10: Me (Contact#Contact#Self): We share this account √
Msg#11: Me (Contact#Contact#Self): I'm Alice too √
Msg#1001: Me (Contact#Contact#Self): We share this account √
Msg#1002: Me (Contact#Contact#Self): I'm Alice too √
--------------------------------------------------------------------------------

View File

@@ -1,5 +1,5 @@
Single#Chat#10: Bob [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
Single#Chat#11001: Bob [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
--------------------------------------------------------------------------------
Msg#10: Me (Contact#Contact#Self): Happy birthday, Bob! √
Msg#11: (Contact#Contact#10): Happy birthday to me, Alice! [FRESH]
Msg#11001: Me (Contact#Contact#Self): Happy birthday, Bob! √
Msg#11002: (Contact#Contact#11001): Happy birthday to me, Alice! [FRESH]
--------------------------------------------------------------------------------

View File

@@ -1,5 +1,5 @@
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️
Single#Chat#1001: bob@example.net [KEY bob@example.net]
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
Msg#11🔒: Me (Contact#Contact#Self): Test This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √
Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#1002🔒: Me (Contact#Contact#Self): Test This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √
--------------------------------------------------------------------------------

View File

@@ -1,4 +1,4 @@
Single#Chat#11: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
Single#Chat#1002: bob@example.net [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
--------------------------------------------------------------------------------
Msg#12: Me (Contact#Contact#Self): One classical MUA message √
Msg#1003: Me (Contact#Contact#Self): One classical MUA message √
--------------------------------------------------------------------------------

View File

@@ -1,6 +1,6 @@
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️
Single#Chat#1001: bob@example.net [KEY bob@example.net]
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH]
Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √
Msg#1001: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#1002🔒: (Contact#Contact#1001): Heyho from DC [FRESH]
Msg#1004🔒: Me (Contact#Contact#Self): Sending with DC again √
--------------------------------------------------------------------------------

View File

@@ -1,9 +1,9 @@
Group#Chat#11: Group [3 member(s)] 🛡️
Group#Chat#6002: Group [3 member(s)]
--------------------------------------------------------------------------------
Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this group.
Msg#6004: info (Contact#Contact#Info): alice@example.org invited you to join this group.
Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
Msg#17: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
Msg#18🔒: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO]
Msg#6006: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
Msg#6003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#6008🔒: (Contact#Contact#6001): Member Me added by alice@example.org. [FRESH][INFO]
--------------------------------------------------------------------------------

View File

@@ -1,11 +1,11 @@
Group#Chat#11: Group [3 member(s)] 🛡️
Group#Chat#3002: Group [3 member(s)]
--------------------------------------------------------------------------------
Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this group.
Msg#3004: info (Contact#Contact#Info): alice@example.org invited you to join this group.
Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
Msg#16🔒: (Contact#Contact#11): [FRESH]
Msg#18: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
Msg#19: info (Contact#Contact#Info): Member bob@example.net added. [NOTICED][INFO]
Msg#20🔒: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO]
Msg#3006: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
Msg#3003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#3008🔒: (Contact#Contact#3002): [FRESH]
Msg#3009: info (Contact#Contact#Info): Member bob@example.net added. [NOTICED][INFO]
Msg#3010🔒: (Contact#Contact#3001): Member Me added by alice@example.org. [FRESH][INFO]
--------------------------------------------------------------------------------

View File

@@ -34,88 +34,112 @@ Content-Transfer-Encoding: 7bit
-----BEGIN PGP MESSAGE-----
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
=a+ak
wV4D5tq63hTeebASAQdAskXUGnR67hXwkJxNWpovE+gx+/9vY9qH9oTfggRP1Dww
jOCvLyKyDd6ezV7lW3hFQ/FxU+7Xjmq02otcwe3kHZrh6jj86vkD44AYFI/GfVO4
wcBMA+PY3JvEjuMiAQf9HCtOIpTv12YXn4U1iRXIc21bPVXrM0/O4Yg4NNCOfIN8
80VL604DOVn3ZTwL9vvM2OJgBL+jlCHPcbdDr0sUR/Zrw7btqSLbNaiAk63RGoWY
koJz+5f5j+1qTYuWJCyhcIFgG+dILfYx602FOnmhMzf0ULlDwW8znhM97fUiO7/W
0PvXCbIOi9VyrJa5sy6wJ0wr3hP4mvlQYHGCCAyObD6+sztCp67FrO8DjHAwvaU0
qTryl18e53nCDcEoiTVuJ8SojvTLle8DvNWqAl2C6ZRoJYzFK4d9poUlE+F5fpwN
vQEPuTgNapP4GRJsa6av7jczWLRDntUewrHk57UFqcHATANSqZ4dqGKuywEH/2ur
FHadl4JTs/FHz+WqtMdlpMU34gBK6kQtj08oyQK1pCAEKoOKLQnz9+rXZqplZu+f
G+JO8nQB6PPfxtes0qiCUIM1JXQsXJOvf1J10ArbUUgNJ+vGIK4naJITa52wXoma
SMQyvgzfawh5UbGK6L9bpd9ZLeGp6PkABLhY6n40ZsLvvzW1NwDhte16a2YbrzyY
4tzZekpuQ1OlbiiXOj/Ialv3yAv/epzHR5D+hf0f0EqKp1nGA3uPsw2XFQPRL7G1
ipQPgD561a5RVooVhJgYZ3kBNIgZ4yKVimpSvFf5Eoa1Dxwgyr3oREqXNIYx1Nc+
fM6Y3rPiRsgLD9YlETvS0FABY4vOs7uUDPTA0fzdgbgRhwSC/uQP9W8ysVGj3los
mrm0ImKY9KW8auPZjZfc0dXx/YsIW3fT2qjM8Mg/zN4Co3NlQdwXmOJLNRhd5p0/
wzyzTSZXmvBsRcC390ddiksdpCAHcmTZSUtoB84DvuZYqR0uEnXWW7LglB+wsEpR
38Ek9x5x9KdneILeWRlETm9FDx0RJ4RaYPRYfeWSOG7nt/gAuoWt5DV7d/FmNPGH
h2PmxvQYc1+eMhGUXry/6WSYgR2ykWCttwmQovsymVWmRZnb1CtRVtMwWVL4pjYG
YKYKfiLFA6jGyOZ7shz00AfzJXqZ9/8w5pZL1QTj1u68ZiIS+JleRaw2h1LOKtyh
TGVVyahDzw/wMNfMhL0VDi1m0x2CmlYfHT1+LoxxLGS1W/vsJivk5X/m5fQ44uXo
JgnMIQ3xtbAobxYkJ2joCbsmgn56ftzn8bV1dvFJw1krCu6FG0bxWOAnbcQQLAtH
XupTtvtJERbMngUxtAkc1XsZmwymBtlUD2XB1ZMI/NCNKpOQTcr6443iVe+irMdS
A2VhrO035q+Gl2G+X2vlJ7FzlMwUF5gTE8MHesq0/w4oNX4Q5cM29BHmn9BeTG2J
oYlyhxKcZrlJmgbnFe1EMZWA1IMmkrdy3thvN2QSqWnKwYrON6ubqiZe9gZDkNB8
mP/oLJbb9iTsJn+U9uCGJsuc0L0OwrH2fDeYoL2Fp3JM0Hh2vvyGjBcWdgdK8Znm
J+B7Ja18+VXeBskOPfA+pDONsx7/1aGrP7ut950Q2c4KWuIZFIjFzJyaQrsnvxCG
lr+uTU2ZcFn1M9ftCX7psnPXMv/M2yEI+nbZcTMx2NDEqHllzBbdrnn4I1zsT08G
q1cjUQse183HNI7h+TBJyjgQpoeosff65mCefapXtRCNoi4xvbf+hZYqDaEXr4C+
r/LkBfQcnophhiHkd0gWq2SQzCNgASk+rCsMwaEprTQwbr8eU9mKYLXSa6/ycWGo
Jfu38ixm475d5G7tzgPSMjbYfC7OLCdoJiwGdX8eyzk1UJm6jPgrfDNLh3/F61jM
Fl+BRodWn6A48+EOGxmPzH3vo15rzoZI6Ors/D6aiZq6Uu9P3Qlr1HnNqDZmRX2S
yE1rRqfrKdRemRydLk3HS8KVRKhRP3iwH5uWpVBGoWAVC73MXb1w0vNQjjaauOBq
ZkHHP2u5x7rcfgKAWOCNZnnQDnM+vjX1dj84QOVXNukYjaYdtAJNL7mc7fUWWmTI
MpeQGd+TWyFA8iWjUgjeJ/Gh6ILGfCAZWWrsWiLDnnlc/g7Wc9pAXVW5hbspI1Pg
yOi4OC2arv4U7ffJRYljBjOEQ8hOY3tP4z8LmtM/QtY+l3+IhmiEZw1xgUZNE6Gk
jSCCFE+p8kD0HYpaU4I8xrmcpIaZ3leWaQebE9wJAqWM3PuKMCLCk9nrZR0GFpy8
uqRquEGwUZvYTqhQQqWvoNn7xq1nLzRgMgmj2uA9w1jdg+9OzvEOK01zEI2cX7EY
HIII+d60JWiYFY6ZMykVcuZOY5ZpDKwdfyqVbzEhnkpp5VP5FGeqoJDDp5gynv0A
Z45yrANigCTfiBmwVQWvkN/U3VgjDLPPDmOCV2MQoUsywCugcDRn5kAZCZEMNiuV
TcRrhtj5wKkzCFjGGJw9iycn6tT+x073fqFnUIKKpyTfL3Pt06KyIv/GCtDhPfRl
5moNZ3S9KBdqqlH3QYmzHwQJcUtBHmuTa9kbh0Q0k3jZBrxAxreA/bqP/szLi4KP
28rvIVikwsg+WPjQcxfEdkwXtcC8tBezZSfvKrSn0ogxv0gUBMaawoz6Apjz3sAY
LDWvRngj2uo+Gp210l2hb6QHWNOSaQeJJ4dXek6vZG3zPukHuvfmBVFLcR3OxKaG
Lg9nPcBz8q5CvRu9LzxWF7XQ/+IvOPoSeDm3k2UoEgz9evAIjGsR4PGOdTck0EFb
WOPqhEWtrxtXRqYIaayTWAwcLGMUqsluGlue7/Ca7WDLWtG7/PxqGa1SciQ7j9LN
jd5r2wOX6gqwv1Gfn+y+zrCZrj/OKrC9LHJMIeRQPBPb88VQmwrqadkCy1KyY09W
ge8nT4e7lYHLCoR0iu1aXb+xFz4E8mF8kVLdfdjNO9pTX6XrMUjfmjZjXGeb06A3
wLLdBOBmskxl8H5XzlRuhhSJG9pqK4lIdkwgMQWoCK5ISjlFz/Yb9X1IQdJvVdHv
SRp96zBKKBBjsOshv5rBtKKwR35piXTroRFstt+gK1it9pO7b5nu2Ea/MXdbPhNL
4H4ViNVBBohEtYFOkMyLkPACeCdkXYnptJ9snCTB3YU0+6CZK9Goevgb5SqX/+dK
uSnY4ul8Pjvn+UBOJ0HVGbbnD7jI0ZpSSwMLrGLE3ON6ejM//NQw5cEO6UcnnKIL
z+SKJ/z0m3J4+qM5+Fcl5kxBanrWQYo90E2JhCnDgu7mTdx4spSdKNbrLVt9Slf0
XFo1G/D1jhJ7QoDFyQ9jCd+WWPTqdF7xJ6BmlJCz5/m5Fs6quQA4tUQg3wGf419+
O1uUT3Jb9/t1Em65m0R8F1fmPoxh0xbHaljG54EXjpwas50zzFb2zXAkysuSLgXS
A7N98VvYg/v9ZMEvK/qXKHrbYFOdpn9Yxdru3fxdEWU3o4pGPHkgS9EkSyZNwS08
Xdr8eoWn9EAAwxMtugxKAGeIplbIoL9LWYwKK82e9SbtaHVw8/Nm30M+Go4NpeYD
YHYJTH5dDCIY/Gj1wFHVu/KBzgAlN1dcnL4b2zjNHaRXtB9BwsHJ5nyt3M4Q7+JZ
BbLubFONAwWFllbw7LWQbHg20/Kru4366493z6W3n0DDODN+UeR/2zKQp6Wo1CRq
GiSvJbKRoIhbx0XMqAUrzDWjDBr7bTfkK9WSqV7/Ue9AGkbKDKfxNysa6enNlnxs
tbDEEqqfpktv9FtSyxLSztTeGTBBltvSRaKhdeQgfeMw6tSMdosa65qDtQbNM1Ex
Y3x+z2vz9X8I/n5VOP+B+XdMEC3dwYc+l3eWJqsVKnEoRd01YMh3PMGVM9NQbcqf
BGP02C23XgAjee0Yz6YQkic3aDgRNV5nh0dMCUzVLHQngMT/+oX4z8j0vEV+MC+l
m3VreBcVXBPOqEJcm24KE8212TTBJCpvUoZ3bRVs4GfdV2mULej/Gc4HddehEwYQ
7Z+gbITkzoWLt7YoRsHDNOgeVIuGA4MQR4qZPabDwFWdTSbY/bm6wKq3hQVU+Xi4
AgW7FBNfTS0Y8Whkm4FdwNa42Hq9iUYVqwCbP9G9duVLDrWdnjuKNcW/Gt6vb0/F
hH7jap28b+dG4Zu6fPD7pUC2LeCTmPrOx1lpDLgA0oA9InWi3pP9tmXtvgbK++L9
BBSroevRpWWFjGDPpEqFYsyzo/JmadZ9wQIVWBANALi65/AmV2T4W3oIQ+MJw8qJ
EU2NRrBvKIhT1cZ/QTGQwsHXyiO+EzRKHUGhdsMufrPDUcQOefXPBD+NfriYEFnn
kpMAS5HeyEj9y/O+iFQ5eYcxLqkpDhKfzwBU7VaUJzeMXc39ek1TQKhzfD8sJw+f
NeprKbRQBj16yES1Ca+VONCcUUmaExqp+wo6MoKGaBd62mGPSh1TepjA2UmWPsHB
NM7tYIahd+eZV3ZjL/cVnL4FK2y2AW4UFQ/aC09oOS5e6K0hnO1kjyg4Z7MrysU2
Lwa2u268Thirs0RGtDESlAXoqICRGteHZ5PP8eVIO9I4GKW26geSHf3esNMAgzGb
hkBpNtQTR8/kInVwy/jL3ITpu6LZOfwzrhoHtN1X1gKWf7a3OuLKnU1cBHH2Tdp5
HDrHqEnztomHbnvlKQyoDji9BXZM9kVfgupwpQIaw9LPMavsZWCAPkAH+kajAaBw
xCd1ZfzD44mIThMAFl7xAWoVW+8tzdKTEIWYP0eNAQvjN4RrGtHUKPhUHP1Lyi8Q
XiziAj+YMLW8VwGUQi1/h/LbBg+bAYQ7P49ktYU0b/ZSL3hrdmUM44jt86TPehjq
IL1K2PUHAbbFrNLVg3qyMyFi7vpnYh88G1KRw1XaF+hSQYY/ykrA2XlgAdZoel/1
2EGT/4VWh2X6tlxkheYCflQ6Wb2LrCgzzZDE7zei6Fm0b/TCHAsH/kYuAT1bIcVs
gAcGjKcxkB0P9HCHGAr7JCcuf+FF3UWxPbYSH5m6LOMBUlVLsI2md5Fo3EcN07at
yu2jyFYnQWW3aohKwerbXsJPy+acShMAnmuIevZ/V6C9HCMCLYyizcrR0VpXQ0aG
IdtCXr+524wVB79JUT4Z6RM6HIZD3Ce4Nmp2AE8R3X26HZvo2HiAHg2jCun1+E54
pOmNWBQXy94R91O+edu0Rl+qqe/BxO0B7QuPlZ5otWvBHUej0CgJmgMls2uvvzmu
3sfpXq/PeiUOeE8EhOlQvtRBxhJxOTjEcO14j4SD+/D5BNlA6wDWUd2TkDDdjuX/
PxlRUCW4sUFNAv1oDyIN8hTnAlR2ugroGHLoJ0DAsormZ6HUeWWtn4QLmPqf2aMv
UXpwo1tBE320fY+RbV1gaNISMGnCsTiXakdjIfPxJjXi7Q2lSrblqf96ZZg0N/Us
uTWSYcym4A6YJTpREf/zikwsodIP27OSnV7c3sQDiKWwTH+jN6z04sx2obOYg2MW
1+VnQh13C8uhgUqWL0yN23ENHdPq5kvqDiU3KM3jWJR6rG+yKUiHvROethNR0DMD
a7UfWDoj0eIkwY8+/JIgevv4aKdN6UM8MG1+w5HGeJDN8Fb/9szitW7K+STjDGUw
QZ9tDknNp3ZMy1x3WJ7r3TVvWejeM4fI5qOvziA3B2AQb4h0DPVEpRKalZ0GQ7c3
yfuM6pzOyy6Wznp0COGVCb2aEIUrgCCYdPLc4Mtc/cgbwI89Q7gbgs31I0WAe9At
Dy9cmFkcjKn+rQPTlPF3WHyoq+3fwlBiy09ejBtNVdTKNzwsz/5kO3j1i3QvovLw
+uvatGwQfjYkbfstgZaXCFHsiojn8kZmzj0pWiifDTZQdHd8WGhn3oPRy1e6TcUK
3YszlkAvJC6aAi1MTRLXCfRjTgKhMMNKimKjtzgq0O+U8k8tp7hgh2/bj9Ro97y/
0YRXyAl4zPQF0EQo98sE8TlzyO9fO2J8rRDFwji7tDw88m0qv/UD/tirFuMES4u+
krqbvo6I9oJxqxGSVZZmxKdcvgrZMU0icT2VGsvNBiTYMRoX/fspAgDGzlOxUn8b
TZFhntnRXF+lGnH45HEn77rtvEc6PNm407gJthbPk8rYChJgABM3gNcA+7DuHFtU
2/HI2knreNAYDJ6XY0dexh2szL6gZOymt7H6twvAcHvHBS3mc0juQ+LxTYFvG46a
uYdTNAI005gFQWLy5hLAUbmCgTBds4ISOIo3Itz8bJcbBiInkwLfGbk6U6NJzNWO
wlfEsJjiDzJDgU5H6bJpH6AQ5JeO1CMWWGO1Yx++4eYL8cbBLYUhLvCFiyyNVQLH
BRCzM2zCASaJb3wPIIKLf+VxNVyv+n5F4KX1tfzjYIsCaNamucVZ38YiK59C5G1M
GfNHsBGTnM74Y5aNaZp7oK1YATVKQnORWZejInbAm6zezofOnn0bDe7a6JGr+rEk
snkgMBPdS6nNmscXx7QDAoQNf/Vjf2DPcPf7YblKtgvyzr9cpUrNf/rfJvzGSqpk
6FTia11h5rMs9bkPOqzhG2DQJX3fEmHHl1ZK0aGwPdXtcxVDuWsbYPEevCjTe24e
NKdUucW9QFBPIrn9ABTLAWDjSiR2FEFP/lq/dU/dZEJoAQzg98F5IdJzbmvjQSL8
9itRo7h+e2Dv7NI9SbkWo+E=
=OL4B
-----END PGP MESSAGE-----