mirror of
https://github.com/chatmail/core.git
synced 2026-05-06 06:46:35 +03:00
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.
This commit is contained in:
@@ -250,6 +250,7 @@ describe('Basic offline Tests', function () {
|
|||||||
'journal_mode',
|
'journal_mode',
|
||||||
'key_gen_type',
|
'key_gen_type',
|
||||||
'last_housekeeping',
|
'last_housekeeping',
|
||||||
|
'last_cant_decrypt_outgoing_msgs',
|
||||||
'level',
|
'level',
|
||||||
'mdns_enabled',
|
'mdns_enabled',
|
||||||
'media_quality',
|
'media_quality',
|
||||||
|
|||||||
@@ -291,6 +291,9 @@ pub enum Config {
|
|||||||
/// Timestamp of the last time housekeeping was run
|
/// Timestamp of the last time housekeeping was run
|
||||||
LastHousekeeping,
|
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.
|
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
|
||||||
#[strum(props(default = "60"))]
|
#[strum(props(default = "60"))]
|
||||||
ScanAllFoldersDebounceSecs,
|
ScanAllFoldersDebounceSecs,
|
||||||
|
|||||||
@@ -815,6 +815,12 @@ impl Context {
|
|||||||
.await?
|
.await?
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
res.insert(
|
||||||
|
"last_cant_decrypt_outgoing_msgs",
|
||||||
|
self.get_config_int(Config::LastCantDecryptOutgoingMsgs)
|
||||||
|
.await?
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
res.insert(
|
res.insert(
|
||||||
"scan_all_folders_debounce_secs",
|
"scan_all_folders_debounce_secs",
|
||||||
self.get_config_int(Config::ScanAllFoldersDebounceSecs)
|
self.get_config_int(Config::ScanAllFoldersDebounceSecs)
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ pub(crate) struct MimeMessage {
|
|||||||
/// Whether the From address was repeated in the signed part
|
/// Whether the From address was repeated in the signed part
|
||||||
/// (and we know that the signer intended to send from this address)
|
/// (and we know that the signer intended to send from this address)
|
||||||
pub from_is_signed: bool,
|
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
|
/// The List-Post address is only set for mailing lists. Users can send
|
||||||
/// messages to this address to post them to the list.
|
/// messages to this address to post them to the list.
|
||||||
pub list_post: Option<String>,
|
pub list_post: Option<String>,
|
||||||
@@ -396,6 +398,7 @@ impl MimeMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let incoming = !context.is_self_addr(&from.addr).await?;
|
||||||
let mut parser = MimeMessage {
|
let mut parser = MimeMessage {
|
||||||
parts: Vec::new(),
|
parts: Vec::new(),
|
||||||
headers,
|
headers,
|
||||||
@@ -403,6 +406,7 @@ impl MimeMessage {
|
|||||||
list_post,
|
list_post,
|
||||||
from,
|
from,
|
||||||
from_is_signed,
|
from_is_signed,
|
||||||
|
incoming,
|
||||||
chat_disposition_notification_to,
|
chat_disposition_notification_to,
|
||||||
decryption_info,
|
decryption_info,
|
||||||
decrypting_failed: mail.is_err(),
|
decrypting_failed: mail.is_err(),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ use crate::simplify;
|
|||||||
use crate::sql;
|
use crate::sql;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
use crate::sync::Sync::*;
|
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};
|
use crate::{contact, imap};
|
||||||
|
|
||||||
/// This is the struct that is returned after receiving one email (aka MIME message).
|
/// 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,
|
context,
|
||||||
"Receiving message {rfc724_mid_orig:?}, seen={seen}...",
|
"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.
|
// check, if the mail is already in our database.
|
||||||
// make sure, this check is done eg. before securejoin-processing.
|
// 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.
|
// Need to update chat id in the db.
|
||||||
} else if let Some(msg_id) = replace_msg_id {
|
} else if let Some(msg_id) = replace_msg_id {
|
||||||
info!(context, "Message is already downloaded.");
|
info!(context, "Message is already downloaded.");
|
||||||
if incoming {
|
if mime_parser.incoming {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
// For the case if we missed a successful SMTP response. Be optimistic that the message is
|
// 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(
|
let to_ids = add_or_lookup_contacts_by_address_list(
|
||||||
context,
|
context,
|
||||||
&mime_parser.recipients,
|
&mime_parser.recipients,
|
||||||
if !incoming {
|
if !mime_parser.incoming {
|
||||||
Origin::OutgoingTo
|
Origin::OutgoingTo
|
||||||
} else if incoming_origin.is_known() {
|
} else if incoming_origin.is_known() {
|
||||||
Origin::IncomingTo
|
Origin::IncomingTo
|
||||||
@@ -346,7 +345,7 @@ pub(crate) async fn receive_imf_inner(
|
|||||||
let received_msg;
|
let received_msg;
|
||||||
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
|
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
|
||||||
let res;
|
let res;
|
||||||
if incoming {
|
if mime_parser.incoming {
|
||||||
res = handle_securejoin_handshake(context, &mime_parser, from_id)
|
res = handle_securejoin_handshake(context, &mime_parser, from_id)
|
||||||
.await
|
.await
|
||||||
.context("error in Secure-Join message handling")?;
|
.context("error in Secure-Join message handling")?;
|
||||||
@@ -413,7 +412,6 @@ pub(crate) async fn receive_imf_inner(
|
|||||||
context,
|
context,
|
||||||
&mut mime_parser,
|
&mut mime_parser,
|
||||||
imf_raw,
|
imf_raw,
|
||||||
incoming,
|
|
||||||
&to_ids,
|
&to_ids,
|
||||||
rfc724_mid_orig,
|
rfc724_mid_orig,
|
||||||
from_id,
|
from_id,
|
||||||
@@ -571,7 +569,7 @@ pub(crate) async fn receive_imf_inner(
|
|||||||
} else if !chat_id.is_trash() {
|
} else if !chat_id.is_trash() {
|
||||||
let fresh = received_msg.state == MessageState::InFresh;
|
let fresh = received_msg.state == MessageState::InFresh;
|
||||||
for msg_id in &received_msg.msg_ids {
|
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();
|
context.new_msgs_notify.notify_one();
|
||||||
@@ -647,7 +645,6 @@ async fn add_parts(
|
|||||||
context: &Context,
|
context: &Context,
|
||||||
mime_parser: &mut MimeMessage,
|
mime_parser: &mut MimeMessage,
|
||||||
imf_raw: &[u8],
|
imf_raw: &[u8],
|
||||||
incoming: bool,
|
|
||||||
to_ids: &[ContactId],
|
to_ids: &[ContactId],
|
||||||
rfc724_mid: &str,
|
rfc724_mid: &str,
|
||||||
from_id: ContactId,
|
from_id: ContactId,
|
||||||
@@ -715,8 +712,9 @@ async fn add_parts(
|
|||||||
// (of course, the user can add other chats manually later)
|
// (of course, the user can add other chats manually later)
|
||||||
let to_id: ContactId;
|
let to_id: ContactId;
|
||||||
let state: MessageState;
|
let state: MessageState;
|
||||||
|
let mut hidden = false;
|
||||||
let mut needs_delete_job = false;
|
let mut needs_delete_job = false;
|
||||||
if incoming {
|
if mime_parser.incoming {
|
||||||
to_id = ContactId::SELF;
|
to_id = ContactId::SELF;
|
||||||
|
|
||||||
let test_normal_chat = if from_id == ContactId::UNDEFINED {
|
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 !to_ids.is_empty() {
|
||||||
if chat_id.is_none() {
|
if chat_id.is_none() {
|
||||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
||||||
@@ -1155,7 +1181,7 @@ async fn add_parts(
|
|||||||
context,
|
context,
|
||||||
mime_parser.timestamp_sent,
|
mime_parser.timestamp_sent,
|
||||||
sort_to_bottom,
|
sort_to_bottom,
|
||||||
incoming,
|
mime_parser.incoming,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -1249,7 +1275,7 @@ async fn add_parts(
|
|||||||
// -> Showing info messages everytime would be a lot of noise
|
// -> Showing info messages everytime would be a lot of noise
|
||||||
// 3. The info messages that are shown to the user ("Your chat partner
|
// 3. The info messages that are shown to the user ("Your chat partner
|
||||||
// likely reinstalled DC" or similar) would be wrong.
|
// 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 {
|
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
|
||||||
warn!(context, "Verification problem: {err:#}.");
|
warn!(context, "Verification problem: {err:#}.");
|
||||||
let s = format!("{err}. See 'Info' for more details");
|
let s = format!("{err}. See 'Info' for more details");
|
||||||
@@ -1415,7 +1441,7 @@ INSERT INTO msgs
|
|||||||
rfc724_mid, chat_id,
|
rfc724_mid, chat_id,
|
||||||
from_id, to_id, timestamp, timestamp_sent,
|
from_id, to_id, timestamp, timestamp_sent,
|
||||||
timestamp_rcvd, type, state, msgrmsg,
|
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,
|
bytes, mime_headers, mime_compressed, mime_in_reply_to,
|
||||||
mime_references, mime_modified, error, ephemeral_timer,
|
mime_references, mime_modified, error, ephemeral_timer,
|
||||||
ephemeral_timestamp, download_state, hop_info
|
ephemeral_timestamp, download_state, hop_info
|
||||||
@@ -1424,7 +1450,7 @@ INSERT INTO msgs
|
|||||||
?,
|
?,
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?,
|
||||||
?, ?, ?, ?,
|
?, ?, ?, ?, ?,
|
||||||
?, ?, ?, ?, 1,
|
?, ?, ?, ?, 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,
|
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
|
||||||
type=excluded.type, msgrmsg=excluded.msgrmsg,
|
type=excluded.type, msgrmsg=excluded.msgrmsg,
|
||||||
txt=excluded.txt, subject=excluded.subject, txt_raw=excluded.txt_raw, param=excluded.param,
|
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_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,
|
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
|
ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
|
||||||
@@ -1461,6 +1487,7 @@ RETURNING id
|
|||||||
} else {
|
} else {
|
||||||
param.to_string()
|
param.to_string()
|
||||||
},
|
},
|
||||||
|
hidden,
|
||||||
part.bytes as isize,
|
part.bytes as isize,
|
||||||
if (save_mime_headers || mime_modified) && !trash {
|
if (save_mime_headers || mime_modified) && !trash {
|
||||||
mime_headers.clone()
|
mime_headers.clone()
|
||||||
@@ -1526,7 +1553,7 @@ RETURNING id
|
|||||||
);
|
);
|
||||||
|
|
||||||
// new outgoing message from another device marks the chat as noticed.
|
// 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?;
|
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
|
// 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,
|
// outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN,
|
||||||
// delete it.
|
// delete it.
|
||||||
|
|||||||
@@ -28,11 +28,24 @@ async fn test_grpid_simple() {
|
|||||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
assert_eq!(mimeparser.incoming, true);
|
||||||
assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None);
|
assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None);
|
||||||
let grpid = Some("HcxyMARjyJy");
|
let grpid = Some("HcxyMARjyJy");
|
||||||
assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid);
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_bad_from() {
|
async fn test_bad_from() {
|
||||||
let context = TestContext::new_alice().await;
|
let context = TestContext::new_alice().await;
|
||||||
@@ -3219,6 +3232,42 @@ async fn test_blocked_contact_creates_group() -> Result<()> {
|
|||||||
Ok(())
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_thunderbird_autocrypt() -> Result<()> {
|
async fn test_thunderbird_autocrypt() -> Result<()> {
|
||||||
let t = TestContext::new_bob().await;
|
let t = TestContext::new_bob().await;
|
||||||
|
|||||||
@@ -424,6 +424,11 @@ pub enum StockMessage {
|
|||||||
fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
|
fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
|
||||||
))]
|
))]
|
||||||
InvalidUnencryptedMail = 174,
|
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 {
|
impl StockMessage {
|
||||||
@@ -750,6 +755,11 @@ pub(crate) async fn cant_decrypt_msg_body(context: &Context) -> String {
|
|||||||
translated(context, StockMessage::CantDecryptMsgBody).await
|
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`.
|
/// Stock string: `Fingerprints`.
|
||||||
pub(crate) async fn finger_prints(context: &Context) -> String {
|
pub(crate) async fn finger_prints(context: &Context) -> String {
|
||||||
translated(context, StockMessage::FingerPrints).await
|
translated(context, StockMessage::FingerPrints).await
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
|
|||||||
Content-Language: en-US
|
Content-Language: en-US
|
||||||
To: bob@example.net
|
To: bob@example.net
|
||||||
From: Alice <alice@example.org>
|
From: Alice <alice@example.org>
|
||||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
|
||||||
attachmentreminder=0; deliveryformat=0
|
|
||||||
X-Identity-Key: id3
|
X-Identity-Key: id3
|
||||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||||
Subject: ...
|
Subject: ...
|
||||||
|
|||||||
Reference in New Issue
Block a user