Fix: Strict RFC822.SIZE == BODY[] length check discards good mail

This commit is contained in:
Maurus Decimus
2026-06-27 12:00:12 +02:00
parent 3e9d4593f7
commit 6ca561fff5
9 changed files with 31 additions and 20 deletions
+9
View File
@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
## [1.0.5] - 2026-06-27
### Added
### Changed
### Fixed
- Strict `RFC822.SIZE` == `BODY[]` length check discards good mail.
## [1.0.4] - 2026-06-21 ## [1.0.4] - 2026-06-21
### Added ### Added
Generated
+1 -1
View File
@@ -2892,7 +2892,7 @@ dependencies = [
[[package]] [[package]]
name = "vandelay" name = "vandelay"
version = "1.0.4" version = "1.0.5"
dependencies = [ dependencies = [
"base64", "base64",
"blake3", "blake3",
+1 -1
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "vandelay" name = "vandelay"
description = "JMAP account migration utility" description = "JMAP account migration utility"
version = "1.0.4" version = "1.0.5"
authors = ["Stalwart Labs LLC <[email protected]>"] authors = ["Stalwart Labs LLC <[email protected]>"]
license = "Apache-2.0 OR MIT" license = "Apache-2.0 OR MIT"
repository = "https://github.com/stalwartlabs/vandelay" repository = "https://github.com/stalwartlabs/vandelay"
+1 -1
View File
@@ -222,7 +222,7 @@ fn try_via_principal(
return Ok(None); return Ok(None);
} }
Err(JmapError::RetriesExhausted(_)) | Err(JmapError::Malformed(_)) => { Err(JmapError::RetriesExhausted(_)) | Err(JmapError::Malformed(_)) => {
return Ok(None) return Ok(None);
} }
Err(e) => return Err(DiscoveryError::Transport(e)), Err(e) => return Err(DiscoveryError::Transport(e)),
}; };
+3 -1
View File
@@ -75,7 +75,9 @@ mod tests {
let conn = mem(); let conn = mem();
let id = insert(&conn, "Sent", Some("sent")); let id = insert(&conn, "Sent", Some("sent"));
assert_eq!( assert_eq!(
unique_role(&conn, Some("sent"), Some(id)).unwrap().as_deref(), unique_role(&conn, Some("sent"), Some(id))
.unwrap()
.as_deref(),
Some("sent") Some("sent")
); );
} }
+1 -2
View File
@@ -160,8 +160,7 @@ pub fn reconcile(
.unwrap_or(0); .unwrap_or(0);
let mut uploader = Uploader::new(net, &ctx.conn); let mut uploader = Uploader::new(net, &ctx.conn);
let mut taken_roles: HashSet<String> = let mut taken_roles: HashSet<String> = targets.iter().filter_map(|t| t.role.clone()).collect();
targets.iter().filter_map(|t| t.role.clone()).collect();
let interleave = ty == ObjectType::FileNode; let interleave = ty == ObjectType::FileNode;
for d in 0..=max_depth { for d in 0..=max_depth {
let level: Vec<&LocalNode> = to_create let level: Vec<&LocalNode> = to_create
+2 -9
View File
@@ -582,12 +582,7 @@ fn upsert_mailboxes(
tx.execute( tx.execute(
"INSERT INTO mailboxes (name, parent_id, role, sort_order, is_subscribed) "INSERT INTO mailboxes (name, parent_id, role, sort_order, is_subscribed)
VALUES (?1, ?2, ?3, 0, ?4)", VALUES (?1, ?2, ?3, 0, ?4)",
params![ params![folder.leaf, parent_local, role, folder.subscribed as i64],
folder.leaf,
parent_local,
role,
folder.subscribed as i64
],
)?; )?;
let new_id = tx.last_insert_rowid(); let new_id = tx.last_insert_rowid();
db::imap_ids::insert_mailbox(&tx, source_id, &folder.name, new_id)?; db::imap_ids::insert_mailbox(&tx, source_id, &folder.name, new_id)?;
@@ -927,12 +922,10 @@ fn insert_single_message(
logger, logger,
LEVEL_DEFAULT, LEVEL_DEFAULT,
&format!( &format!(
"folder {folder:?} uid {uid}: BODY[] {} bytes vs RFC822.SIZE {declared_size}, skipping", "folder {folder:?} uid {uid}: BODY[] {} bytes vs RFC822.SIZE {declared_size}, importing the fetched literal",
body.len() body.len()
), ),
); );
counts.skipped += 1;
return Ok(());
} }
let translation = translate_flags(&attrs.flags, include_deleted); let translation = translate_flags(&attrs.flags, include_deleted);
if translation.has_deleted_flag && !include_deleted { if translation.has_deleted_flag && !include_deleted {
+5 -3
View File
@@ -707,7 +707,7 @@ fn coordinator_falls_back_to_uid_fetch_when_search_all_bad() {
} }
#[test] #[test]
fn coordinator_skips_message_on_size_mismatch() { fn coordinator_imports_message_despite_size_mismatch() {
let control = control_script_one_folder(100, 2, &[1]); let control = control_script_one_folder(100, 2, &[1]);
let worker: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> { let worker: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> {
auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?; auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?;
@@ -734,8 +734,10 @@ fn coordinator_skips_message_on_size_mismatch() {
.iter() .iter()
.find(|(k, _)| *k == "email") .find(|(k, _)| *k == "email")
.unwrap(); .unwrap();
assert_eq!(email.1.skipped, 1); assert_eq!(email.1.skipped, 0);
assert_eq!(email.1.created, 0); assert_eq!(email.1.created, 1);
let dbc = Connection::open(&archive).unwrap();
assert_eq!(count(&dbc, "emails"), 1);
} }
fn single_inbox_scripts(uidvalidity: u32, uidnext: u32, body: &'static [u8]) -> Vec<Script> { fn single_inbox_scripts(uidvalidity: u32, uidnext: u32, body: &'static [u8]) -> Vec<Script> {
+8 -2
View File
@@ -863,7 +863,10 @@ fn import_missing_email_blob_is_skipped_and_counted_once() {
.find(|(t, _)| *t == "Email") .find(|(t, _)| *t == "Email")
.map(|(_, c)| c.clone()) .map(|(_, c)| c.clone())
.expect("email counts"); .expect("email counts");
assert_eq!(email.fetched, 1, "only the email with a present blob imports"); assert_eq!(
email.fetched, 1,
"only the email with a present blob imports"
);
assert_eq!( assert_eq!(
email.failed, 1, email.failed, 1,
"a missing blob counts the email failed exactly once, not twice" "a missing blob counts the email failed exactly once, not twice"
@@ -2571,7 +2574,10 @@ fn export_duplicate_role_mailbox_created_as_plain_folder_keeping_subtree() {
.find(|(t, _)| *t == "Mailbox") .find(|(t, _)| *t == "Mailbox")
.map(|(_, c)| c.clone()) .map(|(_, c)| c.clone())
.expect("mailbox counts"); .expect("mailbox counts");
assert_eq!(counts.skipped, 2, "Inbox and Sent match existing role mailboxes"); assert_eq!(
counts.skipped, 2,
"Inbox and Sent match existing role mailboxes"
);
assert_eq!( assert_eq!(
counts.created, 2, counts.created, 2,
"duplicate-role folder and its child are both created" "duplicate-role folder and its child are both created"