feature request: iCAL file import into calendars #173

Closed
opened 2026-09-01 15:38:06 +00:00 by unknown · 7 comments
unknown commented 2026-09-01 15:38:06 +00:00

The original description was not recovered.

Referenced by commits

Rebuilt from: notification email, session transcript. Opening date is approximate.

*The original description was not recovered.* **Referenced by commits** - coffey-labs/ihasmail@a1fe4fea1ad5 2026-09-02 Skip vCards on re-import that the address book already has - coffey-labs/ihasmail@1a6158aa7051 2026-09-02 Export a calendar as an iCAL file - coffey-labs/ihasmail@74f6d1d0aa19 2026-09-02 Skip events on re-import that the calendar already has - coffey-labs/ihasmail@4121f9263b7e 2026-09-02 Set contact cards in batches the server will take - coffey-labs/ihasmail@17bd54852491 2026-09-02 Import an iCal file in batches the server will take - coffey-labs/ihasmail@6431ec87f543 2026-09-01 Import an iCal file into a calendar <sub>Rebuilt from: notification email, session transcript. Opening date is approximate.</sub>
Owner

Done in #178, and live now on ihasmail.com as 2026.9.1+pr178.

It's where you asked for it: the calendar's own menu (right-click the calendar, or the dots button), between Edit and Share…, and the file goes into that calendar. No global button next to + Event — you were right that this isn't frequent enough to earn one. The item is greyed out on a calendar you only have read access to.

The parsing is the server's, through the same CalendarEvent/parse that emailed invitations already go through, so it's Stalwart's iCalendar reader rather than something hand-rolled in the browser. A file of any number of events works; they all go out in one request.

One deliberate choice worth stating: nothing is mailed to anyone named in the file. Importing is filing something you already have, and sending scheduling messages to its participants would be a surprise to them — which matters for exactly your case, where the invitation came in through a ticketing system rather than to you.

Note that importing the same file twice will create second copies rather than matching them up by UID. That seemed like the right default for an import, but if re-importing an updated export is something you'd do, say so and I'll look at matching on UID.

Done in #178, and live now on ihasmail.com as 2026.9.1+pr178. It's where you asked for it: the calendar's own menu (right-click the calendar, or the dots button), between *Edit* and *Share…*, and the file goes into that calendar. No global button next to *+ Event* — you were right that this isn't frequent enough to earn one. The item is greyed out on a calendar you only have read access to. The parsing is the server's, through the same `CalendarEvent/parse` that emailed invitations already go through, so it's Stalwart's iCalendar reader rather than something hand-rolled in the browser. A file of any number of events works; they all go out in one request. One deliberate choice worth stating: nothing is mailed to anyone named in the file. Importing is filing something you already have, and sending scheduling messages to its participants would be a surprise to them — which matters for exactly your case, where the invitation came in through a ticketing system rather than to you. Note that importing the same file twice will create second copies rather than matching them up by UID. That seemed like the right default for an import, but if re-importing an updated export is something you'd do, say so and I'll look at matching on UID.

Once more, thanks a bunch!

One deliberate choice worth stating: nothing is mailed to anyone named in the file. Importing is filing something you already have, and sending scheduling messages to its participants would be a surprise to them

100% agree

Note that importing the same file twice will create second copies rather than matching them up by UID

My gut reaction was "I'd prefer skipping via UID if one is present" (from my experience not all iCAL files/entries come with them… shame on the programs that create such entries). But let me think about it some more before making an argument about it.

Once more, thanks a bunch! > One deliberate choice worth stating: nothing is mailed to anyone named in the file. Importing is filing something you already have, and sending scheduling messages to its participants would be a surprise to them 100% agree > Note that importing the same file twice will create second copies rather than matching them up by UID My gut reaction was "I'd prefer skipping via UID if one is present" (from my experience not all iCAL files/entries come with them… shame on the programs that create such entries). But let me think about it some more before making an argument about it.

My colleague tried to import an 800 KB iCAL file. This failed with the following error returned from Stalwart's JMAP endpoint:

{"methodResponses":[["error",{"type":"requestTooLarge","description":"The number of ids requested by the client exceeds the maximum number the server is willing to process in a single method call."},"c12_0"]],"sessionState":"e3d7f837"}

My colleague investigated further. Stalwart has server-side settings that determine the maximum number of entries to handle per request. She noted that there seems to be no way to query what those limits currently are.

Therefore she suggests batching writes by the default maximum number, which is 500 objects (JMAP setting "Max Objects - Set").

