diff --git a/CHANGELOG.md b/CHANGELOG.md index 21e068064..3ad2c31ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ - breaking change: You have to call dc_stop_io()/dc_start_io() before/after EXPORT_BACKUP: fix race condition and db corruption when a message was received during backup #2253 +- save subject for messages: + new api `dc_msg_get_subject()`, + when quoting, use the subject of the quoted message as the new subject, instead of the + last subject in the chat + - new apis to get full or html message, `dc_msg_has_html()` and `dc_get_msg_html()` #2125 #2151 diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 83df277b0..15f9d3705 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1498,6 +1498,8 @@ char* dc_get_msg_info (dc_context_t* context, uint32_t ms * this removes the need for the UI * to deal with different formatting options of PLAIN-parts. * + * As the title of the full-message-view, you can use the subject (see dc_msg_get_subject()). + * * **Note:** The returned HTML-code may contain scripts, * external images that may be misused as hidden read-receipts and so on. * Taking care of these parts @@ -3344,6 +3346,25 @@ int64_t dc_msg_get_sort_timestamp (const dc_msg_t* msg); char* dc_msg_get_text (const dc_msg_t* msg); +/** + * Get the subject of the email. + * If there is no subject associated with the message, an empty string is returned. + * NULL is never returned. + * + * You usually don't need this; if the core thinks that the subject might contain important + * information, it automatically prepends it to the message text. + * + * This function was introduced so that you can use the subject as the title for the + * full-message-view (see dc_get_msg_html()). + * + * For outgoing messages, the subject is not stored and an empty string is returned. + * + * @memberof dc_msg_t + * @param msg The message object. + * @return The subject. The result must be released using dc_str_unref(). Never returns NULL. + */ +char* dc_msg_get_subject (const dc_msg_t* msg); + /** * Find out full path, file name and extension of the file associated with a * message. diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index fdc1a198d..839ecb626 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -2698,6 +2698,16 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha ffi_msg.message.get_text().unwrap_or_default().strdup() } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_get_subject(msg: *mut dc_msg_t) -> *mut libc::c_char { + if msg.is_null() { + eprintln!("ignoring careless call to dc_msg_get_subject()"); + return "".strdup(); + } + let ffi_msg = &*msg; + ffi_msg.message.get_subject().strdup() +} + #[no_mangle] pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_char { if msg.is_null() { diff --git a/python/tests/test_account.py b/python/tests/test_account.py index 107237630..e293f4426 100644 --- a/python/tests/test_account.py +++ b/python/tests/test_account.py @@ -1507,7 +1507,6 @@ class TestOnlineAccount: messages = chat2.get_messages() assert len(messages) == 2 assert messages[0].text == "msg1" - lp.sec("dbg file"+messages[1].filename) assert messages[1].filemime == "image/png" assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size ac.set_config("displayname", "new displayname") diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 07584af1c..1f807a19e 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -912,10 +912,10 @@ async fn add_parts( let mut stmt = conn.prepare_cached( "INSERT INTO msgs \ (rfc724_mid, server_folder, server_uid, chat_id, from_id, to_id, timestamp, \ - timestamp_sent, timestamp_rcvd, type, state, msgrmsg, txt, txt_raw, param, \ + timestamp_sent, timestamp_rcvd, type, state, msgrmsg, txt, subject, txt_raw, param, \ bytes, hidden, mime_headers, mime_in_reply_to, mime_references, mime_modified, \ error, ephemeral_timer, ephemeral_timestamp) \ - VALUES (?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?);", + VALUES (?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?);", )?; let is_location_kml = location_kml_is @@ -972,6 +972,7 @@ async fn add_parts( state, is_dc_message, if trash { "" } else { &part.msg }, + if trash { "" } else { &subject }, // txt_raw might contain invalid utf8 if trash { "" } else { &txt_raw }, if trash { diff --git a/src/ephemeral.rs b/src/ephemeral.rs index 58f82e6c5..429409ffc 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -311,7 +311,7 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result, + /// The value of the Subject header. Not set for messages that we sent ourselves. + pub(crate) subject: String, pub(crate) rfc724_mid: String, pub(crate) in_reply_to: Option, pub(crate) server_folder: Option, @@ -356,6 +358,7 @@ impl Message { " m.msgrmsg AS msgrmsg,", " m.mime_modified AS mime_modified,", " m.txt AS txt,", + " m.subject AS subject,", " m.param AS param,", " m.hidden AS hidden,", " m.location_id AS location,", @@ -405,6 +408,7 @@ impl Message { is_dc_message: row.get("msgrmsg")?, mime_modified: row.get("mime_modified")?, text: Some(text), + subject: row.get("subject")?, param: row.get::<_, String>("param")?.parse().unwrap_or_default(), hidden: row.get("hidden")?, location_id: row.get("location")?, @@ -550,6 +554,10 @@ impl Message { .map(|text| dc_truncate(text, DC_MAX_GET_TEXT_LEN).to_string()) } + pub fn get_subject(&self) -> &str { + &self.subject + } + pub fn get_filename(&self) -> Option { self.param .get(Param::File) @@ -2150,7 +2158,7 @@ mod tests { *mvbox_move, *chat_msg, if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam" - true, + false, true, true, false, diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 06d8a6973..0bf098b97 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1,13 +1,10 @@ -use anyhow::{bail, ensure, format_err, Error}; -use chrono::TimeZone; -use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder}; - use crate::blob::BlobObject; use crate::chat::{self, Chat}; use crate::config::Config; use crate::constants::{Chattype, Viewtype, DC_FROM_HANDSHAKE}; use crate::contact::Contact; use crate::context::{get_version_str, Context}; +use crate::dc_tools::IsNoneOrEmpty; use crate::dc_tools::{ dc_create_outgoing_rfc724_mid, dc_create_smeared_timestamp, dc_get_filebytes, time, }; @@ -22,6 +19,10 @@ use crate::param::Param; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::simplify::escape_message_footer_marks; use crate::stock_str; +use anyhow::Context as _; +use anyhow::{bail, ensure, format_err, Error}; +use chrono::TimeZone; +use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder}; use std::convert::TryInto; // attachments of 25 mb brutto should work on the majority of providers @@ -64,6 +65,7 @@ pub struct MimeFactory<'a> { req_mdn: bool, last_added_location_id: u32, attach_selfavatar: bool, + quoted_msg_subject: Option, } /// Result of rendering a message, ready to be submitted to a send job. @@ -153,7 +155,8 @@ impl<'a> MimeFactory<'a> { )) }, ) - .await?; + .await + .context("Can't get mime_in_reply_to, mime_references")?; let default_str = stock_str::status_line(context).await; let factory = MimeFactory { @@ -173,6 +176,7 @@ impl<'a> MimeFactory<'a> { req_mdn, last_added_location_id: 0, attach_selfavatar, + quoted_msg_subject: msg.quoted_message(context).await?.map(|m| m.subject), }; Ok(factory) } @@ -217,6 +221,7 @@ impl<'a> MimeFactory<'a> { req_mdn: false, last_added_location_id: 0, attach_selfavatar: false, + quoted_msg_subject: None, }; Ok(res) @@ -350,54 +355,64 @@ impl<'a> MimeFactory<'a> { } } - async fn subject_str(&self, context: &Context) -> String { - match self.loaded { + async fn subject_str(&self, context: &Context) -> anyhow::Result { + Ok(match self.loaded { Loaded::Message { ref chat } => { if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage { - stock_str::ac_setup_msg_subject(context).await - } else if chat.typ == Chattype::Group { + return Ok(stock_str::ac_setup_msg_subject(context).await); + } + + if chat.typ == Chattype::Group && self.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 { "Re: " }; - format!("{}{}", re, chat.name) - } else { - match chat.param.get(Param::LastSubject) { - Some(last_subject) => { - let subject_start = if last_subject.starts_with("Chat:") { - 0 - } else { - // "Antw:" is the longest abbreviation in - // https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages, - // so look at the first _5_ characters: - match last_subject.chars().take(5).position(|c| c == ':') { - Some(prefix_end) => prefix_end + 1, - None => 0, - } - }; - format!( - "Re: {}", - last_subject - .chars() - .skip(subject_start) - .collect::() - .trim() - ) - } - None => { - let self_name = match context.get_config(Config::Displayname).await { - Some(name) => name, - None => context.get_config(Config::Addr).await.unwrap_or_default(), - }; + return Ok(format!("{}{}", re, chat.name)); + } - stock_str::subject_for_new_contact(context, self_name).await - } + let parent_subject = if self.quoted_msg_subject.is_none_or_empty() { + chat.param.get(Param::LastSubject) + } else { + self.quoted_msg_subject.as_deref() + }; + + match parent_subject { + Some(last_subject) => { + let subject_start = if last_subject.starts_with("Chat:") { + 0 + } else { + // "Antw:" is the longest abbreviation in + // https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages, + // so look at the first _5_ characters: + match last_subject.chars().take(5).position(|c| c == ':') { + Some(prefix_end) => prefix_end + 1, + None => 0, + } + }; + format!( + "Re: {}", + last_subject + .chars() + .skip(subject_start) + .collect::() + .trim() + ) + } + None => { + 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, - } + }) } pub fn recipients(&self) -> Vec { @@ -478,7 +493,7 @@ impl<'a> MimeFactory<'a> { let grpimage = self.grpimage(); let force_plaintext = self.should_force_plaintext(); let skip_autocrypt = self.should_skip_autocrypt(); - let subject_str = self.subject_str(context).await; + let subject_str = self.subject_str(context).await?; let e2ee_guaranteed = self.is_e2ee_guaranteed(); let encrypt_helper = EncryptHelper::new(context).await?; @@ -1283,11 +1298,15 @@ fn maybe_encode_words(words: &str) -> String { #[cfg(test)] mod tests { use super::*; - use crate::chatlist::Chatlist; + use crate::chat::ChatId; + use crate::contact::Origin; use crate::dc_receive_imf::dc_receive_imf; use crate::mimeparser::MimeMessage; use crate::test_utils::TestContext; + use crate::{chatlist::Chatlist, test_utils::get_chat_msg}; + + use pretty_assertions::assert_eq; #[test] fn test_render_email_address() { @@ -1379,8 +1398,8 @@ mod tests { } #[async_std::test] - async fn test_subject() { - // 1.: Receive a mail from an MUA or Delta Chat + async fn test_subject_from_mua() { + // 1.: Receive a mail from an MUA assert_eq!( msg_to_subject_str( b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ @@ -1410,12 +1429,15 @@ mod tests { .await, "Re: Infos: 42" ); + } - // 2. Receive a message from Delta Chat when we did not send any messages before + #[async_std::test] + async fn test_subject_from_dc() { + // 2. Receive a message from Delta Chat assert_eq!( msg_to_subject_str( b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Charlie \n\ + From: bob@example.com\n\ To: alice@example.com\n\ Subject: Chat: hello\n\ Chat-Version: 1.0\n\ @@ -1427,7 +1449,10 @@ mod tests { .await, "Re: Chat: hello" ); + } + #[async_std::test] + async fn test_subject_outgoing() { // 3. Send the first message to a new contact let t = TestContext::new_alice().await; @@ -1438,11 +1463,14 @@ mod tests { .await .unwrap(); assert_eq!(first_subject_str(t).await, "Message from Alice"); + } + #[async_std::test] + async fn test_subject_unicode() { // 4. Receive messages with unicode characters and make sure that we do not panic (we do not care about the result) msg_to_subject_str( "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Charlie \n\ + From: bob@example.com\n\ To: alice@example.com\n\ Subject: äääää\n\ Chat-Version: 1.0\n\ @@ -1456,7 +1484,7 @@ mod tests { msg_to_subject_str( "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Charlie \n\ + From: bob@example.com\n\ To: alice@example.com\n\ Subject: aäääää\n\ Chat-Version: 1.0\n\ @@ -1467,15 +1495,18 @@ mod tests { .as_bytes(), ) .await; + } + #[async_std::test] + async fn test_subject_mdn() { // 5. Receive an mdn (read receipt) and make sure the mdn's subject is not used let t = TestContext::new_alice().await; dc_receive_imf( &t, b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ From: alice@example.com\n\ - To: Charlie \n\ - Subject: Hello, Charlie\n\ + To: bob@example.com\n\ + Subject: Hello, Bob\n\ Chat-Version: 1.0\n\ Message-ID: <2893@example.com>\n\ Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ @@ -1489,7 +1520,7 @@ mod tests { .unwrap(); let new_msg = incoming_msg_to_reply_msg( b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: charlie@example.com\n\ + From: bob@example.com\n\ To: alice@example.com\n\ Subject: message opened\n\ Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ @@ -1508,14 +1539,78 @@ mod tests { Content-Type: message/disposition-notification\n\ \n\ Reporting-UA: Delta Chat 1.28.0\n\ - Original-Recipient: rfc822;charlie@example.com\n\ - Final-Recipient: rfc822;charlie@example.com\n\ + Original-Recipient: rfc822;bob@example.com\n\ + Final-Recipient: rfc822;bob@example.com\n\ Original-Message-ID: <2893@example.com>\n\ Disposition: manual-action/MDN-sent-automatically; displayed\n\ \n", &t).await; let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap(); // The subject string should not be "Re: message opened" - assert_eq!("Re: Hello, Charlie", mf.subject_str(&t).await); + assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap()); + } + + #[async_std::test] + async fn test_subject_in_group() { + // 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject + let t = TestContext::new_alice().await; + let group_id = + chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") + .await + .unwrap(); + let bob = Contact::create(&t, "", "bob@example.org").await.unwrap(); + chat::add_contact_to_chat(&t, group_id, bob).await; + + async fn send_msg_get_subject( + t: &TestContext, + group_id: ChatId, + quote: Option<&Message>, + ) -> String { + let mut new_msg = Message::new(Viewtype::Text); + new_msg.set_text(Some("Hi".to_string())); + if let Some(q) = quote { + new_msg.set_quote(t, q).await.unwrap(); + } + let sent = t.send_msg(group_id, &mut new_msg).await; + t.parse_msg(&sent).await.get_subject().unwrap() + } + + let subject = send_msg_get_subject(&t, group_id, None).await; + assert_eq!(subject, "groupname"); + + let subject = send_msg_get_subject(&t, group_id, None).await; + assert_eq!(subject, "Re: groupname"); + + dc_receive_imf( + &t, + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.com\n\ + Subject: Different subject\n\ + In-Reply-To: {}\n\ + Message-ID: <2893@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n", + t.get_last_msg().await.rfc724_mid + ) + .as_bytes(), + "INBOX", + 5, + false, + ) + .await + .unwrap(); + let message_from_bob = t.get_last_msg().await; + + let subject = send_msg_get_subject(&t, group_id, None).await; + assert_eq!(subject, "Re: groupname"); + + let subject = send_msg_get_subject(&t, group_id, Some(&message_from_bob)).await; + assert_eq!(subject, "Re: Different subject"); + + let subject = send_msg_get_subject(&t, group_id, None).await; + assert_eq!(subject, "Re: groupname"); } async fn first_subject_str(t: TestContext) -> String { @@ -1534,14 +1629,93 @@ mod tests { let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap(); - mf.subject_str(&t).await + mf.subject_str(&t).await.unwrap() } + // In `imf_raw`, From has to be bob@example.com, To has to be alice@example.com async fn msg_to_subject_str(imf_raw: &[u8]) -> String { + let subject_str = msg_to_subject_str_inner(imf_raw, false, false, false).await; + + // Check that combinations of true and false reproduce the same subject_str: + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, true, false, false).await + ); + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, false, true, false).await + ); + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, false, true, true).await + ); + assert_eq!( + subject_str, + msg_to_subject_str_inner(imf_raw, true, true, false).await + ); + + // These two combinations are different: If `message_arrives_inbetween` is true, but + // `reply` is false, the core is actually expected to use the subject of the message + // that arrived inbetween. + assert_eq!( + "Re: Some other, completely unrelated subject", + msg_to_subject_str_inner(imf_raw, false, false, true).await + ); + assert_eq!( + "Re: Some other, completely unrelated subject", + msg_to_subject_str_inner(imf_raw, true, false, true).await + ); + + // We leave away the combination (true, true, true) here: + // It would mean that the original message is quoted without sending the quoting message + // out yet, then the original message is deleted, then another unrelated message arrives + // and then the message with the quote is sent out. Not very realistic. + + subject_str + } + + async fn msg_to_subject_str_inner( + imf_raw: &[u8], + delete_original_msg: bool, + reply: bool, + message_arrives_inbetween: bool, + ) -> String { let t = TestContext::new_alice().await; - let new_msg = incoming_msg_to_reply_msg(imf_raw, &t).await; + let mut new_msg = incoming_msg_to_reply_msg(imf_raw, &t).await; + let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 2).await; + + if delete_original_msg { + incoming_msg.id.delete_from_db(&t).await.unwrap(); + } + + if message_arrives_inbetween { + dc_receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.com\n\ + Subject: Some other, completely unrelated subject\n\ + Message-ID: <3cl4@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + Some other, completely unrelated content\n", + "INBOX", + 2, + false, + ) + .await + .unwrap(); + + let arrived_msg = t.get_last_msg().await; + assert_eq!(arrived_msg.chat_id, incoming_msg.chat_id); + } + + if reply { + new_msg.set_quote(&t, &incoming_msg).await.unwrap(); + } + let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap(); - mf.subject_str(&t).await + mf.subject_str(&t).await.unwrap() } // Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`. diff --git a/src/param.rs b/src/param.rs index dbd536883..eec7fad89 100644 --- a/src/param.rs +++ b/src/param.rs @@ -126,7 +126,9 @@ pub enum Param { /// For Chats Selftalk = b'K', - /// For Chats: So that on sending a new message we can sent the subject to "Re: " + /// For Chats: On sending a new message we set the subject to "Re: ". + /// Usually we just use the subject of the parent message, but if the parent message + /// is deleted, we use the LastSubject of the chat. LastSubject = b't', /// For Chats diff --git a/src/sql.rs b/src/sql.rs index e0306e1a8..e030d6f50 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1504,6 +1504,15 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label); .await?; sql.set_raw_config_int(context, "dbversion", 75).await?; } + if dbversion < 76 { + info!(context, "[migration] v76"); + sql.execute( + "ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", + paramsv![], + ) + .await?; + sql.set_raw_config_int(context, "dbversion", 76).await?; + } // (2) updates that require high-level objects // (the structure is complete now and all objects are usable) diff --git a/src/test_utils.rs b/src/test_utils.rs index c5cee1540..9e425015c 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -9,10 +9,10 @@ use std::{collections::BTreeMap, panic}; use std::{fmt, thread}; use ansi_term::Color; -use async_std::future::Future; use async_std::path::PathBuf; use async_std::sync::{Arc, RwLock}; use async_std::{channel, pin::Pin}; +use async_std::{future::Future, task}; use chat::ChatItem; use once_cell::sync::Lazy; use tempfile::{tempdir, TempDir}; @@ -20,6 +20,7 @@ use tempfile::{tempdir, TempDir}; use crate::chat::{self, Chat, ChatId}; use crate::chatlist::Chatlist; use crate::config::Config; +use crate::constants::Chattype; use crate::constants::{Viewtype, DC_CONTACT_ID_SELF, DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1}; use crate::contact::{Contact, Origin}; use crate::context::Context; @@ -102,9 +103,12 @@ impl TestContext { let (poison_sender, poison_receiver) = channel::bounded(1); async_std::task::spawn(async move { // Make sure that the test fails if there is a panic on this thread here: + let current_id = task::current().id(); let orig_hook = panic::take_hook(); panic::set_hook(Box::new(move |panic_info| { - poison_sender.try_send(panic_info.to_string()).ok(); + if task::current().id() == current_id { + poison_sender.try_send(panic_info.to_string()).ok(); + } orig_hook(panic_info); })); @@ -310,7 +314,11 @@ impl TestContext { /// Gets the most recent message over all chats. pub async fn get_last_msg(&self) -> Message { let chats = Chatlist::try_load(&self.ctx, 0, None, None).await.unwrap(); - let msg_id = chats.get_msg_id(chats.len() - 1).unwrap(); + // 0 is correct in the next line (as opposed to `chats.len() - 1`, which would be the last element): + // The chatlist describes what you see when you open DC, a list of chats and in each of them + // the first words of the last message. To get the last message overall, we look at the chat at the top of the + // list, which has the index 0. + let msg_id = chats.get_msg_id(0).unwrap(); Message::load_from_db(&self.ctx, msg_id).await.unwrap() } @@ -364,8 +372,17 @@ impl TestContext { pub async fn send_text(&self, chat_id: ChatId, txt: &str) -> SentMessage { let mut msg = Message::new(Viewtype::Text); msg.set_text(Some(txt.to_string())); - chat::prepare_msg(self, chat_id, &mut msg).await.unwrap(); - chat::send_msg(self, chat_id, &mut msg).await.unwrap(); + self.send_msg(chat_id, &mut msg).await + } + + /// Sends out the message to the specified chat. + /// + /// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call + /// [`TestContext::recv_msg`] with the returned [`SentMessage`] if it wants to receive + /// the message. + pub async fn send_msg(&self, chat_id: ChatId, msg: &mut Message) -> SentMessage { + chat::prepare_msg(self, chat_id, msg).await.unwrap(); + chat::send_msg(self, chat_id, msg).await.unwrap(); self.pop_sent_msg().await } @@ -375,8 +392,9 @@ impl TestContext { // This code is mainly the same as `log_msglist` in `cmdline.rs`, so one day, we could // merge them to a public function in the `deltachat` crate. #[allow(dead_code)] - pub async fn print_chat(&self, chat: &Chat) { - let msglist = chat::get_chat_msgs(self, chat.get_id(), 0x1, None).await; + #[allow(clippy::clippy::indexing_slicing)] + pub async fn print_chat(&self, chat_id: ChatId) { + let msglist = chat::get_chat_msgs(self, chat_id, 0x1, None).await; let msglist: Vec = msglist .into_iter() .map(|x| match x { @@ -386,6 +404,44 @@ impl TestContext { }) .collect(); + let sel_chat = Chat::load_from_db(self, chat_id).await.unwrap(); + let members = chat::get_chat_contacts(self, sel_chat.id).await; + let subtitle = if sel_chat.is_device_talk() { + "device-talk".to_string() + } else if sel_chat.get_type() == Chattype::Single && !members.is_empty() { + let contact = Contact::get_by_id(self, members[0]).await.unwrap(); + contact.get_addr().to_string() + } else if sel_chat.get_type() == Chattype::Mailinglist && !members.is_empty() { + "mailinglist".to_string() + } else { + format!("{} member(s)", members.len()) + }; + println!( + "{}#{}: {} [{}]{}{}{} {}", + sel_chat.typ, + sel_chat.get_id(), + sel_chat.get_name(), + subtitle, + if sel_chat.is_muted() { "🔇" } else { "" }, + if sel_chat.is_sending_locations() { + "📍" + } else { + "" + }, + match sel_chat.get_profile_image(self).await { + Some(icon) => match icon.to_str() { + Some(icon) => format!(" Icon: {}", icon), + _ => " Icon: Err".to_string(), + }, + _ => "".to_string(), + }, + if sel_chat.is_protected() { + "🛡️" + } else { + "" + }, + ); + let mut lines_out = 0; for msg_id in msglist { if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {