Match an LDIF re-import on the entry's dn

Reported again by the submitter's colleague at LINET after #223 was
closed: duplicate checking was implemented for vCard and never for LDIF,
so re-importing an address book still leaves a second copy of everything.
That was deliberate at the time -- the matching key was an open question
I did not want to answer alone -- but the answer had already been given
on #174 and I closed the issue without acting on it.

The answer, in the submitter's words: an attribute that *can* change is
fine, because it will not have changed between two imports minutes apart.
An import is not a sync. That makes the `dn` usable -- it is the only
identity the file carries, and Mozilla's schema defines no UID -- and it
needs no guessing at all, unlike the name-plus-email fallback I had been
weighing.

So `uidFromDn` derives a namespaced, stable uid from the distinguished
name, normalised for the case and spacing two exports of one directory
differ in. A card the book already holds under that uid is updated rather
than duplicated, merged the way the vCard import merges: what the file
carries wins, what it does not mention is left alone. Reported as created
and updated, which is the pair that was asked for.

Three things worth knowing:

Matching is per address book, so two customer directories that each hold
a `cn=John Smith` stay two people as long as they are filed separately.
Imported into one book they would merge, which is the one way this can be
wrong and the reason the escape hatch is worth naming.

The look-alike count stays, and now means something narrower: entries
that `dn` matching could not catch -- one whose `dn` moved between
exports, and anything imported before there was a `dn` to match on. Those
are still only counted, never merged.

A file holding two entries under one `dn` is malformed, since a directory
cannot, and now becomes one card instead of two sharing an identity.

FEATURES gains the re-import behaviour for both formats; it documented
neither.
This commit is contained in:
2026-09-04 07:50:49 -07:00
parent 1f8c12e29e
commit b4248a6661
9 changed files with 330 additions and 53 deletions
+15 -3
View File
@@ -128,11 +128,23 @@ describe("telling somebody what an LDIF re-import duplicated", () => {
});
it("does not count the file against itself", async () => {
// Two of the same person in one file are two new cards, not a duplicate of
// Two people in one file are two new cards, neither a duplicate of
// something that was already here. The scan is read before anything lands.
server([]);
const twice = entry("Jane Doe", "[email protected]") + "\n" + entry("Jane Doe", "jane@example.com");
const r = await useContacts.getState().importLdif(twice, "book1");
const two = entry("Jane Doe", "[email protected]") + "\n" + entry("Alan Turing", "alan@example.org");
const r = await useContacts.getState().importLdif(two, "book1");
expect(r).toEqual({ created: 2, updated: 0, alike: 0 });
});
it("makes one card of two entries in a file that share a dn", async () => {
// A directory cannot hold two entries under one name, so a file that does
// is malformed -- and must not produce two cards sharing an identity,
// which is the duplication this all exists to prevent. The later wins.
const sets = server([]);
const twice = entry("Jane Doe", "[email protected]") + "\n" + entry("Jane Doe", "[email protected]");
const r = await useContacts.getState().importLdif(twice, "book1");
expect(r).toEqual({ created: 1, updated: 0, alike: 0 });
const only = Object.values(sets[0]!.create!)[0]!;
expect(Object.values(only.emails as Record<string, { address: string }>)[0]!.address).toBe("[email protected]");
});
});