Skip to content

Commit 83ad84f

Browse files
committed
feat: Display vCard contact name in the message summary
1 parent 889b947 commit 83ad84f

File tree

8 files changed

+124
-58
lines changed

8 files changed

+124
-58
lines changed

deltachat-ffi/deltachat.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7366,7 +7366,7 @@ void dc_event_unref(dc_event_t* event);
73667366
/// Used as info message.
73677367
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
73687368

7369-
/// "Contact"
7369+
/// "Contact". Deprecated, currently unused.
73707370
#define DC_STR_CONTACT 200
73717371

73727372
/**

src/chat.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ use crate::tools::{
4949
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
5050
smeared_time, time, IsNoneOrEmpty, SystemTime,
5151
};
52-
use crate::webxdc::WEBXDC_SUFFIX;
5352

5453
/// An chat item, such as a message or a marker.
5554
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -894,8 +893,16 @@ impl ChatId {
894893
.await?
895894
.context("no file stored in params")?;
896895
msg.param.set(Param::File, blob.as_name());
897-
if blob.suffix() == Some(WEBXDC_SUFFIX) {
898-
msg.viewtype = Viewtype::Webxdc;
896+
if msg.viewtype == Viewtype::File {
897+
if let Some((better_type, _)) =
898+
message::guess_msgtype_from_suffix(&blob.to_abs_path())
899+
.filter(|&(vt, _)| vt == Viewtype::Webxdc || vt == Viewtype::Vcard)
900+
{
901+
msg.viewtype = better_type;
902+
}
903+
}
904+
if msg.viewtype == Viewtype::Vcard {
905+
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
899906
}
900907
}
901908
}
@@ -2649,6 +2656,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
26492656
.await?;
26502657
}
26512658

2659+
if msg.viewtype == Viewtype::Vcard {
2660+
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
2661+
}
2662+
26522663
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
26532664
if !send_as_is
26542665
&& (msg.viewtype == Viewtype::Image

src/message.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use std::collections::BTreeSet;
44
use std::path::{Path, PathBuf};
5+
use std::str;
56

67
use anyhow::{ensure, format_err, Context as _, Result};
78
use deltachat_contact_tools::{parse_vcard, VcardContact};
@@ -1093,6 +1094,18 @@ impl Message {
10931094
.await
10941095
}
10951096

1097+
/// Updates message state from the vCard attachment.
1098+
pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> {
1099+
let vcard = fs::read(path).await.context("Could not read {path}")?;
1100+
if let Some(summary) = get_vcard_summary(&vcard) {
1101+
self.param.set(Param::Summary1, summary);
1102+
} else {
1103+
warn!(context, "try_set_vcard: Not a valid DeltaChat vCard.");
1104+
self.viewtype = Viewtype::File;
1105+
}
1106+
Ok(())
1107+
}
1108+
10961109
/// Set different sender name for a message.
10971110
/// This overrides the name set by the `set_config()`-option `displayname`.
10981111
pub fn set_override_sender_name(&mut self, name: Option<String>) {
@@ -1938,6 +1951,19 @@ pub(crate) async fn get_latest_by_rfc724_mids(
19381951
Ok(None)
19391952
}
19401953

1954+
/// Returns the 1st part of summary text (i.e. before the dash if any) for a valid DeltaChat vCard.
1955+
pub(crate) fn get_vcard_summary(vcard: &[u8]) -> Option<String> {
1956+
let vcard = str::from_utf8(vcard).ok()?;
1957+
let contacts = deltachat_contact_tools::parse_vcard(vcard);
1958+
let [c] = &contacts[..] else {
1959+
return None;
1960+
};
1961+
if !deltachat_contact_tools::may_be_valid_addr(&c.addr) {
1962+
return None;
1963+
}
1964+
Some(c.display_name().to_string())
1965+
}
1966+
19411967
/// How a message is primarily displayed.
19421968
#[derive(
19431969
Debug,

src/mimeparser.rs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ use crate::events::EventType;
2828
use crate::headerdef::{HeaderDef, HeaderDefMap};
2929
use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
3030
use crate::message::{
31-
self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype,
31+
self, get_vcard_summary, set_msg_failed, update_msg_state, Message, MessageState, MsgId,
32+
Viewtype,
3233
};
3334
use crate::param::{Param, Params};
3435
use crate::peerstate::Peerstate;
@@ -1233,6 +1234,7 @@ impl MimeMessage {
12331234
return Ok(());
12341235
}
12351236
}
1237+
let mut param = Params::new();
12361238
let msg_type = if context
12371239
.is_webxdc_file(filename, decoded_data)
12381240
.await
@@ -1276,6 +1278,13 @@ impl MimeMessage {
12761278
.unwrap_or_default();
12771279
self.webxdc_status_update = Some(serialized);
12781280
return Ok(());
1281+
} else if msg_type == Viewtype::Vcard {
1282+
if let Some(summary) = get_vcard_summary(decoded_data) {
1283+
param.set(Param::Summary1, summary);
1284+
msg_type
1285+
} else {
1286+
Viewtype::File
1287+
}
12791288
} else {
12801289
msg_type
12811290
};
@@ -1299,18 +1308,19 @@ impl MimeMessage {
12991308
let mut part = Part::default();
13001309
if mime_type.type_() == mime::IMAGE {
13011310
if let Ok((width, height)) = get_filemeta(decoded_data) {
1302-
part.param.set_int(Param::Width, width as i32);
1303-
part.param.set_int(Param::Height, height as i32);
1311+
param.set_int(Param::Width, width as i32);
1312+
param.set_int(Param::Height, height as i32);
13041313
}
13051314
}
13061315

13071316
part.typ = msg_type;
13081317
part.org_filename = Some(filename.to_string());
13091318
part.mimetype = Some(mime_type);
13101319
part.bytes = decoded_data.len();
1311-
part.param.set(Param::File, blob.as_name());
1312-
part.param.set(Param::Filename, filename);
1313-
part.param.set(Param::MimeType, raw_mime);
1320+
param.set(Param::File, blob.as_name());
1321+
param.set(Param::Filename, filename);
1322+
param.set(Param::MimeType, raw_mime);
1323+
part.param = param;
13141324
part.is_related = is_related;
13151325

13161326
self.do_add_single_part(part);
@@ -1928,7 +1938,10 @@ pub struct Part {
19281938
pub(crate) is_reaction: bool,
19291939
}
19301940

1931-
/// return mimetype and viewtype for a parsed mail
1941+
/// Returns the mimetype and viewtype for a parsed mail.
1942+
///
1943+
/// This only looks at the metadata, not at the content;
1944+
/// the viewtype may later be corrected in `do_add_single_file_part()`.
19321945
fn get_mime_type(
19331946
mail: &mailparse::ParsedMail<'_>,
19341947
filename: &Option<String>,
@@ -1937,7 +1950,7 @@ fn get_mime_type(
19371950

19381951
let viewtype = match mimetype.type_() {
19391952
mime::TEXT => match mimetype.subtype() {
1940-
mime::VCARD if is_valid_deltachat_vcard(mail) => Viewtype::Vcard,
1953+
mime::VCARD => Viewtype::Vcard,
19411954
mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
19421955
_ => Viewtype::File,
19431956
},
@@ -1988,17 +2001,6 @@ fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
19882001
.any(|(key, _value)| key.starts_with("filename"))
19892002
}
19902003

1991-
fn is_valid_deltachat_vcard(mail: &mailparse::ParsedMail) -> bool {
1992-
let Ok(body) = &mail.get_body() else {
1993-
return false;
1994-
};
1995-
let contacts = deltachat_contact_tools::parse_vcard(body);
1996-
if let [c] = &contacts[..] {
1997-
return deltachat_contact_tools::may_be_valid_addr(&c.addr);
1998-
}
1999-
false
2000-
}
2001-
20022004
/// Tries to get attachment filename.
20032005
///
20042006
/// If filename is explicitly specified in Content-Disposition, it is

src/param.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ pub enum Param {
8888
/// For Messages: quoted text.
8989
Quote = b'q',
9090

91+
/// For Messages: the 1st part of summary text (i.e. before the dash if any).
92+
Summary1 = b'4',
93+
9194
/// For Messages
9295
Cmd = b'S',
9396

src/receive_imf/tests.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4649,10 +4649,15 @@ async fn test_receive_vcard() -> Result<()> {
46494649
let alice = tcm.alice().await;
46504650
let bob = tcm.bob().await;
46514651

4652-
for vcard_contains_address in [true, false] {
4653-
let mut msg = Message::new(Viewtype::Vcard);
4652+
async fn test(
4653+
alice: &TestContext,
4654+
bob: &TestContext,
4655+
vcard_contains_address: bool,
4656+
viewtype: Viewtype,
4657+
) -> Result<()> {
4658+
let mut msg = Message::new(viewtype);
46544659
msg.set_file_from_bytes(
4655-
&alice,
4660+
alice,
46564661
"claire.vcf",
46574662
format!(
46584663
"BEGIN:VCARD\n\
@@ -4672,19 +4677,24 @@ async fn test_receive_vcard() -> Result<()> {
46724677
.await
46734678
.unwrap();
46744679

4675-
let alice_bob_chat = alice.create_chat(&bob).await;
4680+
let alice_bob_chat = alice.create_chat(bob).await;
46764681
let sent = alice.send_msg(alice_bob_chat.id, &mut msg).await;
46774682
let rcvd = bob.recv_msg(&sent).await;
4683+
let sent = Message::load_from_db(alice, sent.sender_msg_id).await?;
46784684

46794685
if vcard_contains_address {
4686+
assert_eq!(sent.viewtype, Viewtype::Vcard);
4687+
assert_eq!(sent.get_summary_text(alice).await, "👤 Claire");
46804688
assert_eq!(rcvd.viewtype, Viewtype::Vcard);
4689+
assert_eq!(rcvd.get_summary_text(bob).await, "👤 Claire");
46814690
} else {
46824691
// VCards without an email address are not "deltachat contacts",
46834692
// so they are shown as files
4693+
assert_eq!(sent.viewtype, Viewtype::File);
46844694
assert_eq!(rcvd.viewtype, Viewtype::File);
46854695
}
46864696

4687-
let vcard = tokio::fs::read(rcvd.get_file(&bob).unwrap()).await?;
4697+
let vcard = tokio::fs::read(rcvd.get_file(bob).unwrap()).await?;
46884698
let vcard = std::str::from_utf8(&vcard)?;
46894699
let parsed = deltachat_contact_tools::parse_vcard(vcard);
46904700
assert_eq!(parsed.len(), 1);
@@ -4693,6 +4703,13 @@ async fn test_receive_vcard() -> Result<()> {
46934703
} else {
46944704
assert_eq!(&parsed[0].addr, "");
46954705
}
4706+
Ok(())
4707+
}
4708+
4709+
for vcard_contains_address in [true, false] {
4710+
for viewtype in [Viewtype::File, Viewtype::Vcard] {
4711+
test(&alice, &bob, vcard_contains_address, viewtype).await?;
4712+
}
46964713
}
46974714

46984715
Ok(())
@@ -4720,7 +4737,9 @@ async fn test_make_n_send_vcard() -> Result<()> {
47204737
let sent = Message::load_from_db(alice, sent.sender_msg_id).await?;
47214738

47224739
assert_eq!(sent.viewtype, Viewtype::Vcard);
4740+
assert_eq!(sent.get_summary_text(alice).await, "👤 Claire");
47234741
assert_eq!(rcvd.viewtype, Viewtype::Vcard);
4742+
assert_eq!(rcvd.get_summary_text(bob).await, "👤 Claire");
47244743

47254744
let vcard = tokio::fs::read(rcvd.get_file(bob).unwrap()).await?;
47264745
let vcard = std::str::from_utf8(&vcard)?;

src/stock_str.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -443,9 +443,6 @@ pub enum StockMessage {
443443
fallback = "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
444444
))]
445445
SecurejoinWaitTimeout = 191,
446-
447-
#[strum(props(fallback = "Contact"))]
448-
Contact = 200,
449446
}
450447

451448
impl StockMessage {
@@ -1101,11 +1098,6 @@ pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> S
11011098
.replace1(url)
11021099
}
11031100

1104-
/// Stock string: `Contact`.
1105-
pub(crate) async fn contact(context: &Context) -> String {
1106-
translated(context, StockMessage::Contact).await
1107-
}
1108-
11091101
/// Stock string: `Error:\n\n“%1$s”`.
11101102
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
11111103
translated(context, StockMessage::ConfigurationFailed)

src/summary.rs

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::contact::{Contact, ContactId};
1010
use crate::context::Context;
1111
use crate::message::{Message, MessageState, Viewtype};
1212
use crate::mimeparser::SystemMessage;
13+
use crate::param::Param;
1314
use crate::stock_str;
1415
use crate::stock_str::msg_reacted;
1516
use crate::tools::truncate;
@@ -149,7 +150,7 @@ impl Summary {
149150

150151
impl Message {
151152
/// Returns a summary text.
152-
async fn get_summary_text(&self, context: &Context) -> String {
153+
pub(crate) async fn get_summary_text(&self, context: &Context) -> String {
153154
let summary = self.get_summary_text_without_prefix(context).await;
154155

155156
if self.is_forwarded() {
@@ -231,8 +232,8 @@ impl Message {
231232
}
232233
Viewtype::Vcard => {
233234
emoji = Some("👤");
234-
type_name = Some(stock_str::contact(context).await);
235-
type_file = None;
235+
type_name = None;
236+
type_file = self.param.get(Param::Summary1).map(|s| s.to_string());
236237
append_text = true;
237238
}
238239
Viewtype::Text | Viewtype::Unknown => {
@@ -284,6 +285,7 @@ impl Message {
284285
#[cfg(test)]
285286
mod tests {
286287
use super::*;
288+
use crate::chat::ChatId;
287289
use crate::param::Param;
288290
use crate::test_utils as test;
289291

@@ -296,7 +298,9 @@ mod tests {
296298
async fn test_get_summary_text() {
297299
let d = test::TestContext::new().await;
298300
let ctx = &d.ctx;
299-
301+
let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
302+
.await
303+
.unwrap();
300304
let some_text = " bla \t\n\tbla\n\t".to_string();
301305

302306
let mut msg = Message::new(Viewtype::Text);
@@ -367,25 +371,34 @@ mod tests {
367371
assert_summary_texts(&msg, ctx, "Video chat invitation").await; // text is not added for videochat invitations
368372

369373
let mut msg = Message::new(Viewtype::Vcard);
370-
msg.set_file("foo.vcf", None);
371-
assert_summary_texts(&msg, ctx, "👤 Contact").await;
374+
msg.set_file_from_bytes(ctx, "foo.vcf", b"", None)
375+
.await
376+
.unwrap();
377+
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
378+
// If a vCard can't be parsed, the message becomes `Viewtype::File`.
379+
assert_eq!(msg.viewtype, Viewtype::File);
380+
assert_summary_texts(&msg, ctx, "📎 foo.vcf").await;
372381
msg.set_text(some_text.clone());
373-
assert_summary_texts(&msg, ctx, "👤 bla bla").await;
374-
375-
let mut msg = Message::new(Viewtype::Vcard);
376-
msg.set_file_from_bytes(
377-
ctx,
378-
"alice.vcf",
379-
b"BEGIN:VCARD\n\
380-
VERSION:4.0\n\
381-
FN:Alice Wonderland\n\
382-
EMAIL;TYPE=work:[email protected]\n\
383-
END:VCARD",
384-
None,
385-
)
386-
.await
387-
.unwrap();
388-
assert_summary_texts(&msg, ctx, "👤 Contact").await;
382+
assert_summary_texts(&msg, ctx, "📎 foo.vcf \u{2013} bla bla").await;
383+
384+
for vt in [Viewtype::Vcard, Viewtype::File] {
385+
let mut msg = Message::new(vt);
386+
msg.set_file_from_bytes(
387+
ctx,
388+
"alice.vcf",
389+
b"BEGIN:VCARD\n\
390+
VERSION:4.0\n\
391+
FN:Alice Wonderland\n\
392+
EMAIL;TYPE=work:[email protected]\n\
393+
END:VCARD",
394+
None,
395+
)
396+
.await
397+
.unwrap();
398+
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
399+
assert_eq!(msg.viewtype, Viewtype::Vcard);
400+
assert_summary_texts(&msg, ctx, "👤 Alice Wonderland").await;
401+
}
389402

390403
// Forwarded
391404
let mut msg = Message::new(Viewtype::Text);

0 commit comments

Comments
 (0)