Skip to content

Commit ba35e83

Browse files
committed
feat: Add device message about outgoing undecryptable messages (#5164)
Currently when a user sets up another device by logging in, a new key is created. If a message is sent from either device outside, it cannot be decrypted by the other device. The message is replaced with square bracket error like this: ``` <string name="systemmsg_cannot_decrypt">This message cannot be decrypted.\n\n• It might already help to simply reply to this message and ask the sender to send the message again.\n\n• If you just re-installed Delta Chat then it is best if you re-setup Delta Chat now and choose "Add as second device" or import a backup.</string> ``` (taken from Android repo `res/values/strings.xml`) If the message is outgoing, it does not help to "simply reply to this message". Instead, we should add a translatable device message of a special type so UI can link to the FAQ entry about second device. But let's limit such notifications to 1 per day. And as for the undecryptable message itself, let it go to Trash if it can't be assigned to a chat by its references.
1 parent 61a2c55 commit ba35e83

File tree

8 files changed

+116
-18
lines changed

8 files changed

+116
-18
lines changed

node/test/test.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ describe('Basic offline Tests', function () {
250250
'journal_mode',
251251
'key_gen_type',
252252
'last_housekeeping',
253+
'last_cant_decrypt_outgoing_msgs',
253254
'level',
254255
'mdns_enabled',
255256
'media_quality',

src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,9 @@ pub enum Config {
291291
/// Timestamp of the last time housekeeping was run
292292
LastHousekeeping,
293293

294+
/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
295+
LastCantDecryptOutgoingMsgs,
296+
294297
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
295298
#[strum(props(default = "60"))]
296299
ScanAllFoldersDebounceSecs,

src/context.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,12 @@ impl Context {
815815
.await?
816816
.to_string(),
817817
);
818+
res.insert(
819+
"last_cant_decrypt_outgoing_msgs",
820+
self.get_config_int(Config::LastCantDecryptOutgoingMsgs)
821+
.await?
822+
.to_string(),
823+
);
818824
res.insert(
819825
"scan_all_folders_debounce_secs",
820826
self.get_config_int(Config::ScanAllFoldersDebounceSecs)

src/mimeparser.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ pub(crate) struct MimeMessage {
6969
/// Whether the From address was repeated in the signed part
7070
/// (and we know that the signer intended to send from this address)
7171
pub from_is_signed: bool,
72+
/// Whether the message is incoming or outgoing (self-sent).
73+
pub incoming: bool,
7274
/// The List-Post address is only set for mailing lists. Users can send
7375
/// messages to this address to post them to the list.
7476
pub list_post: Option<String>,
@@ -396,13 +398,15 @@ impl MimeMessage {
396398
}
397399
}
398400

401+
let incoming = !context.is_self_addr(&from.addr).await?;
399402
let mut parser = MimeMessage {
400403
parts: Vec::new(),
401404
headers,
402405
recipients,
403406
list_post,
404407
from,
405408
from_is_signed,
409+
incoming,
406410
chat_disposition_notification_to,
407411
decryption_info,
408412
decrypting_failed: mail.is_err(),

src/receive_imf.rs

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use crate::simplify;
3838
use crate::sql;
3939
use crate::stock_str;
4040
use crate::sync::Sync::*;
41-
use crate::tools::{buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters};
41+
use crate::tools::{self, buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters};
4242
use crate::{contact, imap};
4343

4444
/// 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(
220220
context,
221221
"Receiving message {rfc724_mid_orig:?}, seen={seen}...",
222222
);
223-
let incoming = !context.is_self_addr(&mime_parser.from.addr).await?;
224223

225224
// check, if the mail is already in our database.
226225
// make sure, this check is done eg. before securejoin-processing.
@@ -278,7 +277,7 @@ pub(crate) async fn receive_imf_inner(
278277
// Need to update chat id in the db.
279278
} else if let Some(msg_id) = replace_msg_id {
280279
info!(context, "Message is already downloaded.");
281-
if incoming {
280+
if mime_parser.incoming {
282281
return Ok(None);
283282
}
284283
// 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(
331330
let to_ids = add_or_lookup_contacts_by_address_list(
332331
context,
333332
&mime_parser.recipients,
334-
if !incoming {
333+
if !mime_parser.incoming {
335334
Origin::OutgoingTo
336335
} else if incoming_origin.is_known() {
337336
Origin::IncomingTo
@@ -346,7 +345,7 @@ pub(crate) async fn receive_imf_inner(
346345
let received_msg;
347346
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
348347
let res;
349-
if incoming {
348+
if mime_parser.incoming {
350349
res = handle_securejoin_handshake(context, &mime_parser, from_id)
351350
.await
352351
.context("error in Secure-Join message handling")?;
@@ -413,7 +412,6 @@ pub(crate) async fn receive_imf_inner(
413412
context,
414413
&mut mime_parser,
415414
imf_raw,
416-
incoming,
417415
&to_ids,
418416
rfc724_mid_orig,
419417
from_id,
@@ -571,7 +569,7 @@ pub(crate) async fn receive_imf_inner(
571569
} else if !chat_id.is_trash() {
572570
let fresh = received_msg.state == MessageState::InFresh;
573571
for msg_id in &received_msg.msg_ids {
574-
chat_id.emit_msg_event(context, *msg_id, incoming && fresh);
572+
chat_id.emit_msg_event(context, *msg_id, mime_parser.incoming && fresh);
575573
}
576574
}
577575
context.new_msgs_notify.notify_one();
@@ -647,7 +645,6 @@ async fn add_parts(
647645
context: &Context,
648646
mime_parser: &mut MimeMessage,
649647
imf_raw: &[u8],
650-
incoming: bool,
651648
to_ids: &[ContactId],
652649
rfc724_mid: &str,
653650
from_id: ContactId,
@@ -715,8 +712,9 @@ async fn add_parts(
715712
// (of course, the user can add other chats manually later)
716713
let to_id: ContactId;
717714
let state: MessageState;
715+
let mut hidden = false;
718716
let mut needs_delete_job = false;
719-
if incoming {
717+
if mime_parser.incoming {
720718
to_id = ContactId::SELF;
721719

722720
let test_normal_chat = if from_id == ContactId::UNDEFINED {
@@ -1013,6 +1011,34 @@ async fn add_parts(
10131011
}
10141012
}
10151013

1014+
if mime_parser.decrypting_failed && !fetching_existing_messages {
1015+
if chat_id.is_none() {
1016+
chat_id = Some(DC_CHAT_ID_TRASH);
1017+
} else {
1018+
hidden = true;
1019+
}
1020+
let last_time = context
1021+
.get_config_i64(Config::LastCantDecryptOutgoingMsgs)
1022+
.await?;
1023+
let now = tools::time();
1024+
let update_config = if last_time.saturating_add(24 * 60 * 60) <= now {
1025+
let mut msg = Message::new(Viewtype::Text);
1026+
msg.text = stock_str::cant_decrypt_outgoing_msgs(context).await;
1027+
chat::add_device_msg(context, None, Some(&mut msg))
1028+
.await
1029+
.log_err(context)
1030+
.ok();
1031+
true
1032+
} else {
1033+
last_time > now
1034+
};
1035+
if update_config {
1036+
context
1037+
.set_config(Config::LastCantDecryptOutgoingMsgs, Some(&now.to_string()))
1038+
.await?;
1039+
}
1040+
}
1041+
10161042
if !to_ids.is_empty() {
10171043
if chat_id.is_none() {
10181044
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
@@ -1155,7 +1181,7 @@ async fn add_parts(
11551181
context,
11561182
mime_parser.timestamp_sent,
11571183
sort_to_bottom,
1158-
incoming,
1184+
mime_parser.incoming,
11591185
)
11601186
.await?;
11611187

@@ -1249,7 +1275,7 @@ async fn add_parts(
12491275
// -> Showing info messages everytime would be a lot of noise
12501276
// 3. The info messages that are shown to the user ("Your chat partner
12511277
// likely reinstalled DC" or similar) would be wrong.
1252-
if chat.is_protected() && (incoming || chat.typ != Chattype::Single) {
1278+
if chat.is_protected() && (mime_parser.incoming || chat.typ != Chattype::Single) {
12531279
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
12541280
warn!(context, "Verification problem: {err:#}.");
12551281
let s = format!("{err}. See 'Info' for more details");
@@ -1415,7 +1441,7 @@ INSERT INTO msgs
14151441
rfc724_mid, chat_id,
14161442
from_id, to_id, timestamp, timestamp_sent,
14171443
timestamp_rcvd, type, state, msgrmsg,
1418-
txt, subject, txt_raw, param,
1444+
txt, subject, txt_raw, param, hidden,
14191445
bytes, mime_headers, mime_compressed, mime_in_reply_to,
14201446
mime_references, mime_modified, error, ephemeral_timer,
14211447
ephemeral_timestamp, download_state, hop_info
@@ -1424,7 +1450,7 @@ INSERT INTO msgs
14241450
?,
14251451
?, ?, ?, ?,
14261452
?, ?, ?, ?,
1427-
?, ?, ?, ?,
1453+
?, ?, ?, ?, ?,
14281454
?, ?, ?, ?, 1,
14291455
?, ?, ?, ?,
14301456
?, ?, ?, ?
@@ -1434,7 +1460,7 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
14341460
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
14351461
type=excluded.type, msgrmsg=excluded.msgrmsg,
14361462
txt=excluded.txt, subject=excluded.subject, txt_raw=excluded.txt_raw, param=excluded.param,
1437-
bytes=excluded.bytes, mime_headers=excluded.mime_headers,
1463+
hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
14381464
mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
14391465
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
14401466
ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
@@ -1461,6 +1487,7 @@ RETURNING id
14611487
} else {
14621488
param.to_string()
14631489
},
1490+
hidden,
14641491
part.bytes as isize,
14651492
if (save_mime_headers || mime_modified) && !trash {
14661493
mime_headers.clone()
@@ -1526,7 +1553,7 @@ RETURNING id
15261553
);
15271554

15281555
// new outgoing message from another device marks the chat as noticed.
1529-
if !incoming && !chat_id.is_special() {
1556+
if !mime_parser.incoming && !chat_id.is_special() {
15301557
chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?;
15311558
}
15321559

@@ -1549,7 +1576,7 @@ RETURNING id
15491576
}
15501577
}
15511578

1552-
if !incoming && is_mdn && is_dc_message == MessengerMessage::Yes {
1579+
if !mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes {
15531580
// Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all
15541581
// outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN,
15551582
// delete it.

src/receive_imf/tests.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,24 @@ async fn test_grpid_simple() {
2828
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
2929
.await
3030
.unwrap();
31+
assert_eq!(mimeparser.incoming, true);
3132
assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None);
3233
let grpid = Some("HcxyMARjyJy");
3334
assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid);
3435
}
3536

37+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
38+
async fn test_outgoing() -> Result<()> {
39+
let context = TestContext::new_alice().await;
40+
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
41+
From: alice@example.org\n\
42+
\n\
43+
hello";
44+
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None).await?;
45+
assert_eq!(mimeparser.incoming, false);
46+
Ok(())
47+
}
48+
3649
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3750
async fn test_bad_from() {
3851
let context = TestContext::new_alice().await;
@@ -3219,6 +3232,42 @@ async fn test_blocked_contact_creates_group() -> Result<()> {
32193232
Ok(())
32203233
}
32213234

3235+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3236+
async fn test_outgoing_undecryptable() -> Result<()> {
3237+
let alice = &TestContext::new().await;
3238+
alice.configure_addr("alice@example.org").await;
3239+
3240+
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
3241+
receive_imf(alice, raw, false).await?;
3242+
3243+
let bob_contact_id = Contact::lookup_id_by_addr(alice, "bob@example.net", Origin::OutgoingTo)
3244+
.await?
3245+
.unwrap();
3246+
assert!(ChatId::lookup_by_contact(alice, bob_contact_id)
3247+
.await?
3248+
.is_none());
3249+
3250+
let dev_chat_id = ChatId::lookup_by_contact(alice, ContactId::DEVICE)
3251+
.await?
3252+
.unwrap();
3253+
let dev_msg = alice.get_last_msg_in(dev_chat_id).await;
3254+
assert!(dev_msg.error().is_none());
3255+
assert!(dev_msg
3256+
.text
3257+
.contains(&stock_str::cant_decrypt_outgoing_msgs(alice).await));
3258+
3259+
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
3260+
receive_imf(alice, raw, false).await?;
3261+
3262+
assert!(ChatId::lookup_by_contact(alice, bob_contact_id)
3263+
.await?
3264+
.is_none());
3265+
// The device message mustn't be added too frequently.
3266+
assert_eq!(alice.get_last_msg_in(dev_chat_id).await.id, dev_msg.id);
3267+
3268+
Ok(())
3269+
}
3270+
32223271
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
32233272
async fn test_thunderbird_autocrypt() -> Result<()> {
32243273
let t = TestContext::new_bob().await;

src/stock_str.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,11 @@ pub enum StockMessage {
424424
fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
425425
))]
426426
InvalidUnencryptedMail = 174,
427+
428+
#[strum(props(
429+
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."
430+
))]
431+
CantDecryptOutgoingMsgs = 175,
427432
}
428433

429434
impl StockMessage {
@@ -750,6 +755,11 @@ pub(crate) async fn cant_decrypt_msg_body(context: &Context) -> String {
750755
translated(context, StockMessage::CantDecryptMsgBody).await
751756
}
752757

758+
/// Stock string:`Got outgoing message(s) encrypted for another setup...`.
759+
pub(crate) async fn cant_decrypt_outgoing_msgs(context: &Context) -> String {
760+
translated(context, StockMessage::CantDecryptOutgoingMsgs).await
761+
}
762+
753763
/// Stock string: `Fingerprints`.
754764
pub(crate) async fn finger_prints(context: &Context) -> String {
755765
translated(context, StockMessage::FingerPrints).await

test-data/message/thunderbird_encrypted_signed.eml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
99
Content-Language: en-US
1010
To: bob@example.net
1111
From: Alice <alice@example.org>
12-
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
13-
attachmentreminder=0; deliveryformat=0
1412
X-Identity-Key: id3
1513
Fcc: imap://alice%40example.org@in.example.org/Sent
1614
Subject: ...

0 commit comments

Comments
 (0)