mirror of
https://github.com/chatmail/core.git
synced 2026-10-04 12:10:25 +03:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ebd82efa | ||
|
|
903a3a9822 | ||
|
|
1bbcacb89c | ||
|
|
40796156d0 | ||
|
|
ff9623bc19 | ||
|
|
0806cac0cb | ||
|
|
56e66047bf | ||
|
|
ea01dd2a98 | ||
|
|
a85ce53680 | ||
|
|
c1a8fc54c3 | ||
|
|
6bd6fa9b5f |
@@ -2457,14 +2457,20 @@ impl CommandApi {
|
||||
|
||||
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let reuse_existing_draft = data.reuse_existing_draft;
|
||||
let mut message = data
|
||||
.create_message(&ctx)
|
||||
.await
|
||||
.context("Failed to create message")?;
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||
.await
|
||||
.context("Failed to send created message")?
|
||||
.to_u32();
|
||||
let msg_id = chat::send_msg_ex(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
&mut message,
|
||||
reuse_existing_draft.into(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to send created message")?
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
|
||||
@@ -616,6 +616,27 @@ pub struct MessageData {
|
||||
/// Quoted message id. Takes preference over `quoted_text` (see below).
|
||||
pub quoted_message_id: Option<u32>,
|
||||
pub quoted_text: Option<String>,
|
||||
/// Useful for WebXDC app attachments, which can also be opened
|
||||
/// for draft messages.
|
||||
/// Setting this to `true` will ensure that the WebXDC status updates
|
||||
/// of the current draft are preserved when sending the message
|
||||
/// or updating the draft.
|
||||
///
|
||||
/// `false` by default, for backwards compatibility.
|
||||
/// However, you probably want to set it to `true`
|
||||
/// when sending or updating the draft
|
||||
/// from the main message composer section,
|
||||
/// and to `false` when sending a message from secondary places,
|
||||
/// such as a notification "Reply" input.
|
||||
///
|
||||
/// Reusing the draft will also automatically remove the draft
|
||||
/// when it's sent.
|
||||
///
|
||||
/// Note that sometimes the draft cannot be reused,
|
||||
/// for example when the WebXDC attachment [`Self::file`] changes.
|
||||
/// If the draft cannot be reused, it will not get auto-removed.
|
||||
#[serde(default)]
|
||||
pub reuse_existing_draft: bool,
|
||||
}
|
||||
|
||||
impl MessageData {
|
||||
|
||||
+320
-130
@@ -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,24 +739,82 @@ 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)
|
||||
}
|
||||
|
||||
fn has_draft_with_id(&self, conn: &rusqlite::Connection, draft_id: &MsgId) -> Result<bool> {
|
||||
let count: u32 = conn.query_row(
|
||||
"SELECT
|
||||
COUNT(*)
|
||||
FROM msgs
|
||||
WHERE id=? AND chat_id=? AND state=?",
|
||||
(draft_id, self, MessageState::OutDraft),
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
fn can_reuse_draft(context: &Context, old: &Message, new: &Message) -> Result<bool> {
|
||||
ensure!(
|
||||
old.chat_id.is_unset() || new.chat_id.is_unset() || new.chat_id == old.chat_id,
|
||||
"messages belong to different chats"
|
||||
);
|
||||
ensure!(
|
||||
old.get_state() == MessageState::Undefined || old.get_state() == MessageState::OutDraft,
|
||||
"old message is a real message, not a draft"
|
||||
);
|
||||
|
||||
// Reusing the draft is only useful for WebXDC messages,
|
||||
// but for consistency let's reuse it whenever possible.
|
||||
|
||||
if old.get_viewtype() == Viewtype::Webxdc
|
||||
&& (new.get_viewtype() != Viewtype::Webxdc
|
||||
|| old.get_file(context) != new.get_file(context))
|
||||
{
|
||||
// Old draft's WebXDC attachment got removed or replaced.
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Returns draft message, if there is one.
|
||||
pub async fn get_draft(self, context: &Context) -> Result<Option<Message>> {
|
||||
let query_only = true;
|
||||
context
|
||||
.sql
|
||||
.transaction_ex(query_only, |transaction| {
|
||||
self.get_draft_trans(context, transaction)
|
||||
})
|
||||
.await
|
||||
}
|
||||
/// See [`Self::get_draft`].
|
||||
pub fn get_draft_trans(
|
||||
self,
|
||||
context: &Context,
|
||||
conn: &rusqlite::Connection,
|
||||
) -> Result<Option<Message>> {
|
||||
if self.is_special() {
|
||||
return Ok(None);
|
||||
}
|
||||
match self.get_draft_msg_id(context).await? {
|
||||
match self.get_draft_msg_id_trans(conn)? {
|
||||
Some(draft_msg_id) => {
|
||||
let msg = Message::load_from_db(context, draft_msg_id).await?;
|
||||
let msg = Message::load_from_db_trans(context, conn, draft_msg_id)?;
|
||||
Ok(Some(msg))
|
||||
}
|
||||
None => Ok(None),
|
||||
@@ -783,6 +842,9 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
/// thus preserving the ID and possible WebXDC status updates
|
||||
/// associated with the draft message.
|
||||
///
|
||||
/// If `msg.id` is specified, the [`Self::can_reuse_draft`] check
|
||||
/// will be skipped.
|
||||
///
|
||||
/// Returns `false` if the existing draft is already at the state
|
||||
/// that the caller tried to set it to, so it was unchanged.
|
||||
async fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<bool> {
|
||||
@@ -819,48 +881,54 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
msg.state = MessageState::OutDraft;
|
||||
msg.chat_id = self;
|
||||
|
||||
// if possible, replace existing draft and keep id
|
||||
if !msg.id.is_special()
|
||||
&& let Some(old_draft) = self.get_draft(context).await?
|
||||
&& old_draft.id == msg.id
|
||||
&& old_draft.chat_id == self
|
||||
&& old_draft.state == MessageState::OutDraft
|
||||
{
|
||||
let affected_rows = context
|
||||
.sql.execute(
|
||||
"UPDATE msgs
|
||||
SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
|
||||
WHERE id=?7
|
||||
AND (type <> ?2
|
||||
OR txt <> ?3
|
||||
OR txt_normalized <> ?4
|
||||
OR param <> ?5
|
||||
OR mime_in_reply_to <> ?6);",
|
||||
(
|
||||
time(),
|
||||
msg.viewtype,
|
||||
&msg.text,
|
||||
normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
msg.id,
|
||||
),
|
||||
).await?;
|
||||
return Ok(affected_rows > 0);
|
||||
}
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
// Delete existing draft if it exists.
|
||||
transaction.execute(
|
||||
"DELETE FROM msgs WHERE chat_id=? AND state=?",
|
||||
(self, MessageState::OutDraft),
|
||||
let trans_fn = |transaction: &mut rusqlite::Transaction| {
|
||||
// if possible, replace existing draft and keep id
|
||||
let reuse_existing_id: Option<MsgId> = if !msg.id.is_special() {
|
||||
if self.has_draft_with_id(transaction, &msg.id)? {
|
||||
Some(msg.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if let Some(existing) = self.get_draft_trans(context, transaction)?
|
||||
&& Self::can_reuse_draft(context, &existing, msg)?
|
||||
{
|
||||
Some(existing.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(reuse_existing_id) = reuse_existing_id {
|
||||
let affected_rows = transaction.execute(
|
||||
"UPDATE msgs
|
||||
SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
|
||||
WHERE id=?7
|
||||
AND (type <> ?2
|
||||
OR txt <> ?3
|
||||
OR txt_normalized <> ?4
|
||||
OR param <> ?5
|
||||
OR mime_in_reply_to <> ?6);",
|
||||
(
|
||||
time(),
|
||||
msg.viewtype,
|
||||
&msg.text,
|
||||
normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
reuse_existing_id,
|
||||
),
|
||||
)?;
|
||||
let changed = affected_rows > 0;
|
||||
return Ok((reuse_existing_id, changed));
|
||||
}
|
||||
|
||||
// Insert new draft.
|
||||
transaction.execute(
|
||||
"INSERT INTO msgs (
|
||||
// Delete existing draft if it exists.
|
||||
transaction.execute(
|
||||
"DELETE FROM msgs WHERE chat_id=? AND state=?",
|
||||
(self, MessageState::OutDraft),
|
||||
)?;
|
||||
|
||||
// Insert new draft.
|
||||
transaction.execute(
|
||||
"INSERT INTO msgs (
|
||||
chat_id,
|
||||
rfc724_mid,
|
||||
from_id,
|
||||
@@ -873,26 +941,28 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
hidden,
|
||||
mime_in_reply_to)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?);",
|
||||
(
|
||||
self,
|
||||
&msg.rfc724_mid,
|
||||
ContactId::SELF,
|
||||
time(),
|
||||
msg.viewtype,
|
||||
MessageState::OutDraft,
|
||||
&msg.text,
|
||||
normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
),
|
||||
)?;
|
||||
(
|
||||
self,
|
||||
&msg.rfc724_mid,
|
||||
ContactId::SELF,
|
||||
time(),
|
||||
msg.viewtype,
|
||||
MessageState::OutDraft,
|
||||
&msg.text,
|
||||
normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
),
|
||||
)?;
|
||||
|
||||
Ok(transaction.last_insert_rowid())
|
||||
})
|
||||
.await?;
|
||||
msg.id = MsgId::new(row_id.try_into()?);
|
||||
Ok(true)
|
||||
let msg_id = MsgId::new(transaction.last_insert_rowid().try_into()?);
|
||||
let changed = true;
|
||||
Ok((msg_id, changed))
|
||||
};
|
||||
let (msg_id, changed) = context.sql.transaction(trans_fn).await?;
|
||||
msg.id = msg_id;
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
/// Returns number of messages in a chat.
|
||||
@@ -1758,21 +1828,24 @@ impl Chat {
|
||||
/// Adds missing values to the msg object,
|
||||
/// writes the record to the database.
|
||||
///
|
||||
/// If `update_msg_id` is set, that record is reused;
|
||||
/// if `update_msg_id` is None, a new record is created.
|
||||
/// If `update_existing_draft == `[`UseExistingDraftPolicy::Reuse`],
|
||||
/// we will reuse the draft by the specified `msg.id`,
|
||||
/// or if it can be reused according to [`ChatId::can_reuse_draft`].
|
||||
/// If `msg.id` is specified, the [`ChatId::can_reuse_draft`] check
|
||||
/// will be skipped.
|
||||
/// If ID was specified but no such draft exists, an error is returned.
|
||||
///
|
||||
/// If `update_existing_draft == `[`UseExistingDraftPolicy::DontReuse`],
|
||||
/// a new record is created.
|
||||
async fn prepare_msg_raw(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
update_msg_id: Option<MsgId>,
|
||||
update_existing_draft: UseExistingDraftPolicy,
|
||||
) -> Result<()> {
|
||||
let mut to_id = 0;
|
||||
let mut location_id = 0;
|
||||
|
||||
if msg.rfc724_mid.is_empty() {
|
||||
msg.rfc724_mid = create_outgoing_rfc724_mid();
|
||||
}
|
||||
|
||||
if self.typ == Chattype::Single {
|
||||
if let Some(id) = context
|
||||
.sql
|
||||
@@ -1806,11 +1879,13 @@ impl Chat {
|
||||
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
|
||||
// Set "References:" to identify the "thread" of the conversation.
|
||||
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
|
||||
let new_references;
|
||||
//
|
||||
// When `None`, we'll use the current message's `rfc724_mid` as the reference.
|
||||
let new_references_opt: Option<String>;
|
||||
if self.is_self_talk() {
|
||||
// As self-talks are mainly used to transfer data between devices,
|
||||
// we do not set In-Reply-To/References in this case.
|
||||
new_references = String::new();
|
||||
new_references_opt = Some(String::new());
|
||||
} else if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
||||
// We don't filter `OutPending` and `OutFailed` messages because the new message for
|
||||
// which `parent_query()` is done may assume that it will be received in a context
|
||||
@@ -1858,9 +1933,9 @@ impl Chat {
|
||||
if references_vec.is_empty() {
|
||||
// As a fallback, use our Message-ID,
|
||||
// same as in the case of top-level message.
|
||||
new_references = msg.rfc724_mid.clone();
|
||||
new_references_opt = None;
|
||||
} else {
|
||||
new_references = references_vec.join(" ");
|
||||
new_references_opt = Some(references_vec.join(" "));
|
||||
}
|
||||
} else {
|
||||
// This is a top-level message.
|
||||
@@ -1868,7 +1943,13 @@ impl Chat {
|
||||
// This allows us to identify replies to our message even if
|
||||
// email server such as Outlook changes `Message-ID:` header.
|
||||
// MUAs usually keep the first Message-ID in `References:` header unchanged.
|
||||
new_references = msg.rfc724_mid.clone();
|
||||
new_references_opt = None;
|
||||
}
|
||||
fn get_new_references<'a>(
|
||||
new_references_opt: &'a Option<String>,
|
||||
rfc724_mid: &'a str,
|
||||
) -> &'a str {
|
||||
new_references_opt.as_deref().unwrap_or(rfc724_mid)
|
||||
}
|
||||
|
||||
// add independent location to database
|
||||
@@ -1933,10 +2014,54 @@ impl Chat {
|
||||
msg.from_id = ContactId::SELF;
|
||||
|
||||
// add message to the database
|
||||
if let Some(update_msg_id) = update_msg_id {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
let trans_fn = |transaction: &mut rusqlite::Transaction| {
|
||||
let try_reuse: bool = update_existing_draft == UseExistingDraftPolicy::Reuse;
|
||||
let reuse_existing: Option<(MsgId, String)> = if !try_reuse {
|
||||
None
|
||||
} else if !msg.id.is_special() {
|
||||
ensure!(
|
||||
!msg.rfc724_mid.is_empty(),
|
||||
concat!(
|
||||
"cannot reuse existing draft: ",
|
||||
"when `message.id` is set, `message.rfc724_mid` must also be set ",
|
||||
"(as well as all other necessary properties); "
|
||||
)
|
||||
);
|
||||
|
||||
Some((msg.id, msg.rfc724_mid.to_owned()))
|
||||
} else if let Some(existing) = self.id.get_draft_trans(context, transaction)?
|
||||
&& ChatId::can_reuse_draft(context, &existing, msg)?
|
||||
{
|
||||
ensure!(
|
||||
!existing.rfc724_mid.is_empty(),
|
||||
concat!(
|
||||
"cannot reuse existing draft: ",
|
||||
"expected its `rfc724_mid` to be already set in the DB, but it's empty"
|
||||
)
|
||||
);
|
||||
|
||||
Some((existing.id, existing.rfc724_mid.to_owned()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((reuse_existing_id, rfc724_mid)) = reuse_existing {
|
||||
ensure!(!rfc724_mid.is_empty());
|
||||
|
||||
// Maybe we could try to somehow gracefully recover from this,
|
||||
// but better safe than sorry.
|
||||
if !self.id.has_draft_with_id(transaction, &reuse_existing_id)? {
|
||||
bail!(
|
||||
concat!(
|
||||
"wanted to prepare existing draft for sending in chat {0}, ",
|
||||
"but no draft with ID {1} is present ",
|
||||
"(it might have been sent or deleted)"
|
||||
),
|
||||
self.id,
|
||||
reuse_existing_id
|
||||
);
|
||||
}
|
||||
|
||||
transaction.execute(
|
||||
"UPDATE msgs
|
||||
SET rfc724_mid=?, chat_id=?, from_id=?, to_id=?, timestamp=?, type=?,
|
||||
state=?, txt=?, txt_normalized=?, subject=?, param=?,
|
||||
@@ -1945,7 +2070,7 @@ impl Chat {
|
||||
ephemeral_timestamp=?
|
||||
WHERE id=?;",
|
||||
params_slice![
|
||||
msg.rfc724_mid,
|
||||
rfc724_mid,
|
||||
msg.chat_id,
|
||||
msg.from_id,
|
||||
to_id,
|
||||
@@ -1958,45 +2083,49 @@ impl Chat {
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
new_references,
|
||||
get_new_references(&new_references_opt, &rfc724_mid),
|
||||
new_mime_headers.is_some(),
|
||||
new_mime_headers.unwrap_or_default(),
|
||||
location_id as i32,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp,
|
||||
update_msg_id
|
||||
reuse_existing_id
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
msg.id = update_msg_id;
|
||||
} else {
|
||||
let raw_id = context
|
||||
.sql
|
||||
.insert(
|
||||
)?;
|
||||
let inserted = false;
|
||||
Ok((reuse_existing_id, rfc724_mid, inserted))
|
||||
} else {
|
||||
let rfc724_mid = if !msg.rfc724_mid.is_empty() {
|
||||
&msg.rfc724_mid
|
||||
} else {
|
||||
&create_outgoing_rfc724_mid()
|
||||
};
|
||||
|
||||
transaction.execute(
|
||||
"INSERT INTO msgs (
|
||||
rfc724_mid,
|
||||
chat_id,
|
||||
from_id,
|
||||
to_id,
|
||||
timestamp,
|
||||
type,
|
||||
state,
|
||||
txt,
|
||||
txt_normalized,
|
||||
subject,
|
||||
param,
|
||||
hidden,
|
||||
mime_in_reply_to,
|
||||
mime_references,
|
||||
mime_modified,
|
||||
mime_headers,
|
||||
mime_compressed,
|
||||
location_id,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
|
||||
rfc724_mid,
|
||||
chat_id,
|
||||
from_id,
|
||||
to_id,
|
||||
timestamp,
|
||||
type,
|
||||
state,
|
||||
txt,
|
||||
txt_normalized,
|
||||
subject,
|
||||
param,
|
||||
hidden,
|
||||
mime_in_reply_to,
|
||||
mime_references,
|
||||
mime_modified,
|
||||
mime_headers,
|
||||
mime_compressed,
|
||||
location_id,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
|
||||
params_slice![
|
||||
msg.rfc724_mid,
|
||||
rfc724_mid,
|
||||
msg.chat_id,
|
||||
msg.from_id,
|
||||
to_id,
|
||||
@@ -2009,17 +2138,25 @@ impl Chat {
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
new_references,
|
||||
get_new_references(&new_references_opt, rfc724_mid),
|
||||
new_mime_headers.is_some(),
|
||||
new_mime_headers.unwrap_or_default(),
|
||||
location_id as i32,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
let msg_id = MsgId::new(transaction.last_insert_rowid().try_into()?);
|
||||
let inserted = true;
|
||||
Ok((msg_id, rfc724_mid.to_string(), inserted))
|
||||
}
|
||||
};
|
||||
let (msg_id, rfc724_mid, inserted) = context.sql.transaction(trans_fn).await?;
|
||||
|
||||
msg.id = msg_id;
|
||||
msg.rfc724_mid = rfc724_mid;
|
||||
if inserted {
|
||||
context.new_msgs_notify.notify_one();
|
||||
msg.id = MsgId::new(u32::try_from(raw_id)?);
|
||||
|
||||
maybe_set_logging_xdc(context, msg, self.id).await?;
|
||||
context
|
||||
@@ -2607,6 +2744,14 @@ pub async fn is_contact_in_chat(
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
fn is_initialized_draft_msg_of_chat(msg: &Message, chat_id: ChatId) -> bool {
|
||||
if msg.state == MessageState::OutDraft {
|
||||
!msg.id.is_special() && msg.chat_id == chat_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a message object to a chat.
|
||||
///
|
||||
/// Sends the event #DC_EVENT_MSGS_CHANGED on success.
|
||||
@@ -2614,6 +2759,21 @@ pub async fn is_contact_in_chat(
|
||||
/// sending may be delayed eg. due to network problems. However, from your
|
||||
/// view, you're done with the message. Sooner or later it will find its way.
|
||||
pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
let update_existing_draft = is_initialized_draft_msg_of_chat(msg, chat_id);
|
||||
|
||||
send_msg_ex(context, chat_id, msg, update_existing_draft.into()).await
|
||||
}
|
||||
/// Unlike [`send_msg`] (and [`send_msg_sync`]),
|
||||
/// this function allows for reusing the draft even if `msg`
|
||||
/// is not a fully initialized [`Message`],
|
||||
/// i.e. if [`Message::get_id`], [`Message::get_viewtype`],
|
||||
/// [`Message::rfc724_mid`] etc. are unset.
|
||||
pub async fn send_msg_ex(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
msg: &mut Message,
|
||||
update_existing_draft: UseExistingDraftPolicy,
|
||||
) -> Result<MsgId> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
"chat_id cannot be a special chat: {chat_id}"
|
||||
@@ -2630,7 +2790,10 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
|
||||
msg.text = sanitize_bidi_characters(&msg.text);
|
||||
}
|
||||
|
||||
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
|
||||
if !prepare_send_msg(context, chat_id, msg, update_existing_draft)
|
||||
.await?
|
||||
.is_empty()
|
||||
{
|
||||
if !msg.hidden {
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
}
|
||||
@@ -2650,7 +2813,9 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
|
||||
/// Creates jobs in the `smtp` table, then drectly opens an SMTP connection and sends the
|
||||
/// message. If this fails, the jobs remain in the database for later sending.
|
||||
pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
let rowids = prepare_send_msg(context, chat_id, msg).await?;
|
||||
let update_existing_draft = is_initialized_draft_msg_of_chat(msg, chat_id);
|
||||
|
||||
let rowids = prepare_send_msg(context, chat_id, msg, update_existing_draft.into()).await?;
|
||||
if rowids.is_empty() {
|
||||
return Ok(msg.id);
|
||||
}
|
||||
@@ -2671,6 +2836,7 @@ async fn prepare_send_msg(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
msg: &mut Message,
|
||||
update_existing_draft: UseExistingDraftPolicy,
|
||||
) -> Result<Vec<i64>> {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
@@ -2715,17 +2881,9 @@ async fn prepare_send_msg(
|
||||
);
|
||||
}
|
||||
|
||||
// check current MessageState for drafts (to keep msg_id) ...
|
||||
let update_msg_id = if msg.state == MessageState::OutDraft {
|
||||
if msg.state == MessageState::OutDraft {
|
||||
msg.hidden = false;
|
||||
if !msg.id.is_special() && msg.chat_id == chat_id {
|
||||
Some(msg.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
if msg.state == MessageState::Undefined
|
||||
// Legacy SecureJoin "v*-request" messages are unencrypted.
|
||||
@@ -2744,7 +2902,8 @@ async fn prepare_send_msg(
|
||||
if !msg.hidden {
|
||||
chat_id.unarchive_if_not_muted(context, msg.state).await?;
|
||||
}
|
||||
chat.prepare_msg_raw(context, msg, update_msg_id).await?;
|
||||
chat.prepare_msg_raw(context, msg, update_existing_draft)
|
||||
.await?;
|
||||
|
||||
let row_ids = create_send_msg_jobs(context, msg)
|
||||
.await
|
||||
@@ -2755,6 +2914,36 @@ async fn prepare_send_msg(
|
||||
Ok(row_ids)
|
||||
}
|
||||
|
||||
/// When sending a message or setting the draft,
|
||||
/// what to do about the potentially existing draft.
|
||||
///
|
||||
/// This is basically a more explicit bool.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum UseExistingDraftPolicy {
|
||||
/// Create a brand new draft or message, don't reuse the existing one's ID.
|
||||
DontReuse,
|
||||
/// Resuse the existing draft, so that the new draft or message
|
||||
/// keeps its original ID.
|
||||
/// Useful for WebXDC attachments.
|
||||
Reuse,
|
||||
}
|
||||
impl From<UseExistingDraftPolicy> for bool {
|
||||
fn from(reuse: UseExistingDraftPolicy) -> bool {
|
||||
match reuse {
|
||||
UseExistingDraftPolicy::DontReuse => false,
|
||||
UseExistingDraftPolicy::Reuse => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<bool> for UseExistingDraftPolicy {
|
||||
fn from(reuse: bool) -> UseExistingDraftPolicy {
|
||||
match reuse {
|
||||
false => UseExistingDraftPolicy::DontReuse,
|
||||
true => UseExistingDraftPolicy::Reuse,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the Message or splits it into Pre- and Post-Message.
|
||||
///
|
||||
/// Pre-Message is a small message with metadata which announces a larger Post-Message.
|
||||
@@ -4576,7 +4765,8 @@ pub async fn forward_msgs_2ctx(
|
||||
msg.rfc724_mid = create_outgoing_rfc724_mid();
|
||||
msg.pre_rfc724_mid.clear();
|
||||
msg.timestamp_sort = now;
|
||||
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
|
||||
chat.prepare_msg_raw(ctx_dst, &mut msg, UseExistingDraftPolicy::DontReuse)
|
||||
.await?;
|
||||
|
||||
if !create_send_msg_jobs(ctx_dst, &mut msg).await?.is_empty() {
|
||||
ctx_dst.scheduler.interrupt_smtp().await;
|
||||
|
||||
@@ -175,6 +175,41 @@ async fn test_draft_stable_ids() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_send_sent_draft() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id = t.create_chat(bob).await.id;
|
||||
|
||||
let mut msg = Message::new_text("original".to_string());
|
||||
|
||||
chat_id.set_draft(t, Some(&mut msg)).await?;
|
||||
assert_eq!(msg.state, MessageState::OutDraft);
|
||||
|
||||
let mut msg_clone = msg.clone();
|
||||
|
||||
send_msg(t, chat_id, &mut msg).await?;
|
||||
assert_eq!(msg.state, MessageState::OutPending);
|
||||
|
||||
msg_clone.set_text("modified".to_string());
|
||||
// Try to send the stale draft Message object with the same ID again.
|
||||
assert_eq!(msg_clone.id, msg.id);
|
||||
assert_eq!(msg_clone.state, MessageState::OutDraft);
|
||||
let msg_from_db_before_send = Message::load_from_db(t, msg.id).await?;
|
||||
let send_res = send_msg(t, chat_id, &mut msg_clone).await;
|
||||
assert!(send_res.is_err());
|
||||
|
||||
let msg_from_db_after_send = Message::load_from_db(t, msg.id).await?;
|
||||
assert_eq!(msg_from_db_after_send.text, "original");
|
||||
assert_eq!(
|
||||
serde_json::to_string_pretty(&msg_from_db_before_send).unwrap(),
|
||||
serde_json::to_string_pretty(&msg_from_db_after_send).unwrap()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_only_one_draft_per_chat() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
+35
-6
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user