I concur; expecting users to be able to correctly split iCAL files after an arbitrary number of entries is not the way forward.

My colleague tried to import an 800 KB iCAL file. This failed with the following error returned from Stalwart's JMAP endpoint: ```json {"methodResponses":[["error",{"type":"requestTooLarge","description":"The number of ids requested by the client exceeds the maximum number the server is willing to process in a single method call."},"c12_0"]],"sessionState":"e3d7f837"} ``` My colleague investigated further. Stalwart has server-side settings that determine the maximum number of entries to handle per request. She noted that there seems to be no way to query what those limits currently are. Therefore she suggests batching writes by the default maximum number, which is 500 objects (JMAP setting "Max Objects - Set"). I concur; expecting users to be able to correctly split iCAL files after an arbitrary number of entries is not the way forward.
Owner

Fixed in #215, live now on ihasmail.com as 2026.9.2+pr215. Please have your colleague try the 800 KB file again.

Your colleague is right about the cause and right about the number, and half-right about the limits being unqueryable — which is the part worth spelling out, because it's the part I got wrong rather than her.

maxObjectsInSet is advertised, in the session object's urn:ietf:params:jmap:core capability, and ihasmail has read it since long before the import existed: it's how deleting a large folder and flagging a large selection have always been split. The import simply didn't use it. So this was one call site that didn't do what the rest of the client already did, not a missing capability — which is the more embarrassing version, but the cheaper one to fix.

So: the events now go out maxObjectsInSet at a time, splitting by whatever your server advertises rather than by a constant, and falling back to 500 where a server doesn't say. If you've tuned "Max Objects - Set" up or down, the import follows it.

Still batches rather than one call per event, which is worth stating since one-at-a-time would have been the simpler fix. Creating an event invalidates the calendar's cache, and invalidating re-fetches every range currently on screen — so a year of events imported singly would re-read the calendar a few hundred times. It's now one re-read, after the last batch.

One thing you'll only see if it goes wrong: if a later batch fails after earlier ones are already filed, the error says how far it got — "1000 of 1200 events were imported before this happened" — and re-reads the calendar so those events are visible. Reporting only that the import failed would send someone hunting for events that are already in front of them.

A caveat I'd rather state than have you find: the tests run against a mock that refuses an oversized call the same way your Stalwart did, and I haven't put a genuine 800 KB export through a live server. The mock enforces the ceiling on both /get and /set, so the batching itself is exercised, but your colleague's actual file is the real test.

Two things still open, and one I owe you:

  • UID matching on re-import. You said you'd think about it, so I've left it alone. Worth noting it's come up again on #174 for contacts, where duplicate contacts on re-import is the first item — so it's probably one decision for both rather than two. If you land on "skip when a UID is present, import when it isn't", say so and I'll do them together.
  • The address book import has this same bug. ContactCard/set sends everything in one call in three places — the vCard import, the LDIF import, and bulk delete. A large enough address book will fail exactly the way the calendar did. Nobody's hit it yet; I'd rather fix it before someone does.

Thanks to your colleague for the JSON. Pasting the actual error response is what made this a ten-minute diagnosis instead of a guess.

Fixed in #215, live now on ihasmail.com as `2026.9.2+pr215`. Please have your colleague try the 800 KB file again. Your colleague is right about the cause and right about the number, and half-right about the limits being unqueryable — which is the part worth spelling out, because it's the part I got wrong rather than her. `maxObjectsInSet` **is** advertised, in the session object's `urn:ietf:params:jmap:core` capability, and ihasmail has read it since long before the import existed: it's how deleting a large folder and flagging a large selection have always been split. The import simply didn't use it. So this was one call site that didn't do what the rest of the client already did, not a missing capability — which is the more embarrassing version, but the cheaper one to fix. So: the events now go out `maxObjectsInSet` at a time, splitting by whatever your server advertises rather than by a constant, and falling back to 500 where a server doesn't say. If you've tuned "Max Objects - Set" up or down, the import follows it. Still batches rather than one call per event, which is worth stating since one-at-a-time would have been the simpler fix. Creating an event invalidates the calendar's cache, and invalidating re-fetches every range currently on screen — so a year of events imported singly would re-read the calendar a few hundred times. It's now one re-read, after the last batch. One thing you'll only see if it goes wrong: if a later batch fails after earlier ones are already filed, the error says how far it got — *"1000 of 1200 events were imported before this happened"* — and re-reads the calendar so those events are visible. Reporting only that the import failed would send someone hunting for events that are already in front of them. A caveat I'd rather state than have you find: the tests run against a mock that refuses an oversized call the same way your Stalwart did, and I haven't put a genuine 800 KB export through a live server. The mock enforces the ceiling on both `/get` and `/set`, so the batching itself is exercised, but your colleague's actual file is the real test. Two things still open, and one I owe you: - **UID matching on re-import.** You said you'd think about it, so I've left it alone. Worth noting it's come up again on #174 for contacts, where duplicate contacts on re-import is the first item — so it's probably one decision for both rather than two. If you land on "skip when a UID is present, import when it isn't", say so and I'll do them together. - **The address book import has this same bug.** `ContactCard/set` sends everything in one call in three places — the vCard import, the LDIF import, and bulk delete. A large enough address book will fail exactly the way the calendar did. Nobody's hit it yet; I'd rather fix it before someone does. Thanks to your colleague for the JSON. Pasting the actual error response is what made this a ten-minute diagnosis instead of a guess.

