broadcasts (#2707)

* add Chattype::Broadcast and create_broadcast_list()

* do not disclose recipients for broadcasts

* allow sending/add-/remove-member for broadcast

* set broadcast subject same as for one-to-one chats

* broadcast-recipient-list does not include SELF

* use special icon for broadcast groups

* generate initial broadcast names

* make clippy happy

* send BCC message unencrypted to avoid unexpected disclosing; encryption is opportunistic anyway. if we have 'protected chats' at some point, we can think that over.

* reword 'To:'-group

* simplify can-send-check

* add broadcast tests

* tweak comments

* Update deltachat-ffi/deltachat.h

Co-authored-by: Hocuri <hocuri@gmx.de>

* change name of can_edit() to is_self_in_chat()

Co-authored-by: Hocuri <hocuri@gmx.de>
This commit is contained in:
bjoern
2021-09-30 13:56:05 +02:00
committed by GitHub
parent 30a3da97da
commit bcadd0cd5c
13 changed files with 449 additions and 80 deletions

BIN
assets/icon-broadcast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

149
assets/icon-broadcast.svg Normal file
View File

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
enable-background="new 0 0 128 128"
viewBox="0 0 60 60"
version="1.1"
id="svg878"
sodipodi:docname="icon-broadcast.svg"
width="60"
height="60"
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-broadcast.png"
inkscape:export-xdpi="409.60001"
inkscape:export-ydpi="409.60001">
<metadata
id="metadata884">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs882" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1329"
inkscape:window-height="847"
id="namedview880"
showgrid="false"
inkscape:zoom="5.21875"
inkscape:cx="36.598802"
inkscape:cy="32.191617"
inkscape:window-x="111"
inkscape:window-y="205"
inkscape:window-maximized="0"
inkscape:current-layer="svg878"
inkscape:document-rotation="0" />
<radialGradient
id="c"
cx="65.25"
cy="89"
r="26.440001"
gradientTransform="matrix(0.77611266,0.11996647,-0.18999676,1.2286617,-11.305867,-60.065999)"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#FFC107"
offset="0"
id="stop833" />
<stop
stop-color="#FFBD06"
offset=".3502"
id="stop835" />
<stop
stop-color="#FFB104"
offset=".6938"
id="stop837" />
<stop
stop-color="#FFA000"
offset="1"
id="stop839" />
</radialGradient>
<radialGradient
id="b"
cx="52.5"
cy="19.75"
r="92.975998"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(45.323856,68.997115,75.979538)">
<stop
stop-color="#EF5350"
offset="0"
id="stop848" />
<stop
stop-color="#EB4F4C"
offset=".246"
id="stop850" />
<stop
stop-color="#E04341"
offset=".4878"
id="stop852" />
<stop
stop-color="#CD302F"
offset=".7272"
id="stop854" />
<stop
stop-color="#C62828"
offset=".8004"
id="stop856" />
<stop
stop-color="#C62828"
offset="1"
id="stop858" />
</radialGradient>
<radialGradient
id="a"
cx="16.979"
cy="92"
r="24.165001"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(45.323856,68.997115,75.979538)"
xlink:href="#b">
<stop
stop-color="#E0E0E0"
offset="0"
id="stop863" />
<stop
stop-color="#CFCFCF"
offset=".3112"
id="stop865" />
<stop
stop-color="#A4A4A4"
offset=".9228"
id="stop867" />
<stop
stop-color="#9E9E9E"
offset="1"
id="stop869" />
</radialGradient>
<rect
y="0"
x="0"
height="60"
width="60"
id="rect1420"
style="fill:#7cc0bc;fill-opacity:1;stroke:none;stroke-width:1.29077" />
<path
id="path872"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.336872;stroke-opacity:1"
d="m 8.6780027,35.573064 0.032831,-11.910176 c 0.00138,-0.476406 0.4881282,-0.794259 0.9235226,-0.604877 l 4.1144877,2.345752 -0.02386,8.656315 -4.1268029,2.122946 C 9.1617452,36.370003 8.6766889,36.049472 8.6780027,35.573064 Z m 5.0469633,-1.508222 0.02386,-8.656314 31.145424,-9.537653 c 0.841472,-0.219211 1.65915,0.41667 1.656755,1.283728 l -0.06929,25.139995 c -0.0024,0.867062 -0.825942,1.500799 -1.663803,1.274581 z m 3.8042,6.892234 C 16.681121,40.104348 16.315444,38.819414 16.69043,37.591308 l 2.252234,-7.347193 c 0.2644,-0.861571 0.845185,-1.567441 1.641953,-1.989251 0.796769,-0.421808 1.706956,-0.509819 2.568531,-0.245419 l 7.263888,2.225804 c 1.775518,0.543235 2.780299,2.432591 2.232297,4.208094 L 30.3971,41.790532 c -0.545627,1.777887 -2.432591,2.780297 -4.208095,2.232298 l -7.263891,-2.225804 c -0.545033,-0.165864 -1.01825,-0.460162 -1.395948,-0.83995 z m 12.377693,-7.976728 c -0.07601,-0.07642 -0.17114,-0.133864 -0.280621,-0.167516 l -7.263891,-2.225803 c -0.233244,-0.07209 -0.421626,0.0013 -0.512275,0.04861 -0.09064,0.0474 -0.25772,0.166033 -0.327435,0.396899 l -2.252234,7.347191 c -0.108166,0.354628 0.09088,0.731541 0.447888,0.842099 l 7.263891,2.225802 c 0.354626,0.108174 0.731539,-0.09088 0.842099,-0.447888 l 2.249845,-7.344814 c 0.07453,-0.245145 0.0014,-0.504991 -0.167267,-0.67458 z" />
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1299,6 +1299,8 @@ void dc_accept_chat (dc_context_t* context, uint32_t ch
* explicitly as it may happen that oneself gets removed from a still existing
* group
*
* - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
*
* - for mailing lists, the behavior is not documented currently, we will decide on that later.
* for now, the UI should not show the list for mailing lists.
* (we do not know all members and there is not always a global mailing list address,
@@ -1408,6 +1410,36 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
uint32_t dc_create_group_chat (dc_context_t* context, int protect, const char* name);
/**
* Create a new broadcast list.
*
* Broadcast lists are similar to groups on the sending device,
* however, recipients get the messages in normal one-to-one chats
* and will not be aware of other members.
*
* Replies to broadcasts go only to the sender
* and not to all broadcast recipients.
* Moreover, replies will not appear in the broadcast list
* but in the one-to-one chat with the person answering.
*
* The name and the image of the broadcast list is set automatically
* and is visible to the sender only.
* Not asking for these data allows more focused creation
* and we bypass the question who will get which data.
* Also, many users will have at most one broadcast list
* so, a generic name and image is sufficient at the first place.
*
* Later on, however, the name can be changed using dc_set_chat_name().
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
* All in all, this is also what other messengers are doing here.
*
* @memberof dc_context_t
* @param context The context object.
* @return The chat ID of the new broadcast list, 0 on errors.
*/
uint32_t dc_create_broadcast_list (dc_context_t* context);
/**
* Check if a given contact ID is a member of a group chat.
*
@@ -3044,6 +3076,11 @@ uint32_t dc_chat_get_id (const dc_chat_t* chat);
* and cannot be changed using this api.
* moreover, for now, mailist lists are read-only.
*
* - @ref DC_CHAT_TYPE_BROADCAST - a broadcast list,
* the recipients will get messages in a one-to-one chats and
* the sender will get answers in a one-to-one as well.
* chats_contacts contain all recipients but DC_CONTACT_ID_SELF
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return Chat type.
@@ -4744,6 +4781,11 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_CHAT_TYPE_MAILINGLIST 140
/**
* A broadcast list. See dc_chat_get_type() for details.
*/
#define DC_CHAT_TYPE_BROADCAST 160
/**
* @}
*/
@@ -5967,6 +6009,11 @@ void dc_event_unref(dc_event_t* event);
/// Used as a subtitle in quota context; can be plural always.
#define DC_STR_MESSAGES 114
/// "Broadcast List"
///
/// Used as the default name for broadcast lists; a number may be added.
#define DC_STR_BROADCAST_LIST 115
/**
* @}
*/

View File

@@ -1330,6 +1330,19 @@ pub unsafe extern "C" fn dc_create_group_chat(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_create_broadcast_list(context: *mut dc_context_t) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_create_broadcast_list()");
return 0;
}
let ctx = &*context;
block_on(chat::create_broadcast_list(ctx))
.log_err(ctx, "Failed to create broadcast list")
.map(|id| id.to_u32())
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_is_contact_in_chat(
context: *mut dc_context_t,

View File

@@ -370,6 +370,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat [<chat-id>|0]\n\
createchat <contact-id>\n\
creategroup <name>\n\
createbroadcast\n\
createprotected <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
@@ -714,6 +715,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Group#{} created successfully.", chat_id);
}
"createbroadcast" => {
let chat_id = chat::create_broadcast_list(&context).await?;
println!("Broadcast#{} created successfully.", chat_id);
}
"createprotected" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =

View File

@@ -167,13 +167,14 @@ const DB_COMMANDS: [&str; 10] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 33] = [
const CHAT_COMMANDS: [&str; 34] = [
"listchats",
"listarchived",
"chat",
"createchat",
"creategroup",
"createverified",
"createbroadcast",
"createprotected",
"addmember",
"removemember",
"groupname",

View File

@@ -271,7 +271,9 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
Chattype::Undefined => bail!("Can't block chat of undefined chattype"),
Chattype::Undefined | Chattype::Broadcast => {
bail!("Can't block chat of type {:?}", chat.typ)
}
Chattype::Single => {
for contact_id in get_chat_contacts(context, self).await? {
if contact_id != DC_CONTACT_ID_SELF {
@@ -311,7 +313,7 @@ impl ChatId {
match chat.typ {
Chattype::Undefined => bail!("Can't accept chat of undefined chattype"),
Chattype::Single | Chattype::Group => {
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
// User has "created a chat" with all these contacts.
//
// Previously accepting a chat literally created a chat because unaccepted chats
@@ -355,7 +357,7 @@ impl ChatId {
match protect {
ProtectionStatus::Protected => match chat.typ {
Chattype::Single | Chattype::Group => {
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
let contact_ids = get_chat_contacts(context, self).await?;
for contact_id in contact_ids.into_iter() {
let contact = Contact::get_by_id(context, contact_id).await?;
@@ -980,8 +982,18 @@ impl Chat {
&& !self.is_device_talk()
&& !self.is_mailing_list()
&& !self.is_contact_request()
&& (self.typ == Chattype::Single
|| is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await?))
&& self.is_self_in_chat(context).await?)
}
/// Checks if the user is part of a chat
/// and has basically the permissions to edit the chat therefore.
/// The function does not check if the chat type allows editing of concrete elements.
pub(crate) async fn is_self_in_chat(&self, context: &Context) -> Result<bool> {
match self.typ {
Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
Chattype::Group => is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await,
Chattype::Undefined => Ok(false),
}
}
pub async fn update_param(&mut self, context: &Context) -> Result<()> {
@@ -1023,8 +1035,11 @@ impl Chat {
return contact.get_profile_image(context).await;
}
}
} else if self.typ == Chattype::Broadcast {
if let Ok(image_rel) = get_broadcast_icon(context).await {
return Ok(Some(dc_get_abs_path(context, image_rel)));
}
}
Ok(None)
}
@@ -1126,18 +1141,18 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
if !(self.typ == Chattype::Single || self.typ == Chattype::Group) {
error!(context, "Cannot send to chat type #{}.", self.typ,);
bail!("Cannot set to chat type #{}", self.typ);
}
if self.typ == Chattype::Group
&& !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await?
{
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot send message; self not in group.".into(),
));
bail!("Cannot set message; self not in group.");
if !self.can_send(context).await? {
if self.typ == Chattype::Group
&& !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await?
{
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot send message; self not in group.".into(),
));
bail!("Cannot set message; self not in group.");
} else {
error!(context, "Cannot send to chat type #{}.", self.typ,);
bail!("Cannot send to chat type #{}", self.typ);
}
}
let from = context
@@ -1461,6 +1476,21 @@ pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
Ok(())
}
pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<String> {
if let Some(icon) = context.sql.get_raw_config("icon-broadcast").await? {
return Ok(icon);
}
let icon = include_bytes!("../assets/icon-broadcast.png");
let blob = BlobObject::create(context, "icon-broadcast.png", icon).await?;
let icon = blob.as_name().to_string();
context
.sql
.set_raw_config("icon-broadcast", Some(&icon))
.await?;
Ok(icon)
}
async fn update_special_chat_name(context: &Context, contact_id: u32, name: String) -> Result<()> {
if let Some(chat_id) = ChatId::lookup_by_contact(context, contact_id).await? {
// the `!= name` condition avoids unneeded writes
@@ -2221,6 +2251,56 @@ pub async fn create_group_chat(
Ok(chat_id)
}
/// Finds an unused name for a new broadcast list.
async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
let base_name = stock_str::broadcast_list(context).await;
for attempt in 1..1000 {
let better_name = if attempt > 1 {
format!("{} {}", base_name, attempt)
} else {
base_name.clone()
};
if !context
.sql
.exists(
"SELECT COUNT(*) FROM chats WHERE type=? AND name=?;",
paramsv![Chattype::Broadcast, better_name],
)
.await?
{
return Ok(better_name);
}
}
Ok(base_name)
}
/// Creates a new broadcast list.
pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
let chat_name = find_unused_broadcast_list_name(context).await?;
let grpid = dc_create_id();
let row_id = context
.sql
.insert(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
paramsv![
Chattype::Broadcast,
chat_name,
grpid,
dc_create_smeared_timestamp(context).await,
],
)
.await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
context.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
Ok(chat_id)
}
/// Adds a contact to the `chats_contacts` table.
pub(crate) async fn add_to_chat_contacts_table(
context: &Context,
@@ -2278,8 +2358,8 @@ pub(crate) async fn add_contact_to_chat_ex(
/*this also makes sure, not contacts are added to special or normal chats*/
let mut chat = Chat::load_from_db(context, chat_id).await?;
ensure!(
chat.typ == Chattype::Group,
"{} is not a group where one can add members",
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
"{} is not a group/broadcast where one can add members",
chat_id
);
ensure!(
@@ -2288,14 +2368,18 @@ pub(crate) async fn add_contact_to_chat_ex(
contact_id
);
ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
ensure!(
chat.typ != Chattype::Broadcast || contact_id != DC_CONTACT_ID_SELF,
"Cannot add SELF to broadcast."
);
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await? {
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
if !chat.is_self_in_chat(context).await? {
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot add contact to group; self not in group.".into(),
));
bail!("can not add contact because our account is not part of it");
}
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
chat.param.remove(Param::Unpromoted);
chat.update_param(context).await?;
@@ -2334,7 +2418,7 @@ pub(crate) async fn add_contact_to_chat_ex(
}
add_to_chat_contacts_table(context, chat_id, contact_id).await?;
}
if chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 0 {
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
msg.text = Some(
@@ -2499,14 +2583,14 @@ pub async fn remove_contact_from_chat(
/* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
if chat.typ == Chattype::Group {
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await? {
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
if !chat.is_self_in_chat(context).await? {
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot remove contact from chat; self not in group.".into(),
));
} else {
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
if chat.is_promoted() {
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
if contact.id == DC_CONTACT_ID_SELF {
set_group_explicitly_left(context, &chat.grpid).await?;
@@ -2593,48 +2677,47 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
let chat = Chat::load_from_db(context, chat_id).await?;
let mut msg = Message::default();
if chat.typ == Chattype::Group || chat.typ == Chattype::Mailinglist {
if chat.typ == Chattype::Group
|| chat.typ == Chattype::Mailinglist
|| chat.typ == Chattype::Broadcast
{
if chat.name == new_name {
success = true;
} else if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await? {
} else if !chat.is_self_in_chat(context).await? {
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot set chat name; self not in group".into(),
));
} else {
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
if context
context
.sql
.execute(
"UPDATE chats SET name=? WHERE id=?;",
paramsv![new_name.to_string(), chat_id],
)
.await
.is_ok()
{
if chat.is_promoted() && !chat.is_mailing_list() {
msg.viewtype = Viewtype::Text;
msg.text = Some(
stock_str::msg_grp_name(
context,
&chat.name,
&new_name,
DC_CONTACT_ID_SELF as u32,
)
.await,
);
msg.param.set_cmd(SystemMessage::GroupNameChanged);
if !chat.name.is_empty() {
msg.param.set(Param::Arg, &chat.name);
}
msg.id = send_msg(context, chat_id, &mut msg).await?;
context.emit_event(EventType::MsgsChanged {
chat_id,
msg_id: msg.id,
});
.await?;
if chat.is_promoted() && !chat.is_mailing_list() && chat.typ != Chattype::Broadcast {
msg.viewtype = Viewtype::Text;
msg.text = Some(
stock_str::msg_grp_name(
context,
&chat.name,
&new_name,
DC_CONTACT_ID_SELF as u32,
)
.await,
);
msg.param.set_cmd(SystemMessage::GroupNameChanged);
if !chat.name.is_empty() {
msg.param.set(Param::Arg, &chat.name);
}
context.emit_event(EventType::ChatModified(chat_id));
success = true;
msg.id = send_msg(context, chat_id, &mut msg).await?;
context.emit_event(EventType::MsgsChanged {
chat_id,
msg_id: msg.id,
});
}
context.emit_event(EventType::ChatModified(chat_id));
success = true;
}
}
@@ -4243,4 +4326,49 @@ mod tests {
);
Ok(())
}
#[async_std::test]
async fn test_broadcast() -> Result<()> {
// create two context, send two messages so both know the other
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(&bob).await;
send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let chat_bob = bob.create_chat(&alice).await;
send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?;
alice.recv_msg(&bob.pop_sent_msg().await).await;
let msg = alice.get_last_msg().await;
assert!(msg.get_showpadlock());
// test broadcast list
let broadcast_id = create_broadcast_list(&alice).await?;
add_contact_to_chat(
&alice,
broadcast_id,
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
)
.await?;
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
assert_eq!(chat.typ, Chattype::Broadcast);
assert_eq!(chat.name, stock_str::broadcast_list(&alice).await);
assert!(!chat.is_self_talk());
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.chat_id, chat.id);
bob.recv_msg(&alice.pop_sent_msg().await).await;
let msg = bob.get_last_msg().await;
assert_eq!(msg.get_text(), Some("ola!".to_string()));
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Single);
assert_eq!(chat.id, chat_bob.id);
assert!(!chat.is_self_talk());
Ok(())
}
}

View File

@@ -329,7 +329,7 @@ impl Chatlist {
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?;
(Some(lastmsg), Some(lastcontact))
}

View File

@@ -153,6 +153,7 @@ pub enum Chattype {
Single = 100,
Group = 120,
Mailinglist = 140,
Broadcast = 160,
}
impl Default for Chattype {
@@ -348,6 +349,7 @@ mod tests {
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap());
}
#[test]

View File

@@ -604,7 +604,7 @@ impl Message {
let contact = if self.from_id != DC_CONTACT_ID_SELF {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
Some(Contact::get_by_id(context, self.from_id).await?)
}
Chattype::Single | Chattype::Undefined => None,
@@ -1599,7 +1599,7 @@ async fn ndn_maybe_add_info_msg(
chat_type: Chattype,
) -> Result<()> {
match chat_type {
Chattype::Group => {
Chattype::Group | Chattype::Broadcast => {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)

View File

@@ -316,6 +316,10 @@ impl<'a> MimeFactory<'a> {
Loaded::Message { chat } => {
if chat.is_protected() {
false
} else if chat.typ == Chattype::Broadcast {
// encryption may disclose recipients;
// this is probably a worse issue than not opportunistically (!) encrypting
true
} else {
self.msg
.param
@@ -393,8 +397,6 @@ impl<'a> MimeFactory<'a> {
}
if chat.typ == Chattype::Group && quoted_msg_subject.is_none_or_empty() {
// If we have a `quoted_msg_subject`, we use the subject of the quoted message
// instead of the group name
let re = if self.in_reply_to.is_empty() {
""
} else {
@@ -403,22 +405,22 @@ impl<'a> MimeFactory<'a> {
return Ok(format!("{}{}", re, chat.name));
}
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
chat.param.get(Param::LastSubject)
} else {
quoted_msg_subject.as_deref()
};
if let Some(last_subject) = parent_subject {
format!("Re: {}", remove_subject_prefix(last_subject))
} else {
let self_name = match context.get_config(Config::Displayname).await? {
Some(name) => name,
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
if chat.typ != Chattype::Broadcast {
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
chat.param.get(Param::LastSubject)
} else {
quoted_msg_subject.as_deref()
};
stock_str::subject_for_new_contact(context, self_name).await
if let Some(last_subject) = parent_subject {
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
}
}
let self_name = match context.get_config(Config::Displayname).await? {
Some(name) => name,
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
};
stock_str::subject_for_new_contact(context, self_name).await
}
Loaded::Mdn { .. } => stock_str::read_rcpt(context).await,
};
@@ -555,9 +557,21 @@ impl<'a> MimeFactory<'a> {
render_rfc724_mid(&rfc724_mid),
));
headers
.unprotected
.push(Header::new_with_value("To".into(), to).unwrap());
let undisclosed_recipients = match &self.loaded {
Loaded::Message { chat } => chat.typ == Chattype::Broadcast,
Loaded::Mdn { .. } => false,
};
if undisclosed_recipients {
headers
.unprotected
.push(Header::new("To".into(), "hidden-recipients: ;".to_string()));
} else {
headers
.unprotected
.push(Header::new_with_value("To".into(), to).unwrap());
}
headers
.unprotected
.push(Header::new_with_value("From".into(), vec![from]).unwrap());

View File

@@ -311,6 +311,9 @@ pub enum StockMessage {
#[strum(props(fallback = "Messages"))]
Messages = 114,
#[strum(props(fallback = "Broadcast List"))]
BroadcastList = 115,
}
impl StockMessage {
@@ -985,6 +988,12 @@ pub(crate) async fn messages(context: &Context) -> String {
translated(context, StockMessage::Messages).await
}
/// Stock string: `Broadcast List`.
/// Used as the default name for broadcast lists; a number may be added.
pub(crate) async fn broadcast_list(context: &Context) -> String {
translated(context, StockMessage::BroadcastList).await
}
impl Context {
/// Set the stock string for the [StockMessage].
///

View File

@@ -72,7 +72,7 @@ impl Summary {
}
} else {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
if msg.is_info() || contact.is_none() {
None
} else {