Compare commits

...

1 Commits

Author SHA1 Message Date
iequidoo
7de76e1708 feat: MsgId::get_html: Make only one db query
Merge `message::get_mime_headers()` into `MsgId::get_html()` for that, it's unlikely that it will be
used elsewhere.
2026-02-05 08:30:00 -03:00
3 changed files with 69 additions and 81 deletions

View File

@@ -9,7 +9,7 @@
use std::mem;
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, ensure};
use base64::Engine as _;
use mailparse::ParsedContentType;
use mime::Mime;
@@ -17,10 +17,12 @@ use mime::Mime;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::log::warn;
use crate::message::{self, Message, MsgId};
use crate::message::{Message, MsgId};
use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
use crate::param::{Param::SendHtml, Params};
use crate::plaintext::PlainText;
use crate::sql;
use crate::tools::{buf_compress, buf_decompress};
impl Message {
/// Check if the message can be retrieved as HTML.
@@ -258,28 +260,71 @@ impl MsgId {
/// NB: we do not save raw mime unconditionally in the database to save space.
/// The corresponding ffi-function is `dc_get_msg_html()`.
pub async fn get_html(self, context: &Context) -> Result<Option<String>> {
// If there are many concurrent db readers, going to the queue earlier makes sense.
let (param, rawmime) = tokio::join!(
self.get_param(context),
message::get_mime_headers(context, self)
);
if let Some(html) = param?.get(SendHtml) {
let (param, headers, compressed) = context
.sql
.query_row(
"SELECT param, mime_headers, mime_compressed FROM msgs WHERE id=?",
(self,),
|row| {
let param: String = row.get(0)?;
let param: Params = param.parse().unwrap_or_default();
let headers = sql::row_get_vec(row, 1)?;
let compressed: bool = row.get(2)?;
Ok((param, headers, compressed))
},
)
.await?;
if let Some(html) = param.get(SendHtml) {
return Ok(Some(html.to_string()));
}
let rawmime = rawmime?;
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, &rawmime).await {
Err(err) => {
warn!(context, "get_html: parser error: {:#}", err);
Ok(None)
let from_rawmime = |rawmime: Vec<u8>| async move {
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, &rawmime).await {
Err(err) => {
warn!(context, "get_html: parser error: {:#}", err);
Ok(None)
}
Ok((parser, _)) => Ok(Some(parser.html)),
}
Ok((parser, _)) => Ok(Some(parser.html)),
} else {
warn!(context, "get_html: no mime for {}", self);
Ok(None)
}
} else {
warn!(context, "get_html: no mime for {}", self);
Ok(None)
};
if compressed {
return from_rawmime(buf_decompress(&headers)?).await;
}
let headers2 = headers.clone();
let compressed = match tokio::task::block_in_place(move || buf_compress(&headers2)) {
Err(e) => {
warn!(context, "get_mime_headers: buf_compress() failed: {}", e);
return from_rawmime(headers).await;
}
Ok(o) => o,
};
let update = |conn: &mut rusqlite::Connection| {
match conn.execute(
"
UPDATE msgs SET mime_headers=?, mime_compressed=1
WHERE id=? AND mime_headers!='' AND mime_compressed=0",
(compressed, self),
) {
Ok(rows_updated) => ensure!(rows_updated <= 1),
Err(e) => {
warn!(context, "get_mime_headers: UPDATE failed: {}", e);
return Err(e.into());
}
}
Ok(())
};
if let Err(e) = context.sql.call_write(update).await {
warn!(
context,
"get_mime_headers: failed to update mime_headers: {}", e
);
}
return from_rawmime(headers).await;
}
}

View File

@@ -32,13 +32,12 @@ use crate::mimeparser::{SystemMessage, parse_message_id};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::reaction::get_msg_reactions;
use crate::sql;
use crate::summary::Summary;
use crate::sync::SyncData;
use crate::tools::create_outgoing_rfc724_mid;
use crate::tools::{
buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
sanitize_filename, time, timestamp_to_str,
get_filebytes, get_filemeta, gm2local_offset, read_file, sanitize_filename, time,
timestamp_to_str,
};
/// Message ID, including reserved IDs.
@@ -1617,62 +1616,6 @@ pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &
Some(info)
}
/// Get the raw mime-headers of the given message.
/// Raw headers are saved for large messages
/// that need a "Show full message..."
/// to see HTML part.
///
/// Returns an empty vector if there are no headers saved for the given message.
pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
let (headers, compressed) = context
.sql
.query_row(
"SELECT mime_headers, mime_compressed FROM msgs WHERE id=?",
(msg_id,),
|row| {
let headers = sql::row_get_vec(row, 0)?;
let compressed: bool = row.get(1)?;
Ok((headers, compressed))
},
)
.await?;
if compressed {
return buf_decompress(&headers);
}
let headers2 = headers.clone();
let compressed = match tokio::task::block_in_place(move || buf_compress(&headers2)) {
Err(e) => {
warn!(context, "get_mime_headers: buf_compress() failed: {}", e);
return Ok(headers);
}
Ok(o) => o,
};
let update = |conn: &mut rusqlite::Connection| {
match conn.execute(
"\
UPDATE msgs SET mime_headers=?, mime_compressed=1 \
WHERE id=? AND mime_headers!='' AND mime_compressed=0",
(compressed, msg_id),
) {
Ok(rows_updated) => ensure!(rows_updated <= 1),
Err(e) => {
warn!(context, "get_mime_headers: UPDATE failed: {}", e);
return Err(e.into());
}
}
Ok(())
};
if let Err(e) = context.sql.call_write(update).await {
warn!(
context,
"get_mime_headers: failed to update mime_headers: {}", e
);
}
Ok(headers)
}
/// Delete a single message from the database, including references in other tables.
/// This may be called in batches; the final events are emitted in delete_msgs_locally_done() then.
pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {

View File

@@ -1680,8 +1680,8 @@ async fn test_save_mime_headers_off() -> anyhow::Result<()> {
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.get_text(), "hi!");
let mime = message::get_mime_headers(&bob, msg.id).await?;
assert!(mime.is_empty());
let html = msg.id.get_html(&bob).await?;
assert!(html.is_none());
Ok(())
}