The same bug #215 fixed in the calendar import, in the three places contacts write more than one card at once. Promised on #173; nobody has hit it yet.
ContactCard/set is refused whole over maxObjectsInSet — requestTooLarge, nothing created — so:
a large enough vCard import filed nothing,
a large enough LDIF import filed nothing (#174's format),
and select-all → delete over a large address book deleted nothing, reporting it in JMAP's words.
What changed
Both imports now go through one createCards, which splits by the ceiling the session advertises and falls back to 500. That is what the LDIF import's own comment — "from ContactCard/set down they are the same" — was already claiming, and is now true of.
destroyCards splits the same way, and takes off the list the ids the server said it destroyed rather than everything that was asked for. It removed all of them before, which was harmless while there was one call and wrong the moment a later batch can fail: deleted contacts must not stay on screen, and live ones must not disappear from it.
One behaviour change beyond the batching: a vCard import the server accepted no card of returned 0, and the view reported importing no contacts — which reads as an empty file rather than as a refusal. It now says why, which is what the LDIF import has always done. A genuinely empty file still says so, earlier and separately.
Tests
Eleven new ones in contact-set-batching.test.ts, against a server that refuses an oversized call the way Stalwart does rather than quietly taking the first 500. Ten of the eleven fail on main (the eleventh is a regression guard on partial-success counting, which already worked).
Covers all three call sites: the 500/500/200 split for each, splitting by the advertised value rather than a constant (40 → 40/40/20), entries staying distinct across the split, the partial-failure message, and — for delete — that a third-batch failure leaves exactly the 200 undeleted cards on the list.
npm run typecheck, npm test (889 web + 122 server), npm run build pass.
Not in this PR
Duplicate checking on re-import, which is #174's first item and still open on #173 for calendars. Worth one decision for both rather than two.
The same bug #215 fixed in the calendar import, in the three places contacts write more than one card at once. Promised on #173; nobody has hit it yet.
`ContactCard/set` is refused whole over `maxObjectsInSet` — `requestTooLarge`, nothing created — so:
- a large enough **vCard** import filed nothing,
- a large enough **LDIF** import filed nothing (#174's format),
- and **select-all → delete** over a large address book deleted nothing, reporting it in JMAP's words.
## What changed
Both imports now go through one `createCards`, which splits by the ceiling the session advertises and falls back to 500. That is what the LDIF import's own comment — *"from `ContactCard/set` down they are the same"* — was already claiming, and is now true of.
`destroyCards` splits the same way, and takes off the list **the ids the server said it destroyed** rather than everything that was asked for. It removed all of them before, which was harmless while there was one call and wrong the moment a later batch can fail: deleted contacts must not stay on screen, and live ones must not disappear from it.
One behaviour change beyond the batching: a vCard import the server accepted no card of returned `0`, and the view reported importing no contacts — which reads as an empty file rather than as a refusal. It now says why, which is what the LDIF import has always done. A genuinely empty file still says so, earlier and separately.
## Tests
Eleven new ones in `contact-set-batching.test.ts`, against a server that refuses an oversized call the way Stalwart does rather than quietly taking the first 500. Ten of the eleven fail on `main` (the eleventh is a regression guard on partial-success counting, which already worked).
Covers all three call sites: the 500/500/200 split for each, splitting by the advertised value rather than a constant (40 → 40/40/20), entries staying distinct across the split, the partial-failure message, and — for delete — that a third-batch failure leaves exactly the 200 undeleted cards on the list.
`npm run typecheck`, `npm test` (889 web + 122 server), `npm run build` pass.
## Not in this PR
Duplicate checking on re-import, which is #174's first item and still open on #173 for calendars. Worth one decision for both rather than two.
**Merged** 2026-09-02 as coffey-labs/ihasmail@6e1ac42d5a49
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The same bug #215 fixed in the calendar import, in the three places contacts write more than one card at once. Promised on #173; nobody has hit it yet.
ContactCard/setis refused whole overmaxObjectsInSet—requestTooLarge, nothing created — so:What changed
Both imports now go through one
createCards, which splits by the ceiling the session advertises and falls back to 500. That is what the LDIF import's own comment — "fromContactCard/setdown they are the same" — was already claiming, and is now true of.destroyCardssplits the same way, and takes off the list the ids the server said it destroyed rather than everything that was asked for. It removed all of them before, which was harmless while there was one call and wrong the moment a later batch can fail: deleted contacts must not stay on screen, and live ones must not disappear from it.One behaviour change beyond the batching: a vCard import the server accepted no card of returned
0, and the view reported importing no contacts — which reads as an empty file rather than as a refusal. It now says why, which is what the LDIF import has always done. A genuinely empty file still says so, earlier and separately.Tests
Eleven new ones in
contact-set-batching.test.ts, against a server that refuses an oversized call the way Stalwart does rather than quietly taking the first 500. Ten of the eleven fail onmain(the eleventh is a regression guard on partial-success counting, which already worked).Covers all three call sites: the 500/500/200 split for each, splitting by the advertised value rather than a constant (40 → 40/40/20), entries staying distinct across the split, the partial-failure message, and — for delete — that a third-batch failure leaves exactly the 200 undeleted cards on the list.
npm run typecheck,npm test(889 web + 122 server),npm run buildpass.Not in this PR
Duplicate checking on re-import, which is #174's first item and still open on #173 for calendars. Worth one decision for both rather than two.
Merged 2026-09-02 as coffey-labs/ihasmail@6e1ac42d5a
Rebuilt from: git history, session transcript.