diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 7f41b6014b..1111cb6258 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -7366,7 +7366,7 @@ void dc_event_unref(dc_event_t* event); /// Used as info message. #define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191 -/// "Contact" +/// "Contact". Deprecated, currently unused. #define DC_STR_CONTACT 200 /** diff --git a/src/chat.rs b/src/chat.rs index 09f6e119c2..be9b049e93 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -49,7 +49,6 @@ use crate::tools::{ create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input, smeared_time, time, IsNoneOrEmpty, SystemTime, }; -use crate::webxdc::WEBXDC_SUFFIX; /// An chat item, such as a message or a marker. #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -894,8 +893,15 @@ impl ChatId { .await? .context("no file stored in params")?; msg.param.set(Param::File, blob.as_name()); - if blob.suffix() == Some(WEBXDC_SUFFIX) { - msg.viewtype = Viewtype::Webxdc; + if msg.viewtype == Viewtype::File { + if let Some((better_type, _)) = + message::guess_msgtype_from_suffix(&blob.to_abs_path()) + { + msg.viewtype = better_type; + } + } + if msg.viewtype == Viewtype::Vcard { + msg.try_set_vcard(context, &blob.to_abs_path()).await?; } } } @@ -2649,6 +2655,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { .await?; } + if msg.viewtype == Viewtype::Vcard { + msg.try_set_vcard(context, &blob.to_abs_path()).await?; + } + let mut maybe_sticker = msg.viewtype == Viewtype::Sticker; if !send_as_is && (msg.viewtype == Viewtype::Image diff --git a/src/message.rs b/src/message.rs index f8c1bd23e4..063efe58cb 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2,6 +2,7 @@ use std::collections::BTreeSet; use std::path::{Path, PathBuf}; +use std::str; use anyhow::{ensure, format_err, Context as _, Result}; use deltachat_contact_tools::{parse_vcard, VcardContact}; @@ -1093,6 +1094,18 @@ impl Message { .await } + /// Updates message state from the vCard attachment. + pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> { + let vcard = fs::read(path).await.context("Could not read {path}")?; + if let Some(summary) = get_vcard_summary(&vcard) { + self.param.set(Param::Summary1, summary); + } else { + warn!(context, "try_set_vcard: Not a valid DeltaChat vCard."); + self.viewtype = Viewtype::File; + } + Ok(()) + } + /// Set different sender name for a message. /// This overrides the name set by the `set_config()`-option `displayname`. pub fn set_override_sender_name(&mut self, name: Option) { @@ -1938,6 +1951,19 @@ pub(crate) async fn get_latest_by_rfc724_mids( Ok(None) } +/// Returns the 1st part of summary text (i.e. before the dash if any) for a valid DeltaChat vCard. +pub(crate) fn get_vcard_summary(vcard: &[u8]) -> Option { + let vcard = str::from_utf8(vcard).ok()?; + let contacts = deltachat_contact_tools::parse_vcard(vcard); + let [c] = &contacts[..] else { + return None; + }; + if !deltachat_contact_tools::may_be_valid_addr(&c.addr) { + return None; + } + Some(c.display_name().to_string()) +} + /// How a message is primarily displayed. #[derive( Debug, diff --git a/src/mimeparser.rs b/src/mimeparser.rs index e0a1b4594e..4471151d9e 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -28,7 +28,8 @@ use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey}; use crate::message::{ - self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype, + self, get_vcard_summary, set_msg_failed, update_msg_state, Message, MessageState, MsgId, + Viewtype, }; use crate::param::{Param, Params}; use crate::peerstate::Peerstate; @@ -1233,6 +1234,7 @@ impl MimeMessage { return Ok(()); } } + let mut param = Params::new(); let msg_type = if context .is_webxdc_file(filename, decoded_data) .await @@ -1276,6 +1278,13 @@ impl MimeMessage { .unwrap_or_default(); self.webxdc_status_update = Some(serialized); return Ok(()); + } else if msg_type == Viewtype::Vcard { + if let Some(summary) = get_vcard_summary(decoded_data) { + param.set(Param::Summary1, summary); + msg_type + } else { + Viewtype::File + } } else { msg_type }; @@ -1299,8 +1308,8 @@ impl MimeMessage { let mut part = Part::default(); if mime_type.type_() == mime::IMAGE { if let Ok((width, height)) = get_filemeta(decoded_data) { - part.param.set_int(Param::Width, width as i32); - part.param.set_int(Param::Height, height as i32); + param.set_int(Param::Width, width as i32); + param.set_int(Param::Height, height as i32); } } @@ -1308,9 +1317,10 @@ impl MimeMessage { part.org_filename = Some(filename.to_string()); part.mimetype = Some(mime_type); part.bytes = decoded_data.len(); - part.param.set(Param::File, blob.as_name()); - part.param.set(Param::Filename, filename); - part.param.set(Param::MimeType, raw_mime); + param.set(Param::File, blob.as_name()); + param.set(Param::Filename, filename); + param.set(Param::MimeType, raw_mime); + part.param = param; part.is_related = is_related; self.do_add_single_part(part); @@ -1928,7 +1938,10 @@ pub struct Part { pub(crate) is_reaction: bool, } -/// return mimetype and viewtype for a parsed mail +/// Returns the mimetype and viewtype for a parsed mail. +/// +/// This only looks at the metadata, not at the content; +/// the viewtype may later be corrected in `do_add_single_file_part()`. fn get_mime_type( mail: &mailparse::ParsedMail<'_>, filename: &Option, @@ -1937,7 +1950,7 @@ fn get_mime_type( let viewtype = match mimetype.type_() { mime::TEXT => match mimetype.subtype() { - mime::VCARD if is_valid_deltachat_vcard(mail) => Viewtype::Vcard, + mime::VCARD => Viewtype::Vcard, mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text, _ => Viewtype::File, }, @@ -1988,17 +2001,6 @@ fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool { .any(|(key, _value)| key.starts_with("filename")) } -fn is_valid_deltachat_vcard(mail: &mailparse::ParsedMail) -> bool { - let Ok(body) = &mail.get_body() else { - return false; - }; - let contacts = deltachat_contact_tools::parse_vcard(body); - if let [c] = &contacts[..] { - return deltachat_contact_tools::may_be_valid_addr(&c.addr); - } - false -} - /// Tries to get attachment filename. /// /// If filename is explicitly specified in Content-Disposition, it is diff --git a/src/param.rs b/src/param.rs index 4348422617..b65cfd1d39 100644 --- a/src/param.rs +++ b/src/param.rs @@ -88,6 +88,9 @@ pub enum Param { /// For Messages: quoted text. Quote = b'q', + /// For Messages: the 1st part of summary text (i.e. before the dash if any). + Summary1 = b'4', + /// For Messages Cmd = b'S', diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index 9ae1ca25a4..42b2718338 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -4649,10 +4649,15 @@ async fn test_receive_vcard() -> Result<()> { let alice = tcm.alice().await; let bob = tcm.bob().await; - for vcard_contains_address in [true, false] { - let mut msg = Message::new(Viewtype::Vcard); + async fn test( + alice: &TestContext, + bob: &TestContext, + vcard_contains_address: bool, + viewtype: Viewtype, + ) -> Result<()> { + let mut msg = Message::new(viewtype); msg.set_file_from_bytes( - &alice, + alice, "claire.vcf", format!( "BEGIN:VCARD\n\ @@ -4672,19 +4677,24 @@ async fn test_receive_vcard() -> Result<()> { .await .unwrap(); - let alice_bob_chat = alice.create_chat(&bob).await; + let alice_bob_chat = alice.create_chat(bob).await; let sent = alice.send_msg(alice_bob_chat.id, &mut msg).await; let rcvd = bob.recv_msg(&sent).await; + let sent = Message::load_from_db(alice, sent.sender_msg_id).await?; if vcard_contains_address { + assert_eq!(sent.viewtype, Viewtype::Vcard); + assert_eq!(sent.get_summary_text(alice).await, "šŸ‘¤ Claire"); assert_eq!(rcvd.viewtype, Viewtype::Vcard); + assert_eq!(rcvd.get_summary_text(bob).await, "šŸ‘¤ Claire"); } else { // VCards without an email address are not "deltachat contacts", // so they are shown as files + assert_eq!(sent.viewtype, Viewtype::File); assert_eq!(rcvd.viewtype, Viewtype::File); } - let vcard = tokio::fs::read(rcvd.get_file(&bob).unwrap()).await?; + let vcard = tokio::fs::read(rcvd.get_file(bob).unwrap()).await?; let vcard = std::str::from_utf8(&vcard)?; let parsed = deltachat_contact_tools::parse_vcard(vcard); assert_eq!(parsed.len(), 1); @@ -4693,6 +4703,13 @@ async fn test_receive_vcard() -> Result<()> { } else { assert_eq!(&parsed[0].addr, ""); } + Ok(()) + } + + for vcard_contains_address in [true, false] { + for viewtype in [Viewtype::File, Viewtype::Vcard] { + test(&alice, &bob, vcard_contains_address, viewtype).await?; + } } Ok(()) @@ -4720,7 +4737,9 @@ async fn test_make_n_send_vcard() -> Result<()> { let sent = Message::load_from_db(alice, sent.sender_msg_id).await?; assert_eq!(sent.viewtype, Viewtype::Vcard); + assert_eq!(sent.get_summary_text(alice).await, "šŸ‘¤ Claire"); assert_eq!(rcvd.viewtype, Viewtype::Vcard); + assert_eq!(rcvd.get_summary_text(bob).await, "šŸ‘¤ Claire"); let vcard = tokio::fs::read(rcvd.get_file(bob).unwrap()).await?; let vcard = std::str::from_utf8(&vcard)?; diff --git a/src/stock_str.rs b/src/stock_str.rs index 2d7da10437..984a658f82 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -443,9 +443,6 @@ pub enum StockMessage { fallback = "Could not yet establish guaranteed end-to-end encryption, but you may already send a message." ))] SecurejoinWaitTimeout = 191, - - #[strum(props(fallback = "Contact"))] - Contact = 200, } impl StockMessage { @@ -1101,11 +1098,6 @@ pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> S .replace1(url) } -/// Stock string: `Contact`. -pub(crate) async fn contact(context: &Context) -> String { - translated(context, StockMessage::Contact).await -} - /// Stock string: `Error:\n\nā€œ%1$sā€`. pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String { translated(context, StockMessage::ConfigurationFailed) diff --git a/src/summary.rs b/src/summary.rs index 9fe63530b3..f3f3c3438c 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -10,6 +10,7 @@ use crate::contact::{Contact, ContactId}; use crate::context::Context; use crate::message::{Message, MessageState, Viewtype}; use crate::mimeparser::SystemMessage; +use crate::param::Param; use crate::stock_str; use crate::stock_str::msg_reacted; use crate::tools::truncate; @@ -149,7 +150,7 @@ impl Summary { impl Message { /// Returns a summary text. - async fn get_summary_text(&self, context: &Context) -> String { + pub(crate) async fn get_summary_text(&self, context: &Context) -> String { let summary = self.get_summary_text_without_prefix(context).await; if self.is_forwarded() { @@ -231,8 +232,8 @@ impl Message { } Viewtype::Vcard => { emoji = Some("šŸ‘¤"); - type_name = Some(stock_str::contact(context).await); - type_file = None; + type_name = None; + type_file = self.param.get(Param::Summary1).map(|s| s.to_string()); append_text = true; } Viewtype::Text | Viewtype::Unknown => { @@ -284,6 +285,7 @@ impl Message { #[cfg(test)] mod tests { use super::*; + use crate::chat::ChatId; use crate::param::Param; use crate::test_utils as test; @@ -296,7 +298,9 @@ mod tests { async fn test_get_summary_text() { let d = test::TestContext::new().await; let ctx = &d.ctx; - + let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF) + .await + .unwrap(); let some_text = " bla \t\n\tbla\n\t".to_string(); let mut msg = Message::new(Viewtype::Text); @@ -367,25 +371,34 @@ mod tests { assert_summary_texts(&msg, ctx, "Video chat invitation").await; // text is not added for videochat invitations let mut msg = Message::new(Viewtype::Vcard); - msg.set_file("foo.vcf", None); - assert_summary_texts(&msg, ctx, "šŸ‘¤ Contact").await; + msg.set_file_from_bytes(ctx, "foo.vcf", b"", None) + .await + .unwrap(); + chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap(); + // If a vCard can't be parsed, the message becomes `Viewtype::File`. + assert_eq!(msg.viewtype, Viewtype::File); + assert_summary_texts(&msg, ctx, "šŸ“Ž foo.vcf").await; msg.set_text(some_text.clone()); - assert_summary_texts(&msg, ctx, "šŸ‘¤ bla bla").await; - - let mut msg = Message::new(Viewtype::Vcard); - msg.set_file_from_bytes( - ctx, - "alice.vcf", - b"BEGIN:VCARD\n\ - VERSION:4.0\n\ - FN:Alice Wonderland\n\ - EMAIL;TYPE=work:alice@example.org\n\ - END:VCARD", - None, - ) - .await - .unwrap(); - assert_summary_texts(&msg, ctx, "šŸ‘¤ Contact").await; + assert_summary_texts(&msg, ctx, "šŸ“Ž foo.vcf \u{2013} bla bla").await; + + for vt in [Viewtype::Vcard, Viewtype::File] { + let mut msg = Message::new(vt); + msg.set_file_from_bytes( + ctx, + "alice.vcf", + b"BEGIN:VCARD\n\ + VERSION:4.0\n\ + FN:Alice Wonderland\n\ + EMAIL;TYPE=work:alice@example.org\n\ + END:VCARD", + None, + ) + .await + .unwrap(); + chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap(); + assert_eq!(msg.viewtype, Viewtype::Vcard); + assert_summary_texts(&msg, ctx, "šŸ‘¤ Alice Wonderland").await; + } // Forwarded let mut msg = Message::new(Viewtype::Text);