Mailbox roles must be unique per archive (fixes #8)

This commit is contained in:
Maurus Decimus
2026-06-15 21:28:27 +02:00
parent 48e38f19f3
commit c1f3f742fe
11 changed files with 336 additions and 7 deletions
+1
View File
@@ -14,6 +14,7 @@ pub mod imap_state;
pub mod init;
pub mod maildir_ids;
pub mod managesieve_ids;
pub mod roles;
pub mod sources;
pub mod sync_state_jmap;
pub mod takeout_ids;
+90
View File
@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
use rusqlite::{Connection, params};
pub fn unique_role(
conn: &Connection,
role: Option<&str>,
exclude_id: Option<i64>,
) -> rusqlite::Result<Option<String>> {
let Some(r) = role else {
return Ok(None);
};
let taken: bool = conn.query_row(
"SELECT EXISTS(SELECT 1 FROM mailboxes WHERE role = ?1 AND (?2 IS NULL OR id != ?2))",
params![r, exclude_id],
|row| row.get(0),
)?;
Ok((!taken).then(|| r.to_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::init;
fn mem() -> Connection {
let conn = Connection::open_in_memory().unwrap();
init::apply_schema(&conn).unwrap();
conn
}
fn insert(conn: &Connection, name: &str, role: Option<&str>) -> i64 {
conn.execute(
"INSERT INTO mailboxes (name, parent_id, role, sort_order, is_subscribed)
VALUES (?1, NULL, ?2, 0, 1)",
params![name, role],
)
.unwrap();
conn.last_insert_rowid()
}
#[test]
fn first_claimant_keeps_role_second_is_dropped() {
let conn = mem();
let first = unique_role(&conn, Some("sent"), None).unwrap();
assert_eq!(first.as_deref(), Some("sent"));
insert(&conn, "Sent", first.as_deref());
let second = unique_role(&conn, Some("sent"), None).unwrap();
assert_eq!(second, None);
}
#[test]
fn null_role_stays_null() {
let conn = mem();
assert_eq!(unique_role(&conn, None, None).unwrap(), None);
}
#[test]
fn distinct_roles_are_independent() {
let conn = mem();
insert(&conn, "Sent", Some("sent"));
assert_eq!(
unique_role(&conn, Some("trash"), None).unwrap().as_deref(),
Some("trash")
);
}
#[test]
fn update_excludes_own_row() {
let conn = mem();
let id = insert(&conn, "Sent", Some("sent"));
assert_eq!(
unique_role(&conn, Some("sent"), Some(id)).unwrap().as_deref(),
Some("sent")
);
}
#[test]
fn update_detects_other_holder() {
let conn = mem();
insert(&conn, "Sent", Some("sent"));
let other = insert(&conn, "Envoyés", None);
assert_eq!(unique_role(&conn, Some("sent"), Some(other)).unwrap(), None);
}
}
+18 -1
View File
@@ -160,6 +160,8 @@ pub fn reconcile(
.unwrap_or(0);
let mut uploader = Uploader::new(net, &ctx.conn);
let mut taken_roles: HashSet<String> =
targets.iter().filter_map(|t| t.role.clone()).collect();
let interleave = ty == ObjectType::FileNode;
for d in 0..=max_depth {
let level: Vec<&LocalNode> = to_create
@@ -225,7 +227,22 @@ pub fn reconcile(
continue;
}
match build_create(ctx, ty, n.local, maps, &mut uploader) {
Ok(obj) => {
Ok(mut obj) => {
if ty == ObjectType::Mailbox
&& let Some(r) = n.role.as_deref()
{
if taken_roles.contains(r) {
if let Value::Object(m) = &mut obj {
m.remove("role");
}
logger.warn(&format!(
"Mailbox local {}: role '{r}' already present on target, creating as a plain folder",
n.local
));
} else {
taken_roles.insert(r.to_owned());
}
}
batch.push((format!("c{}", n.local), obj));
}
Err(e) => {
+4
View File
@@ -380,6 +380,8 @@ fn upsert_mailbox(
.map_err(|e| Error::Partial(e.to_string()))?;
let existing = ctx.local.get(&folder.folder.folder_id.id);
let local_id = if let Some(row) = existing {
let role = crate::db::roles::unique_role(&tx, role, Some(row.local_id))
.map_err(|e| Error::Partial(e.to_string()))?;
tx.execute(
"UPDATE mailboxes SET name = ?1, parent_id = ?2, role = ?3 WHERE id = ?4",
params![folder.folder.display_name, parent_local, role, row.local_id],
@@ -396,6 +398,8 @@ fn upsert_mailbox(
ctx.counts.fetched += 1;
row.local_id
} else {
let role = crate::db::roles::unique_role(&tx, role, None)
.map_err(|e| Error::Partial(e.to_string()))?;
tx.execute(
"INSERT INTO mailboxes (name, parent_id, role, sort_order, is_subscribed) \
VALUES (?1, ?2, ?3, 0, 1)",
@@ -91,6 +91,7 @@ pub fn reconcile_mail(
server_ids.push(graph_id.to_owned());
let local_id = if let Some(id) = existing {
let role = crate::db::roles::unique_role(&tx, role, Some(id))?;
tx.execute(
"UPDATE mailboxes SET name = ?1, parent_id = ?2, role = ?3, is_subscribed = ?4
WHERE id = ?5",
@@ -99,6 +100,7 @@ pub fn reconcile_mail(
counts.fetched += 1;
id
} else {
let role = crate::db::roles::unique_role(&tx, role, None)?;
tx.execute(
"INSERT INTO mailboxes (name, parent_id, role, sort_order, is_subscribed)
VALUES (?1, ?2, ?3, 0, ?4)",
+4 -2
View File
@@ -564,26 +564,28 @@ fn upsert_mailboxes(
};
let existing = db::imap_ids::local_for_mailbox(&tx, source_id, &folder.name)?;
let id = if let Some(id) = existing {
let role = db::roles::unique_role(&tx, folder.role, Some(id))?;
tx.execute(
"UPDATE mailboxes SET name = ?1, parent_id = ?2, role = ?3,
is_subscribed = ?4 WHERE id = ?5",
params![
folder.leaf,
parent_local,
folder.role,
role,
folder.subscribed as i64,
id
],
)?;
id
} else {
let role = db::roles::unique_role(&tx, folder.role, None)?;
tx.execute(
"INSERT INTO mailboxes (name, parent_id, role, sort_order, is_subscribed)
VALUES (?1, ?2, ?3, 0, ?4)",
params![
folder.leaf,
parent_local,
folder.role,
role,
folder.subscribed as i64
],
)?;
+4 -2
View File
@@ -113,13 +113,14 @@ pub fn insert_mailbox(
resolver: &impl LocalResolver,
) -> Result<i64, JmapError> {
let parent = opt_parent(resolver, ObjectType::Mailbox, &wire.parent_id);
let role = crate::db::roles::unique_role(conn, wire.role.as_deref(), None)?;
conn.execute(
"INSERT INTO mailboxes (name, parent_id, role, sort_order, is_subscribed)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![
wire.name,
parent,
wire.role,
role,
wire.sort_order,
wire.is_subscribed as i64
],
@@ -401,6 +402,7 @@ pub fn update_mailbox(
resolver: &impl LocalResolver,
) -> Result<bool, JmapError> {
let parent = opt_parent(resolver, ObjectType::Mailbox, &wire.parent_id);
let role = crate::db::roles::unique_role(conn, wire.role.as_deref(), Some(local_id))?;
let n = conn.execute(
"UPDATE mailboxes SET name = ?1, parent_id = ?2, role = ?3, sort_order = ?4,
is_subscribed = ?5
@@ -409,7 +411,7 @@ pub fn update_mailbox(
params![
wire.name,
parent,
wire.role,
role,
wire.sort_order,
wire.is_subscribed as i64,
local_id