diff --git a/node/test/test.mjs b/node/test/test.mjs index 7bef6d21cf..bc9ba8a3f0 100644 --- a/node/test/test.mjs +++ b/node/test/test.mjs @@ -250,6 +250,7 @@ describe('Basic offline Tests', function () { 'journal_mode', 'key_gen_type', 'last_housekeeping', + 'last_cant_decrypt_outgoing_msgs', 'level', 'mdns_enabled', 'media_quality', diff --git a/src/config.rs b/src/config.rs index 99e97c0ae4..a99902bc3b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -291,6 +291,9 @@ pub enum Config { /// Timestamp of the last time housekeeping was run LastHousekeeping, + /// Timestamp of the last `CantDecryptOutgoingMsgs` notification. + LastCantDecryptOutgoingMsgs, + /// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely. #[strum(props(default = "60"))] ScanAllFoldersDebounceSecs, diff --git a/src/context.rs b/src/context.rs index a52097ab97..54969471bc 100644 --- a/src/context.rs +++ b/src/context.rs @@ -815,6 +815,12 @@ impl Context { .await? .to_string(), ); + res.insert( + "last_cant_decrypt_outgoing_msgs", + self.get_config_int(Config::LastCantDecryptOutgoingMsgs) + .await? + .to_string(), + ); res.insert( "scan_all_folders_debounce_secs", self.get_config_int(Config::ScanAllFoldersDebounceSecs) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 739a9653f3..69df2d4239 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -69,6 +69,8 @@ pub(crate) struct MimeMessage { /// Whether the From address was repeated in the signed part /// (and we know that the signer intended to send from this address) pub from_is_signed: bool, + /// Whether the message is incoming or outgoing (self-sent). + pub incoming: bool, /// The List-Post address is only set for mailing lists. Users can send /// messages to this address to post them to the list. pub list_post: Option, @@ -396,6 +398,7 @@ impl MimeMessage { } } + let incoming = !context.is_self_addr(&from.addr).await?; let mut parser = MimeMessage { parts: Vec::new(), headers, @@ -403,6 +406,7 @@ impl MimeMessage { list_post, from, from_is_signed, + incoming, chat_disposition_notification_to, decryption_info, decrypting_failed: mail.is_err(), diff --git a/src/receive_imf.rs b/src/receive_imf.rs index e2cc72dde1..355cba10c7 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -38,7 +38,7 @@ use crate::simplify; use crate::sql; use crate::stock_str; use crate::sync::Sync::*; -use crate::tools::{buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters}; +use crate::tools::{self, buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters}; use crate::{contact, imap}; /// This is the struct that is returned after receiving one email (aka MIME message). @@ -220,7 +220,6 @@ pub(crate) async fn receive_imf_inner( context, "Receiving message {rfc724_mid_orig:?}, seen={seen}...", ); - let incoming = !context.is_self_addr(&mime_parser.from.addr).await?; // check, if the mail is already in our database. // make sure, this check is done eg. before securejoin-processing. @@ -278,7 +277,7 @@ pub(crate) async fn receive_imf_inner( // Need to update chat id in the db. } else if let Some(msg_id) = replace_msg_id { info!(context, "Message is already downloaded."); - if incoming { + if mime_parser.incoming { return Ok(None); } // For the case if we missed a successful SMTP response. Be optimistic that the message is @@ -331,7 +330,7 @@ pub(crate) async fn receive_imf_inner( let to_ids = add_or_lookup_contacts_by_address_list( context, &mime_parser.recipients, - if !incoming { + if !mime_parser.incoming { Origin::OutgoingTo } else if incoming_origin.is_known() { Origin::IncomingTo @@ -346,7 +345,7 @@ pub(crate) async fn receive_imf_inner( let received_msg; if mime_parser.get_header(HeaderDef::SecureJoin).is_some() { let res; - if incoming { + if mime_parser.incoming { res = handle_securejoin_handshake(context, &mime_parser, from_id) .await .context("error in Secure-Join message handling")?; @@ -413,7 +412,6 @@ pub(crate) async fn receive_imf_inner( context, &mut mime_parser, imf_raw, - incoming, &to_ids, rfc724_mid_orig, from_id, @@ -571,7 +569,7 @@ pub(crate) async fn receive_imf_inner( } else if !chat_id.is_trash() { let fresh = received_msg.state == MessageState::InFresh; for msg_id in &received_msg.msg_ids { - chat_id.emit_msg_event(context, *msg_id, incoming && fresh); + chat_id.emit_msg_event(context, *msg_id, mime_parser.incoming && fresh); } } context.new_msgs_notify.notify_one(); @@ -647,7 +645,6 @@ async fn add_parts( context: &Context, mime_parser: &mut MimeMessage, imf_raw: &[u8], - incoming: bool, to_ids: &[ContactId], rfc724_mid: &str, from_id: ContactId, @@ -715,8 +712,9 @@ async fn add_parts( // (of course, the user can add other chats manually later) let to_id: ContactId; let state: MessageState; + let mut hidden = false; let mut needs_delete_job = false; - if incoming { + if mime_parser.incoming { to_id = ContactId::SELF; let test_normal_chat = if from_id == ContactId::UNDEFINED { @@ -1013,6 +1011,34 @@ async fn add_parts( } } + if mime_parser.decrypting_failed && !fetching_existing_messages { + if chat_id.is_none() { + chat_id = Some(DC_CHAT_ID_TRASH); + } else { + hidden = true; + } + let last_time = context + .get_config_i64(Config::LastCantDecryptOutgoingMsgs) + .await?; + let now = tools::time(); + let update_config = if last_time.saturating_add(24 * 60 * 60) <= now { + let mut msg = Message::new(Viewtype::Text); + msg.text = stock_str::cant_decrypt_outgoing_msgs(context).await; + chat::add_device_msg(context, None, Some(&mut msg)) + .await + .log_err(context) + .ok(); + true + } else { + last_time > now + }; + if update_config { + context + .set_config(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string())) + .await?; + } + } + if !to_ids.is_empty() { if chat_id.is_none() { if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group( @@ -1155,7 +1181,7 @@ async fn add_parts( context, mime_parser.timestamp_sent, sort_to_bottom, - incoming, + mime_parser.incoming, ) .await?; @@ -1249,7 +1275,7 @@ async fn add_parts( // -> Showing info messages everytime would be a lot of noise // 3. The info messages that are shown to the user ("Your chat partner // likely reinstalled DC" or similar) would be wrong. - if chat.is_protected() && (incoming || chat.typ != Chattype::Single) { + if chat.is_protected() && (mime_parser.incoming || chat.typ != Chattype::Single) { if let VerifiedEncryption::NotVerified(err) = verified_encryption { warn!(context, "Verification problem: {err:#}."); let s = format!("{err}. See 'Info' for more details"); @@ -1415,7 +1441,7 @@ INSERT INTO msgs rfc724_mid, chat_id, from_id, to_id, timestamp, timestamp_sent, timestamp_rcvd, type, state, msgrmsg, - txt, subject, txt_raw, param, + txt, subject, txt_raw, param, hidden, bytes, mime_headers, mime_compressed, mime_in_reply_to, mime_references, mime_modified, error, ephemeral_timer, ephemeral_timestamp, download_state, hop_info @@ -1424,7 +1450,7 @@ INSERT INTO msgs ?, ?, ?, ?, ?, ?, ?, ?, ?, - ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ? @@ -1434,7 +1460,7 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id, from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent, type=excluded.type, msgrmsg=excluded.msgrmsg, txt=excluded.txt, subject=excluded.subject, txt_raw=excluded.txt_raw, param=excluded.param, - bytes=excluded.bytes, mime_headers=excluded.mime_headers, + hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers, mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to, mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer, ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info @@ -1461,6 +1487,7 @@ RETURNING id } else { param.to_string() }, + hidden, part.bytes as isize, if (save_mime_headers || mime_modified) && !trash { mime_headers.clone() @@ -1526,7 +1553,7 @@ RETURNING id ); // new outgoing message from another device marks the chat as noticed. - if !incoming && !chat_id.is_special() { + if !mime_parser.incoming && !chat_id.is_special() { chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?; } @@ -1549,7 +1576,7 @@ RETURNING id } } - if !incoming && is_mdn && is_dc_message == MessengerMessage::Yes { + if !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes { // Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all // outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN, // delete it. diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index d871531a19..7b08c704a8 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -28,11 +28,24 @@ async fn test_grpid_simple() { let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None) .await .unwrap(); + assert_eq!(mimeparser.incoming, true); assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None); let grpid = Some("HcxyMARjyJy"); assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_outgoing() -> Result<()> { + let context = TestContext::new_alice().await; + let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + \n\ + hello"; + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await?; + assert_eq!(mimeparser.incoming, false); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_bad_from() { let context = TestContext::new_alice().await; @@ -3219,6 +3232,42 @@ async fn test_blocked_contact_creates_group() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_outgoing_undecryptable() -> Result<()> { + let alice = &TestContext::new().await; + alice.configure_addr("alice@example.org").await; + + let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); + receive_imf(alice, raw, false).await?; + + let bob_contact_id = Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo) + .await? + .unwrap(); + assert!(ChatId::lookup_by_contact(alice, bob_contact_id) + .await? + .is_none()); + + let dev_chat_id = ChatId::lookup_by_contact(alice, ContactId::DEVICE) + .await? + .unwrap(); + let dev_msg = alice.get_last_msg_in(dev_chat_id).await; + assert!(dev_msg.error().is_none()); + assert!(dev_msg + .text + .contains(&stock_str::cant_decrypt_outgoing_msgs(alice).await)); + + let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml"); + receive_imf(alice, raw, false).await?; + + assert!(ChatId::lookup_by_contact(alice, bob_contact_id) + .await? + .is_none()); + // The device message mustn't be added too frequently. + assert_eq!(alice.get_last_msg_in(dev_chat_id).await.id, dev_msg.id); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_thunderbird_autocrypt() -> Result<()> { let t = TestContext::new_bob().await; diff --git a/src/stock_str.rs b/src/stock_str.rs index f64a3685c4..70853eb3f5 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -424,6 +424,11 @@ pub enum StockMessage { fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet." ))] InvalidUnencryptedMail = 174, + + #[strum(props( + fallback = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions." + ))] + CantDecryptOutgoingMsgs = 175, } impl StockMessage { @@ -750,6 +755,11 @@ pub(crate) async fn cant_decrypt_msg_body(context: &Context) -> String { translated(context, StockMessage::CantDecryptMsgBody).await } +/// Stock string:`Got outgoing message(s) encrypted for another setup...`. +pub(crate) async fn cant_decrypt_outgoing_msgs(context: &Context) -> String { + translated(context, StockMessage::CantDecryptOutgoingMsgs).await +} + /// Stock string: `Fingerprints`. pub(crate) async fn finger_prints(context: &Context) -> String { translated(context, StockMessage::FingerPrints).await diff --git a/test-data/message/thunderbird_encrypted_signed.eml b/test-data/message/thunderbird_encrypted_signed.eml index 245ea66e56..50bfd0911d 100644 --- a/test-data/message/thunderbird_encrypted_signed.eml +++ b/test-data/message/thunderbird_encrypted_signed.eml @@ -9,8 +9,6 @@ User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Content-Language: en-US To: bob@example.net From: Alice -X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; - attachmentreminder=0; deliveryformat=0 X-Identity-Key: id3 Fcc: imap://alice%40example.org@in.example.org/Sent Subject: ...