Thanks!

maxObjectsInSet is advertised, in the session object's urn:ietf:params:jmap:core capability

That's good to know, thanks.

UID matching on re-import. You said you'd think about it, so I've left it alone.

Reasonable 😁 My colleague stumbled across the duplication during her tests, too, and was confused. Both of us would prefer duplicate checks on UIDs if UID present in event.

Thanks! > `maxObjectsInSet` **is** advertised, in the session object's `urn:ietf:params:jmap:core` capability That's good to know, thanks. > UID matching on re-import. You said you'd think about it, so I've left it alone. Reasonable 😁 My colleague stumbled across the duplication during her tests, too, and was confused. Both of us would prefer duplicate checks on UIDs if UID present in event.
Owner

Filed as #222 so the UID decision doesn't get lost in this thread — skip on a UID the calendar already holds, import what arrives without one, exactly as you and your colleague asked for. The contacts half is #223, kept separate because LDIF has no UID to match on and that question is still open.

Filed as #222 so the UID decision doesn't get lost in this thread — skip on a UID the calendar already holds, import what arrives without one, exactly as you and your colleague asked for. The contacts half is #223, kept separate because LDIF has no UID to match on and that question is still open.
Owner

Done, in #225 — live now as 2026.9.2+pr225. Exactly the rule you and your colleague asked for: an event whose UID the calendar already holds is skipped, and one that arrives without a UID is imported as before, since there is nothing to match it on and a softer match would guess in both directions.

One thing you get for free from how UIDs work, which I want to state because it could otherwise look like the check failing: matching is per calendar. A UID is what makes an event the same event across calendars, so importing the same file into a second calendar still imports it. Only a re-import into the same calendar skips.

The counts are now reported separately — "Imported 12 events · 200 were already here" — because "Imported 12 events" over a 212-event file reads as a failure, and re-importing an unchanged file would otherwise say it imported nothing at all rather than telling you everything was already there.

Worth mentioning what I did not do. A matching UID is skipped, not updated. Re-importing an updated export is a plausible reason to do this at all, and if that is what your colleague actually wants — the file being the newer copy, and the calendar taking the change — that is a different behaviour and a bigger one. Say so and I will look at it; I did not want to decide it on your behalf when you had asked for skip.

Your colleague's find, again. Please pass it on.

Done, in #225 — live now as `2026.9.2+pr225`. Exactly the rule you and your colleague asked for: an event whose UID the calendar already holds is skipped, and one that arrives without a UID is imported as before, since there is nothing to match it on and a softer match would guess in both directions. One thing you get for free from how UIDs work, which I want to state because it could otherwise look like the check failing: matching is **per calendar**. A UID is what makes an event the same event *across* calendars, so importing the same file into a second calendar still imports it. Only a re-import into the same calendar skips. The counts are now reported separately — "Imported 12 events · 200 were already here" — because "Imported 12 events" over a 212-event file reads as a failure, and re-importing an unchanged file would otherwise say it imported nothing at all rather than telling you everything was already there. Worth mentioning what I did *not* do. A matching UID is skipped, not updated. Re-importing an updated export is a plausible reason to do this at all, and if that is what your colleague actually wants — the file being the newer copy, and the calendar taking the change — that is a different behaviour and a bigger one. Say so and I will look at it; I did not want to decide it on your behalf when you had asked for skip. Your colleague's find, again. Please pass it on.
This repo is archived. You cannot comment on issues.