diff --git a/assets/icon-broadcast.png b/assets/icon-broadcast.png
new file mode 100644
index 000000000..aa761885e
Binary files /dev/null and b/assets/icon-broadcast.png differ
diff --git a/assets/icon-broadcast.svg b/assets/icon-broadcast.svg
new file mode 100644
index 000000000..733e48c1d
--- /dev/null
+++ b/assets/icon-broadcast.svg
@@ -0,0 +1,149 @@
+
+
diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h
index 9848fc70d..7ea527518 100644
--- a/deltachat-ffi/deltachat.h
+++ b/deltachat-ffi/deltachat.h
@@ -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
+
/**
* @}
*/
diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs
index e66c5da30..c000a174c 100644
--- a/deltachat-ffi/src/lib.rs
+++ b/deltachat-ffi/src/lib.rs
@@ -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,
diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs
index 23ea29b2b..4556e84db 100644
--- a/examples/repl/cmdline.rs
+++ b/examples/repl/cmdline.rs
@@ -370,6 +370,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat [|0]\n\
createchat \n\
creategroup \n\
+ createbroadcast\n\
createprotected \n\
addmember \n\
removemember \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 missing.");
let chat_id =
diff --git a/examples/repl/main.rs b/examples/repl/main.rs
index 6c7622419..da6f4e957 100644
--- a/examples/repl/main.rs
+++ b/examples/repl/main.rs
@@ -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",
diff --git a/src/chat.rs b/src/chat.rs
index 5e974633e..b3f2fcc5e 100644
--- a/src/chat.rs
+++ b/src/chat.rs
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ 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(())
+ }
}
diff --git a/src/chatlist.rs b/src/chatlist.rs
index c33ff8df7..a8cd888e7 100644
--- a/src/chatlist.rs
+++ b/src/chatlist.rs
@@ -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))
}
diff --git a/src/constants.rs b/src/constants.rs
index 5f1e990ba..108351cc0 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -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]
diff --git a/src/message.rs b/src/message.rs
index 3674cb969..c7ebeca5e 100644
--- a/src/message.rs
+++ b/src/message.rs
@@ -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)
diff --git a/src/mimefactory.rs b/src/mimefactory.rs
index 79c1fb926..210de7eb0 100644
--- a/src/mimefactory.rs
+++ b/src/mimefactory.rs
@@ -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());
diff --git a/src/stock_str.rs b/src/stock_str.rs
index 27fe437e3..e9fd1e621 100644
--- a/src/stock_str.rs
+++ b/src/stock_str.rs
@@ -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].
///
diff --git a/src/summary.rs b/src/summary.rs
index e6fe559d3..885ff2a85 100644
--- a/src/summary.rs
+++ b/src/summary.rs
@@ -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 {