Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 39 additions & 26 deletions src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,10 @@ impl ChatId {
let chat = Chat::load_from_db(context, chat_id).await?;

if chat.is_encrypted(context).await? {
chat_id.add_encrypted_msg(context, timestamp).await?;
let respect_delayed_msgs = true;
chat_id
.add_encrypted_msg(context, timestamp, respect_delayed_msgs)
.await?;
}

info!(
Expand Down Expand Up @@ -459,15 +462,23 @@ impl ChatId {
}

/// Adds message "Messages are end-to-end encrypted".
async fn add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> {
async fn add_encrypted_msg(
self,
context: &Context,
timestamp_sent: i64,
respect_delayed_msgs: bool,
) -> Result<()> {
let text = stock_str::messages_e2e_encrypted(context).await;
add_info_msg_with_cmd(
context,
self,
&text,
SystemMessage::ChatE2ee,
timestamp_sort,
None,
// Create a time window for delayed encrypted messages so that they are sorted under
// "Messages are end-to-end encrypted." This way time still monotonically increases and
// there's no magic "N years ago" which should be adjusted in the future.
timestamp_sent / if respect_delayed_msgs { 2 } else { 1 },
Some(timestamp_sent),
None,
None,
None,
Expand Down Expand Up @@ -1218,37 +1229,34 @@ impl ChatId {
)
.await?
} else if received {
// Received messages shouldn't mingle with just sent ones and appear somewhere in the
// middle of the chat, so we go after the newest non fresh message.
//
// But if a received outgoing message is older than some seen message, better sort the
// received message purely by timestamp. We could place it just before that seen
// message, but anyway the user may not notice it.
// Received incoming messages shouldn't mingle with just sent ones and appear somewhere
// in the middle of the chat, so we go after the newest non fresh message. Received
// outgoing messages are allowed to mingle with seen messages though to avoid seen
// replies appearing before messages sent from another device (cases like the user
// sharing the account with others or bots are rare, so let them break sometimes).
//
// NB: Received outgoing messages may break sorting of fresh incoming ones, but this
// shouldn't happen frequently. Seen incoming messages don't really break sorting of
// fresh ones, they rather mean that older incoming messages are actually seen as well.
let states = match incoming {
true => "13, 16, 18, 20, 24, 26", // `> MessageState::InFresh`
false => "18, 20, 24, 26", // `> MessageState::InSeen`
};
context
.sql
.query_row_optional(
"SELECT MAX(timestamp), MAX(IIF(state=?,timestamp_sent,0))
FROM msgs
WHERE chat_id=? AND hidden=0 AND state>?
HAVING COUNT(*) > 0",
(MessageState::InSeen, self, MessageState::InFresh),
&format!(
"SELECT MAX(timestamp) FROM msgs
WHERE state IN ({states}) AND hidden=0 AND chat_id=?
HAVING COUNT(*) > 0"
),
(self,),
|row| {
let ts: i64 = row.get(0)?;
let ts_sent_seen: i64 = row.get(1)?;
Ok((ts, ts_sent_seen))
Ok(ts)
},
)
.await?
.and_then(|(ts, ts_sent_seen)| {
match incoming || ts_sent_seen <= message_timestamp {
true => Some(ts),
false => None,
}
})
} else {
None
};
Expand Down Expand Up @@ -2423,7 +2431,10 @@ impl ChatIdBlocked {
&& !chat.param.exists(Param::Devicetalk)
&& !chat.param.exists(Param::Selftalk)
{
chat_id.add_encrypted_msg(context, smeared_time).await?;
let respect_delayed_msgs = true;
chat_id
.add_encrypted_msg(context, smeared_time, respect_delayed_msgs)
.await?;
}

Ok(Self {
Expand Down Expand Up @@ -3449,8 +3460,10 @@ pub(crate) async fn create_group_ex(
chatlist_events::emit_chatlist_item_changed(context, chat_id);

if is_encrypted {
// Add "Messages are end-to-end encrypted." message.
chat_id.add_encrypted_msg(context, timestamp).await?;
let respect_delayed_msgs = false;
chat_id
.add_encrypted_msg(context, timestamp, respect_delayed_msgs)
.await?;
}

if !context.get_config_bool(Config::Bot).await?
Expand Down
2 changes: 1 addition & 1 deletion src/events/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub enum EventType {
/// Emitted when an IMAP message has been moved
ImapMessageMoved(String),

/// Emitted before going into IDLE on the Inbox folder.
/// Emitted before going into IDLE on any folder.
ImapInboxIdle,

/// Emitted when an new file in the $BLOBDIR was created
Expand Down
26 changes: 17 additions & 9 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,17 @@ impl MsgId {
.sql
.query_row_optional(
concat!(
"SELECT m.state, mdns.msg_id",
"SELECT m.state, m.from_id, mdns.msg_id",
" FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
" WHERE id=?",
" LIMIT 1",
),
(self,),
|row| {
let state: MessageState = row.get(0)?;
let mdn_msg_id: Option<MsgId> = row.get(1)?;
Ok(state.with_mdns(mdn_msg_id.is_some()))
let from_id: ContactId = row.get(1)?;
let mdn_msg_id: Option<MsgId> = row.get(2)?;
Ok(state.with(from_id, mdn_msg_id.is_some()))
},
)
.await?
Expand Down Expand Up @@ -551,22 +552,23 @@ impl Message {
}
_ => String::new(),
};
let from_id = row.get("from_id")?;
let msg = Message {
id: row.get("id")?,
rfc724_mid: row.get::<_, String>("rfc724mid")?,
in_reply_to: row
.get::<_, Option<String>>("mime_in_reply_to")?
.and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
chat_id: row.get("chat_id")?,
from_id: row.get("from_id")?,
from_id,
to_id: row.get("to_id")?,
timestamp_sort: row.get("timestamp")?,
timestamp_sent: row.get("timestamp_sent")?,
timestamp_rcvd: row.get("timestamp_rcvd")?,
ephemeral_timer: row.get("ephemeral_timer")?,
ephemeral_timestamp: row.get("ephemeral_timestamp")?,
viewtype: row.get("type").unwrap_or_default(),
state: state.with_mdns(mdn_msg_id.is_some()),
state: state.with(from_id, mdn_msg_id.is_some()),
download_state: row.get("download_state")?,
error: Some(row.get::<_, String>("error")?)
.filter(|error| !error.is_empty()),
Expand Down Expand Up @@ -1366,7 +1368,7 @@ pub enum MessageState {
OutDelivered = 26,

/// Outgoing message read by the recipient (two checkmarks; this
/// requires goodwill on the receiver's side). Not used in the db for new messages.
/// requires goodwill on the receiver's side). API-only, not used in the db.
OutMdnRcvd = 28,
}

Expand Down Expand Up @@ -1410,10 +1412,16 @@ impl MessageState {
)
}

/// Returns adjusted message state if the message has MDNs.
pub(crate) fn with_mdns(self, has_mdns: bool) -> Self {
/// Returns adjusted message state.
pub(crate) fn with(mut self, from_id: ContactId, has_mdns: bool) -> Self {
if MessageState::InFresh <= self
&& self <= MessageState::InSeen
&& from_id == ContactId::SELF
{
self = MessageState::OutDelivered;
}
if self == MessageState::OutDelivered && has_mdns {
return MessageState::OutMdnRcvd;
self = MessageState::OutMdnRcvd;
}
self
}
Expand Down
7 changes: 4 additions & 3 deletions src/receive_imf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1692,15 +1692,14 @@ async fn add_parts(
};

let state = if !mime_parser.incoming {
MessageState::OutDelivered
MessageState::InFresh
} else if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
// No check for `hidden` because only reactions are such and they should be `InFresh`.
{
MessageState::InSeen
} else {
MessageState::InFresh
};
let in_fresh = state == MessageState::InFresh;

let sort_to_bottom = false;
let received = true;
Expand Down Expand Up @@ -1997,7 +1996,7 @@ async fn add_parts(
save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
let save_mime_modified = save_mime_modified && parts.peek().is_none();

let ephemeral_timestamp = if in_fresh {
let ephemeral_timestamp = if state == MessageState::InFresh {
0
} else {
match ephemeral_timer {
Expand Down Expand Up @@ -2109,6 +2108,8 @@ RETURNING id
ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
created_db_entries.push(row_id);
}
let has_mdns = false;
let state = state.with(from_id, has_mdns);

// Maybe set logging xdc and add gossip topics for webxdcs.
for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
Expand Down
12 changes: 12 additions & 0 deletions src/sql/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,18 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
.await?;
}

inc_and_check(&mut migration_version, 135)?;
if dbversion < migration_version {
// Tweak the index for `chat::calc_sort_timestamp()`.
sql.execute_migration(
"UPDATE msgs SET state=26 WHERE state=28;
DROP INDEX IF EXISTS msgs_index7;
CREATE INDEX msgs_index7 ON msgs (state, hidden, chat_id, timestamp);",
migration_version,
)
.await?;
}

let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
Expand Down
30 changes: 23 additions & 7 deletions src/tests/verified_chats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,11 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
/// This test tests that the messages are still in the right order.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_4() -> Result<()> {
let alice = TestContext::new_alice().await;
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let msg_incoming = receive_imf(
&alice,
alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2-3@example.org>\n\
Expand All @@ -227,7 +229,7 @@ async fn test_old_message_4() -> Result<()> {
.unwrap();

let msg_sent = receive_imf(
&alice,
alice,
b"From: alice@example.org\n\
To: Bob <bob@example.net>\n\
Message-ID: <1234-2-4@example.org>\n\
Expand All @@ -242,12 +244,22 @@ async fn test_old_message_4() -> Result<()> {
// The "Happy birthday" message should be shown first, and then the "Thanks" message
assert!(msg_sent.sort_timestamp < msg_incoming.sort_timestamp);

// And now the same for encrypted messages.
let msg_incoming = tcm.send_recv(bob, alice, "Thanks, Alice!").await;
message::markseen_msgs(alice, vec![msg_incoming.id]).await?;
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
let msg_sent = receive_imf(alice, raw, true).await?.unwrap();
assert_eq!(msg_sent.chat_id, msg_incoming.chat_id);
assert!(msg_sent.sort_timestamp < msg_incoming.timestamp_sort);
alice
.golden_test_chat(msg_sent.chat_id, "test_old_message_4")
.await;
Ok(())
}

/// Alice is offline for some time.
/// When they come online, first their sentbox is synced and then their inbox.
/// This test tests that the messages are still in the right order.
/// Alice's device#0 is offline for some time.
/// When it comes online, it sees a message from another device and an incoming message. Messages
/// may come from different folders.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_5() -> Result<()> {
let alice = TestContext::new_alice().await;
Expand Down Expand Up @@ -277,7 +289,11 @@ async fn test_old_message_5() -> Result<()> {
.await?
.unwrap();

assert!(msg_sent.sort_timestamp == msg_incoming.sort_timestamp);
// If the messages come from the same folder and `msg_sent` is sent by Alice, it's better to
// sort `msg_incoming` after it so that it's more visible. Messages coming from different
// folders are a rare case now, but if Alice shares her account with someone else or has some
// auto-reply bot, messages should be sorted just by "Date".
assert!(msg_incoming.sort_timestamp < msg_sent.sort_timestamp);
alice
.golden_test_chat(msg_sent.chat_id, "test_old_message_5")
.await;
Expand Down
6 changes: 6 additions & 0 deletions test-data/golden/test_old_message_4
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Single#Chat#11: bob@example.net [KEY bob@example.net]
--------------------------------------------------------------------------------
Msg#12: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#14🔒: Me (Contact#Contact#Self): Test – This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √
Msg#13🔒: (Contact#Contact#11): Thanks, Alice! [SEEN]
--------------------------------------------------------------------------------
2 changes: 1 addition & 1 deletion test-data/golden/test_old_message_5
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Single#Chat#10: Bob [bob@example.net] Icon: 4138c52e5bc1c576cda7dd44d088c07.png
--------------------------------------------------------------------------------
Msg#10: Me (Contact#Contact#Self): Happy birthday, Bob! √
Msg#11: (Contact#Contact#10): Happy birthday to me, Alice! [FRESH]
Msg#10: Me (Contact#Contact#Self): Happy birthday, Bob! √
--------------------------------------------------------------------------------
2 changes: 1 addition & 1 deletion test-data/golden/two_group_securejoins
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Group#Chat#11: Group [3 member(s)]
--------------------------------------------------------------------------------
Msg#12: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#13: info (Contact#Contact#Info): alice@example.org invited you to join this group.

Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
Msg#15: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
Msg#12: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#17🔒: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO]
--------------------------------------------------------------------------------