Compare commits

...

1 Commits

Author SHA1 Message Date
WofWca
c1a8fc54c3 fix: get_draft possibly returning non-draft msg
Due to a gap between `get_draft_msg_id()` and `Message::load_from_db`.
Possibly can happen if the draft gets sent
while `get_draft()` is in progress.

Solved by doing both queries in a transaction.
2026-08-02 23:31:54 +04:00
2 changed files with 60 additions and 16 deletions

View File

@@ -13,6 +13,7 @@ use chrono::TimeZone;
use deltachat_contact_tools::{ContactAddress, sanitize_bidi_characters, sanitize_single_line};
use humansize::{BINARY, format_size};
use mail_builder::mime::MimePart;
use rusqlite::OptionalExtension;
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
@@ -738,13 +739,21 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
/// Returns ID of the draft message, if there is one.
async fn get_draft_msg_id(self, context: &Context) -> Result<Option<MsgId>> {
let msg_id: Option<MsgId> = context
let query_only = true;
context
.sql
.query_get_value(
// `call` instead of `transaction_ex` because it's a single query.
.call(query_only, |conn| self.get_draft_msg_id_trans(conn))
.await
}
fn get_draft_msg_id_trans(self, conn: &rusqlite::Connection) -> Result<Option<MsgId>> {
let msg_id: Option<MsgId> = conn
.query_row(
"SELECT id FROM msgs WHERE chat_id=? AND state=?;",
(self, MessageState::OutDraft),
|row| row.get(0),
)
.await?;
.optional()?;
Ok(msg_id)
}
@@ -753,13 +762,19 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
if self.is_special() {
return Ok(None);
}
match self.get_draft_msg_id(context).await? {
Some(draft_msg_id) => {
let msg = Message::load_from_db(context, draft_msg_id).await?;
Ok(Some(msg))
}
None => Ok(None),
}
let query_only = true;
context
.sql
.transaction_ex(query_only, |transaction| {
match self.get_draft_msg_id_trans(transaction)? {
Some(draft_msg_id) => {
let msg = Message::load_from_db_trans(context, transaction, draft_msg_id)?;
Ok(Some(msg))
}
None => Ok(None),
}
})
.await
}
/// Deletes draft message, if there is one.

View File

@@ -10,6 +10,7 @@ use deltachat_derive::{FromSql, ToSql};
use humansize::BINARY;
use humansize::format_size;
use num_traits::FromPrimitive;
use rusqlite::OptionalExtension;
use serde::{Deserialize, Serialize};
use tokio::{fs, io};
@@ -493,8 +494,22 @@ impl Message {
///
/// Returns an error if the message does not exist.
pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
let message = Self::load_from_db_optional(context, id)
.await?
let query_only = true;
context
.sql
// `call` instead of `transaction_ex` because it's a single query.
.call(query_only, |conn| {
Self::load_from_db_trans(context, conn, id)
})
.await
}
/// See [`Self::load_from_db`].
pub(crate) fn load_from_db_trans(
context: &Context,
conn: &rusqlite::Connection,
id: MsgId,
) -> Result<Message> {
let message = Self::load_from_db_optional_trans(context, conn, id)?
.with_context(|| format!("Message {id} does not exist"))?;
Ok(message)
}
@@ -503,13 +518,27 @@ impl Message {
///
/// Returns `None` if the message does not exist.
pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
let query_only = true;
context
.sql
// `call` instead of `transaction_ex` because it's a single query.
.call(query_only, |conn| {
Self::load_from_db_optional_trans(context, conn, id)
})
.await
}
/// See [`Self::load_from_db_optional`].
pub(crate) fn load_from_db_optional_trans(
context: &Context,
conn: &rusqlite::Connection,
id: MsgId,
) -> Result<Option<Message>> {
ensure!(
!id.is_special(),
"Can not load special message ID {id} from DB"
);
let mut msg = context
.sql
.query_row_optional(
let mut msg = conn
.query_row(
"SELECT
m.id AS id,
rfc724_mid AS rfc724mid,
@@ -603,7 +632,7 @@ impl Message {
Ok(msg)
},
)
.await
.optional()
.with_context(|| format!("failed to load message {id} from the database"))?;
if let Some(msg) = &mut msg {