mirror of
https://github.com/chatmail/core.git
synced 2026-05-23 00:36:32 +03:00
api: Add Viewtype::Vcard (#5202)
Co-authored-by: Hocuri <hocuri@gmx.de>
This commit is contained in:
@@ -101,7 +101,7 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parses `VcardContact`s from a given `&str`.
|
/// Parses `VcardContact`s from a given `&str`.
|
||||||
pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||||
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
|
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
|
||||||
let start_of_s = s.get(..prefix.len())?;
|
let start_of_s = s.get(..prefix.len())?;
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(contacts)
|
contacts
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Valid contact address.
|
/// Valid contact address.
|
||||||
@@ -438,8 +438,7 @@ EMAIL;PREF=1:bobzzz@freenet.de
|
|||||||
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
||||||
END:VCARD
|
END:VCARD
|
||||||
",
|
",
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
|
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
|
||||||
assert_eq!(contacts[0].display_name, "Alice Mueller".to_string());
|
assert_eq!(contacts[0].display_name, "Alice Mueller".to_string());
|
||||||
@@ -469,8 +468,7 @@ KEY;TYPE=PGP;ENCODING=b:[base64-data]
|
|||||||
REV:20240418T184242Z
|
REV:20240418T184242Z
|
||||||
|
|
||||||
END:VCARD",
|
END:VCARD",
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||||
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
||||||
@@ -502,7 +500,7 @@ END:VCARD",
|
|||||||
for len in 0..=contacts.len() {
|
for len in 0..=contacts.len() {
|
||||||
let contacts = &contacts[0..len];
|
let contacts = &contacts[0..len];
|
||||||
let vcard = make_vcard(contacts);
|
let vcard = make_vcard(contacts);
|
||||||
let parsed = parse_vcard(&vcard).unwrap();
|
let parsed = parse_vcard(&vcard);
|
||||||
assert_eq!(parsed.len(), contacts.len());
|
assert_eq!(parsed.len(), contacts.len());
|
||||||
for i in 0..parsed.len() {
|
for i in 0..parsed.len() {
|
||||||
assert_eq!(parsed[i].addr, contacts[i].addr);
|
assert_eq!(parsed[i].addr, contacts[i].addr);
|
||||||
@@ -580,8 +578,7 @@ FN:Alice
|
|||||||
EMAIL;HOME:alice@example.org
|
EMAIL;HOME:alice@example.org
|
||||||
END:VCARD
|
END:VCARD
|
||||||
",
|
",
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||||
assert_eq!(contacts[0].display_name, "Bob".to_string());
|
assert_eq!(contacts[0].display_name, "Bob".to_string());
|
||||||
@@ -605,8 +602,7 @@ END:VCARD
|
|||||||
EMAIL;TYPE=work:alice@example.org\n\
|
EMAIL;TYPE=work:alice@example.org\n\
|
||||||
REV:20240418T184242\n\
|
REV:20240418T184242\n\
|
||||||
END:VCARD",
|
END:VCARD",
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
assert_eq!(contacts.len(), 1);
|
assert_eq!(contacts.len(), 1);
|
||||||
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
|
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
|
||||||
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
||||||
|
|||||||
@@ -5480,6 +5480,11 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*/
|
*/
|
||||||
#define DC_MSG_WEBXDC 80
|
#define DC_MSG_WEBXDC 80
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message containing shared contacts represented as a vCard (virtual contact file)
|
||||||
|
* with email addresses and possibly other fields.
|
||||||
|
*/
|
||||||
|
#define DC_MSG_VCARD 90
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
@@ -7338,6 +7343,9 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// Used as info message.
|
/// Used as info message.
|
||||||
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
||||||
|
|
||||||
|
/// "Contact"
|
||||||
|
#define DC_STR_CONTACT 200
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1431,7 +1431,7 @@ impl CommandApi {
|
|||||||
async fn parse_vcard(&self, path: String) -> Result<Vec<VcardContact>> {
|
async fn parse_vcard(&self, path: String) -> Result<Vec<VcardContact>> {
|
||||||
let vcard = tokio::fs::read(Path::new(&path)).await?;
|
let vcard = tokio::fs::read(Path::new(&path)).await?;
|
||||||
let vcard = str::from_utf8(&vcard)?;
|
let vcard = str::from_utf8(&vcard)?;
|
||||||
Ok(deltachat_contact_tools::parse_vcard(vcard)?
|
Ok(deltachat_contact_tools::parse_vcard(vcard)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| c.into())
|
.map(|c| c.into())
|
||||||
.collect())
|
.collect())
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ impl ContactObject {
|
|||||||
pub struct VcardContact {
|
pub struct VcardContact {
|
||||||
/// Email address.
|
/// Email address.
|
||||||
addr: String,
|
addr: String,
|
||||||
|
/// The contact's name, or the email address if no name was given.
|
||||||
display_name: String,
|
display_name: String,
|
||||||
/// Public PGP key in Base64.
|
/// Public PGP key in Base64.
|
||||||
key: Option<String>,
|
key: Option<String>,
|
||||||
|
|||||||
@@ -274,6 +274,11 @@ pub enum MessageViewtype {
|
|||||||
|
|
||||||
/// Message is an webxdc instance.
|
/// Message is an webxdc instance.
|
||||||
Webxdc,
|
Webxdc,
|
||||||
|
|
||||||
|
/// Message containing shared contacts represented as a vCard (virtual contact file)
|
||||||
|
/// with email addresses and possibly other fields.
|
||||||
|
/// Use `parse_vcard()` to retrieve them.
|
||||||
|
Vcard,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Viewtype> for MessageViewtype {
|
impl From<Viewtype> for MessageViewtype {
|
||||||
@@ -290,6 +295,7 @@ impl From<Viewtype> for MessageViewtype {
|
|||||||
Viewtype::File => MessageViewtype::File,
|
Viewtype::File => MessageViewtype::File,
|
||||||
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||||
|
Viewtype::Vcard => MessageViewtype::Vcard,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,6 +314,7 @@ impl From<MessageViewtype> for Viewtype {
|
|||||||
MessageViewtype::File => Viewtype::File,
|
MessageViewtype::File => Viewtype::File,
|
||||||
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||||
|
MessageViewtype::Vcard => Viewtype::Vcard,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1416,8 +1416,8 @@ pub(crate) fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)>
|
|||||||
"tif" => (Viewtype::File, "image/tiff"),
|
"tif" => (Viewtype::File, "image/tiff"),
|
||||||
"ttf" => (Viewtype::File, "font/ttf"),
|
"ttf" => (Viewtype::File, "font/ttf"),
|
||||||
"txt" => (Viewtype::File, "text/plain"),
|
"txt" => (Viewtype::File, "text/plain"),
|
||||||
"vcard" => (Viewtype::File, "text/vcard"),
|
"vcard" => (Viewtype::Vcard, "text/vcard"),
|
||||||
"vcf" => (Viewtype::File, "text/vcard"),
|
"vcf" => (Viewtype::Vcard, "text/vcard"),
|
||||||
"wav" => (Viewtype::File, "audio/wav"),
|
"wav" => (Viewtype::File, "audio/wav"),
|
||||||
"weba" => (Viewtype::File, "audio/webm"),
|
"weba" => (Viewtype::File, "audio/webm"),
|
||||||
"webm" => (Viewtype::Video, "video/webm"),
|
"webm" => (Viewtype::Video, "video/webm"),
|
||||||
@@ -1938,7 +1938,8 @@ pub enum Viewtype {
|
|||||||
Text = 10,
|
Text = 10,
|
||||||
|
|
||||||
/// Image message.
|
/// Image message.
|
||||||
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
|
/// If the image is a GIF and has the appropriate extension, the viewtype is auto-changed to
|
||||||
|
/// `Gif` when sending the message.
|
||||||
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
|
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
|
||||||
/// and retrieved via dc_msg_set_file(), dc_msg_set_dimension().
|
/// and retrieved via dc_msg_set_file(), dc_msg_set_dimension().
|
||||||
Image = 20,
|
Image = 20,
|
||||||
@@ -1982,6 +1983,11 @@ pub enum Viewtype {
|
|||||||
|
|
||||||
/// Message is an webxdc instance.
|
/// Message is an webxdc instance.
|
||||||
Webxdc = 80,
|
Webxdc = 80,
|
||||||
|
|
||||||
|
/// Message containing shared contacts represented as a vCard (virtual contact file)
|
||||||
|
/// with email addresses and possibly other fields.
|
||||||
|
/// Use `parse_vcard()` to retrieve them.
|
||||||
|
Vcard = 90,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Viewtype {
|
impl Viewtype {
|
||||||
@@ -1999,6 +2005,7 @@ impl Viewtype {
|
|||||||
Viewtype::File => true,
|
Viewtype::File => true,
|
||||||
Viewtype::VideochatInvitation => false,
|
Viewtype::VideochatInvitation => false,
|
||||||
Viewtype::Webxdc => true,
|
Viewtype::Webxdc => true,
|
||||||
|
Viewtype::Vcard => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2512,6 +2519,7 @@ mod tests {
|
|||||||
Viewtype::from_i32(70).unwrap()
|
Viewtype::from_i32(70).unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
|
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
|
||||||
|
assert_eq!(Viewtype::Vcard, Viewtype::from_i32(90).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
|||||||
@@ -600,6 +600,7 @@ impl MimeMessage {
|
|||||||
| Viewtype::Audio
|
| Viewtype::Audio
|
||||||
| Viewtype::Voice
|
| Viewtype::Voice
|
||||||
| Viewtype::Video
|
| Viewtype::Video
|
||||||
|
| Viewtype::Vcard
|
||||||
| Viewtype::File
|
| Viewtype::File
|
||||||
| Viewtype::Webxdc => true,
|
| Viewtype::Webxdc => true,
|
||||||
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
|
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
|
||||||
@@ -1933,16 +1934,11 @@ fn get_mime_type(
|
|||||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||||
|
|
||||||
let viewtype = match mimetype.type_() {
|
let viewtype = match mimetype.type_() {
|
||||||
mime::TEXT => {
|
mime::TEXT => match mimetype.subtype() {
|
||||||
if !is_attachment_disposition(mail) {
|
mime::VCARD if is_valid_deltachat_vcard(mail) => Viewtype::Vcard,
|
||||||
match mimetype.subtype() {
|
mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
|
||||||
mime::PLAIN | mime::HTML => Viewtype::Text,
|
_ => Viewtype::File,
|
||||||
_ => Viewtype::File,
|
},
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Viewtype::File
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mime::IMAGE => match mimetype.subtype() {
|
mime::IMAGE => match mimetype.subtype() {
|
||||||
mime::GIF => Viewtype::Gif,
|
mime::GIF => Viewtype::Gif,
|
||||||
mime::SVG => Viewtype::File,
|
mime::SVG => Viewtype::File,
|
||||||
@@ -1990,6 +1986,17 @@ fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
|
|||||||
.any(|(key, _value)| key.starts_with("filename"))
|
.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.
|
/// Tries to get attachment filename.
|
||||||
///
|
///
|
||||||
/// If filename is explicitly specified in Content-Disposition, it is
|
/// If filename is explicitly specified in Content-Disposition, it is
|
||||||
|
|||||||
@@ -4552,3 +4552,58 @@ async fn test_list_from() -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_receive_vcard() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = tcm.alice().await;
|
||||||
|
let bob = tcm.bob().await;
|
||||||
|
|
||||||
|
for vcard_contains_address in [true, false] {
|
||||||
|
let mut msg = Message::new(Viewtype::Vcard);
|
||||||
|
msg.set_file_from_bytes(
|
||||||
|
&alice,
|
||||||
|
"claire.vcf",
|
||||||
|
format!(
|
||||||
|
"BEGIN:VCARD\n\
|
||||||
|
VERSION:4.0\n\
|
||||||
|
FN:Claire\n\
|
||||||
|
{}\
|
||||||
|
END:VCARD",
|
||||||
|
if vcard_contains_address {
|
||||||
|
"EMAIL;TYPE=work:claire@example.org\n"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if vcard_contains_address {
|
||||||
|
assert_eq!(rcvd.viewtype, Viewtype::Vcard);
|
||||||
|
} else {
|
||||||
|
// VCards without an email address are not "deltachat contacts",
|
||||||
|
// so they are shown as files
|
||||||
|
assert_eq!(rcvd.viewtype, Viewtype::File);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if vcard_contains_address {
|
||||||
|
assert_eq!(&parsed[0].addr, "claire@example.org");
|
||||||
|
} else {
|
||||||
|
assert_eq!(&parsed[0].addr, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -443,6 +443,9 @@ pub enum StockMessage {
|
|||||||
fallback = "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
|
fallback = "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
|
||||||
))]
|
))]
|
||||||
SecurejoinWaitTimeout = 191,
|
SecurejoinWaitTimeout = 191,
|
||||||
|
|
||||||
|
#[strum(props(fallback = "Contact"))]
|
||||||
|
Contact = 200,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StockMessage {
|
impl StockMessage {
|
||||||
@@ -1098,6 +1101,11 @@ pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> S
|
|||||||
.replace1(url)
|
.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”`.
|
/// Stock string: `Error:\n\n“%1$s”`.
|
||||||
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
|
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
|
||||||
translated(context, StockMessage::ConfigurationFailed)
|
translated(context, StockMessage::ConfigurationFailed)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::str;
|
||||||
|
|
||||||
use crate::chat::Chat;
|
use crate::chat::Chat;
|
||||||
use crate::constants::Chattype;
|
use crate::constants::Chattype;
|
||||||
@@ -228,6 +229,12 @@ impl Message {
|
|||||||
);
|
);
|
||||||
append_text = true;
|
append_text = true;
|
||||||
}
|
}
|
||||||
|
Viewtype::Vcard => {
|
||||||
|
emoji = Some("👤");
|
||||||
|
type_name = Some(stock_str::contact(context).await);
|
||||||
|
type_file = None;
|
||||||
|
append_text = true;
|
||||||
|
}
|
||||||
Viewtype::Text | Viewtype::Unknown => {
|
Viewtype::Text | Viewtype::Unknown => {
|
||||||
emoji = None;
|
emoji = None;
|
||||||
if self.param.get_cmd() == SystemMessage::LocationOnly {
|
if self.param.get_cmd() == SystemMessage::LocationOnly {
|
||||||
@@ -340,10 +347,6 @@ mod tests {
|
|||||||
msg.set_file("foo.mp3", None);
|
msg.set_file("foo.mp3", None);
|
||||||
assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; // file name is added for audio
|
assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; // file name is added for audio
|
||||||
|
|
||||||
let mut msg = Message::new(Viewtype::Audio);
|
|
||||||
msg.set_file("foo.mp3", None);
|
|
||||||
assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; // file name is added for audio, empty text is not added
|
|
||||||
|
|
||||||
let mut msg = Message::new(Viewtype::Audio);
|
let mut msg = Message::new(Viewtype::Audio);
|
||||||
msg.set_text(some_text.clone());
|
msg.set_text(some_text.clone());
|
||||||
msg.set_file("foo.mp3", None);
|
msg.set_file("foo.mp3", None);
|
||||||
@@ -363,6 +366,27 @@ mod tests {
|
|||||||
msg.set_file("foo.bar", None);
|
msg.set_file("foo.bar", None);
|
||||||
assert_summary_texts(&msg, ctx, "Video chat invitation").await; // text is not added for videochat invitations
|
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_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;
|
||||||
|
|
||||||
// Forwarded
|
// Forwarded
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
msg.set_text(some_text.clone());
|
msg.set_text(some_text.clone());
|
||||||
|
|||||||
Reference in New Issue
Block a user