| 419 |
419 |
|
|
| 420 |
420 |
|
let mut safe_uids: Vec<u32> = Vec::new();
|
| 421 |
421 |
|
let mut skipped_large = 0usize;
|
|
422 |
+ |
// Highest UID we may advance the watermark past without fetching a body:
|
|
423 |
+ |
// oversized messages are permanently over the size cap, so re-fetching
|
|
424 |
+ |
// them would skip them identically. Parseable messages advance the
|
|
425 |
+ |
// watermark only once saved (below), so a transient parse failure is
|
|
426 |
+ |
// retried next sync instead of being silently dropped forever.
|
|
427 |
+ |
let mut max_skipped_uid: Option<u32> = None;
|
| 422 |
428 |
|
while let Some(result) = size_stream.next().await {
|
| 423 |
429 |
|
if let Ok(msg) = result
|
| 424 |
430 |
|
&& let Some(uid) = msg.uid {
|
| 425 |
431 |
|
if let Some(size) = msg.size
|
| 426 |
432 |
|
&& size > MAX_EMAIL_SIZE {
|
| 427 |
433 |
|
skipped_large += 1;
|
|
434 |
+ |
max_skipped_uid = Some(max_skipped_uid.map_or(uid, |m: u32| m.max(uid)));
|
| 428 |
435 |
|
tracing::warn!(uid, size, folder = %folder, "Skipping oversized email ({} bytes)", size);
|
| 429 |
436 |
|
continue;
|
| 430 |
437 |
|
}
|
| 452 |
459 |
|
let mut msg_count = 0;
|
| 453 |
460 |
|
let mut body_count = 0;
|
| 454 |
461 |
|
let mut parse_errors = 0;
|
| 455 |
|
- |
// Seed max_uid from all UIDs (including skipped large ones) so they aren't re-fetched
|
| 456 |
|
- |
let mut max_uid: Option<u32> = uids.iter().copied().max();
|
|
462 |
+ |
// Seed from the oversized (permanently-skipped) UIDs only. Parseable
|
|
463 |
+ |
// messages advance this as they are saved, so a message that fails to
|
|
464 |
+ |
// parse does not silently fold into the persisted watermark.
|
|
465 |
+ |
let mut max_uid: Option<u32> = max_skipped_uid;
|
| 457 |
466 |
|
|
| 458 |
467 |
|
// Fetch full bodies in bounded UID batches so neither the IMAP command
|
| 459 |
468 |
|
// nor the in-flight fetch pipeline grows with the size of the mailbox.
|
| 481 |
490 |
|
}
|
| 482 |
491 |
|
};
|
| 483 |
492 |
|
|
| 484 |
|
- |
max_uid = Some(max_uid.map_or(uid, |m: u32| m.max(uid)));
|
| 485 |
|
- |
|
| 486 |
493 |
|
let is_read = message.flags().any(|f| matches!(f, async_imap::types::Flag::Seen));
|
| 487 |
494 |
|
|
| 488 |
495 |
|
if let Some(body) = message.body() {
|
| 490 |
497 |
|
if !mime_nesting_within_limit(body) {
|
| 491 |
498 |
|
tracing::warn!(uid, folder = %folder_name, "skipping email with excessive MIME nesting");
|
| 492 |
499 |
|
parse_errors += 1;
|
|
500 |
+ |
// Deterministic skip (same message always trips the guard):
|
|
501 |
+ |
// safe to advance past so it isn't re-fetched every sync.
|
|
502 |
+ |
max_uid = Some(max_uid.map_or(uid, |m: u32| m.max(uid)));
|
| 493 |
503 |
|
continue;
|
| 494 |
504 |
|
}
|
| 495 |
505 |
|
match mailparse::parse_mail(body) {
|
| 496 |
|
- |
Ok(parsed) => emails.push(Self::build_parsed_email(&parsed, uid, &folder_name, is_read)),
|
|
506 |
+ |
Ok(parsed) => {
|
|
507 |
+ |
emails.push(Self::build_parsed_email(&parsed, uid, &folder_name, is_read));
|
|
508 |
+ |
// Advance only once the message is saved, so a parse
|
|
509 |
+ |
// failure below is retried rather than silently lost.
|
|
510 |
+ |
max_uid = Some(max_uid.map_or(uid, |m: u32| m.max(uid)));
|
|
511 |
+ |
}
|
| 497 |
512 |
|
Err(e) => {
|
| 498 |
513 |
|
tracing::debug!(uid, folder = %folder_name, error = %e, "Failed to parse email");
|
| 499 |
514 |
|
parse_errors += 1;
|