Skip to content

Commit f15fb01

Browse files
committed
feat: Don't ignore receive_imf_inner() errors, try adding partially downloaded message if so (#7196)
Ignoring `receive_imf_inner()` errors, i.e. silently skipping messages on failures, leads to bugs never fixed. As for temporary I/O errors, ignoring them leads to lost messages, in this case it's better to bubble up the error and get the IMAP loop stuck. However if there's some logic error, it's better to show it to the user so that it's more likely reported, and continue receiving messages. To distinguish these cases, on error, try adding the message as partially downloaded with the error set to `msgs.error`, this way the user also can retry downloading the message to finally see it if the problem is fixed.
1 parent 90b0ca7 commit f15fb01

File tree

4 files changed

+43
-38
lines changed

4 files changed

+43
-38
lines changed

src/download.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,20 @@ impl MimeMessage {
238238
/// the mime-structure itself is not available.
239239
///
240240
/// The placeholder part currently contains a text with size and availability of the message;
241+
/// `error` is set as the part error;
241242
/// in the future, we may do more advanced things as previews here.
242243
pub(crate) async fn create_stub_from_partial_download(
243244
&mut self,
244245
context: &Context,
245246
org_bytes: u32,
247+
error: Option<String>,
246248
) -> Result<()> {
249+
let prefix = match error {
250+
None => "",
251+
Some(_) => "[❗] ",
252+
};
247253
let mut text = format!(
248-
"[{}]",
254+
"{prefix}[{}]",
249255
stock_str::partial_download_msg_body(context, org_bytes).await
250256
);
251257
if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
@@ -256,12 +262,12 @@ impl MimeMessage {
256262
.await;
257263
text += format!(" [{until}]").as_str();
258264
};
259-
260265
info!(context, "Partial download: {}", text);
261266

262267
self.parts.push(Part {
263268
typ: Viewtype::Text,
264269
msg: text,
270+
error,
265271
..Default::default()
266272
});
267273

src/imap.rs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,28 +1467,39 @@ impl Session {
14671467
context,
14681468
"Passing message UID {} to receive_imf().", request_uid
14691469
);
1470-
match receive_imf_inner(
1470+
let res = receive_imf_inner(
14711471
context,
14721472
folder,
14731473
uidvalidity,
14741474
request_uid,
14751475
rfc724_mid,
14761476
body,
14771477
is_seen,
1478-
partial,
1478+
partial.map(|msg_size| (msg_size, None)),
14791479
)
1480-
.await
1481-
{
1482-
Ok(received_msg) => {
1483-
received_msgs_channel
1484-
.send((request_uid, received_msg))
1485-
.await?;
1486-
}
1487-
Err(err) => {
1488-
warn!(context, "receive_imf error: {:#}.", err);
1489-
received_msgs_channel.send((request_uid, None)).await?;
1480+
.await;
1481+
let received_msg = if let Err(err) = res {
1482+
warn!(context, "receive_imf error: {:#}.", err);
1483+
if partial.is_some() {
1484+
return Err(err);
14901485
}
1486+
receive_imf_inner(
1487+
context,
1488+
folder,
1489+
uidvalidity,
1490+
request_uid,
1491+
rfc724_mid,
1492+
body,
1493+
is_seen,
1494+
Some((body.len().try_into()?, Some(format!("{err:#}")))),
1495+
)
1496+
.await?
1497+
} else {
1498+
res?
14911499
};
1500+
received_msgs_channel
1501+
.send((request_uid, received_msg))
1502+
.await?;
14921503
}
14931504

14941505
// If we don't process the whole response, IMAP client is left in a broken state where

src/mimeparser.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,12 @@ const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
240240
impl MimeMessage {
241241
/// Parse a mime message.
242242
///
243-
/// If `partial` is set, it contains the full message size in bytes
244-
/// and `body` contains the header only.
243+
/// If `partial` is set, it contains the full message size in bytes and an optional error text
244+
/// for the partially downloaded message, and `body` contains the HEADER only.
245245
pub(crate) async fn from_bytes(
246246
context: &Context,
247247
body: &[u8],
248-
partial: Option<u32>,
248+
partial: Option<(u32, Option<String>)>,
249249
) -> Result<Self> {
250250
let mail = mailparse::parse_mail(body)?;
251251

@@ -612,9 +612,9 @@ impl MimeMessage {
612612
};
613613

614614
match partial {
615-
Some(org_bytes) => {
615+
Some((org_bytes, err)) => {
616616
parser
617-
.create_stub_from_partial_download(context, org_bytes)
617+
.create_stub_from_partial_download(context, org_bytes, err)
618618
.await?;
619619
}
620620
None => match mail {

src/receive_imf.rs

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ pub(crate) async fn receive_imf_from_inbox(
196196
rfc724_mid,
197197
imf_raw,
198198
seen,
199-
is_partial_download,
199+
is_partial_download.map(|msg_size| (msg_size, None)),
200200
)
201201
.await
202202
}
@@ -494,9 +494,8 @@ async fn get_to_and_past_contact_ids(
494494
/// If the message is so wrong that we didn't even create a database entry,
495495
/// returns `Ok(None)`.
496496
///
497-
/// If `is_partial_download` is set, it contains the full message size in bytes.
498-
/// Do not confuse that with `replace_msg_id` that will be set when the full message is loaded
499-
/// later.
497+
/// If `partial` is set, it contains the full message size in bytes and an optional error text for
498+
/// the partially downloaded message.
500499
#[expect(clippy::too_many_arguments)]
501500
pub(crate) async fn receive_imf_inner(
502501
context: &Context,
@@ -506,7 +505,7 @@ pub(crate) async fn receive_imf_inner(
506505
rfc724_mid: &str,
507506
imf_raw: &[u8],
508507
seen: bool,
509-
is_partial_download: Option<u32>,
508+
partial: Option<(u32, Option<String>)>,
510509
) -> Result<Option<ReceivedMsg>> {
511510
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
512511
info!(
@@ -516,8 +515,8 @@ pub(crate) async fn receive_imf_inner(
516515
);
517516
}
518517

519-
let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw, is_partial_download).await
520-
{
518+
let is_partial_download = partial.as_ref().map(|(msg_size, _err)| *msg_size);
519+
let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw, partial).await {
521520
Err(err) => {
522521
warn!(context, "receive_imf: can't parse MIME: {err:#}.");
523522
if rfc724_mid.starts_with(GENERATED_PREFIX) {
@@ -551,22 +550,11 @@ pub(crate) async fn receive_imf_inner(
551550
// make sure, this check is done eg. before securejoin-processing.
552551
let (replace_msg_id, replace_chat_id);
553552
if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
554-
if is_partial_download.is_some() {
555-
// Should never happen, see imap::prefetch_should_download(), but still.
556-
info!(
557-
context,
558-
"Got a partial download and message is already in DB."
559-
);
560-
return Ok(None);
561-
}
562553
let msg = Message::load_from_db(context, old_msg_id).await?;
563554
replace_msg_id = Some(old_msg_id);
564555
replace_chat_id = if msg.download_state() != DownloadState::Done {
565556
// the message was partially downloaded before and is fully downloaded now.
566-
info!(
567-
context,
568-
"Message already partly in DB, replacing by full message."
569-
);
557+
info!(context, "Message already partly in DB, replacing.");
570558
Some(msg.chat_id)
571559
} else {
572560
None

0 commit comments

Comments
 (0)