Rebuilt from: session transcript. Opening date is approximate.
*The original description was not recovered.*
**Referenced by commits**
- coffey-labs/ihasmail@0e58b886b582 2026-09-02 Define the zones an export names, instead of only naming them
- coffey-labs/ihasmail@1a6158aa7051 2026-09-02 Export a calendar as an iCAL file
<sub>Rebuilt from: session transcript. Opening date is approximate.</sub>
Done, in #227 — live now as 2026.9.2+pr227. Minor priority or not, it was the last thing calendars were missing that address books already had, which made it hard to leave alone.
It's in the calendar's own menu beside the import, so "which calendar?" is answered by where you clicked. No rights check on it: exporting is reading, and a calendar you can't read isn't in your list to begin with.
Unlike the import, this one is written in the browser. The import hands parsing to Stalwart because CalendarEvent/parse exists and reimplementing an .ics reader would be foolish — but there's no method the other way, in Stalwart or in the JMAP calendar drafts, so the file is built from the RFC 8984 objects the server already returns. That turns out to be mostly renaming, since 8984 was written as a restatement of 5545.
Three things I'd rather tell you than have you discover in whatever you import it into:
Recurring events export as the rule, not as the occurrences. A weekly meeting leaves as one VEVENT with its RRULE. A changed occurrence gets its own VEVENT with the same UID and a RECURRENCE-ID, a cancelled one becomes an EXDATE. An export that had flattened the rule into a year of identical events would import somewhere else as something nobody can maintain.
No VTIMEZONE components. A TZID names the IANA zone — Europe/Berlin — with nothing defining it beside it. Writing the definition means shipping a zone database to describe rules the reader's own system already knows, and every client that matters resolves IANA names. A strict validator will complain about this, so you should know it's deliberate. The alternative is converting everything to UTC, which is worse than a complaint: a weekly 09:00 that becomes 08:00 for half the year is a wrong calendar, not a pedantic one. If something in your estate actually refuses it, tell me and I'll reconsider — that's the one decision here I'd change on evidence.
Recurrence overrides are applied at the top level only. An override is a JSON patch, and one addressing a nested path like locations/x/name isn't flattened; the occurrence keeps the master's value for it. A moved time or a changed title comes across fine.
Worth being straight about one gap: the writer is well covered by tests — zones, UNTIL in both forms, escaping, folding, attendees, alarms — and two of them feed the output back through ihasmail's own iCalendar parser. But I haven't round-tripped a real export into Thunderbird or SOGo. If your colleagues do that before I do, I'd genuinely like to hear what those two make of it.
Done, in #227 — live now as `2026.9.2+pr227`. Minor priority or not, it was the last thing calendars were missing that address books already had, which made it hard to leave alone.
It's in the calendar's own menu beside the import, so "which calendar?" is answered by where you clicked. No rights check on it: exporting is reading, and a calendar you can't read isn't in your list to begin with.
Unlike the import, this one is written in the browser. The import hands parsing to Stalwart because `CalendarEvent/parse` exists and reimplementing an `.ics` reader would be foolish — but there's no method the other way, in Stalwart or in the JMAP calendar drafts, so the file is built from the RFC 8984 objects the server already returns. That turns out to be mostly renaming, since 8984 was written as a restatement of 5545.
Three things I'd rather tell you than have you discover in whatever you import it into:
**Recurring events export as the rule, not as the occurrences.** A weekly meeting leaves as one VEVENT with its RRULE. A changed occurrence gets its own VEVENT with the same UID and a RECURRENCE-ID, a cancelled one becomes an EXDATE. An export that had flattened the rule into a year of identical events would import somewhere else as something nobody can maintain.
**No VTIMEZONE components.** A TZID names the IANA zone — `Europe/Berlin` — with nothing defining it beside it. Writing the definition means shipping a zone database to describe rules the reader's own system already knows, and every client that matters resolves IANA names. A strict validator will complain about this, so you should know it's deliberate. The alternative is converting everything to UTC, which is worse than a complaint: a weekly 09:00 that becomes 08:00 for half the year is a wrong calendar, not a pedantic one. If something in your estate actually refuses it, tell me and I'll reconsider — that's the one decision here I'd change on evidence.
**Recurrence overrides are applied at the top level only.** An override is a JSON patch, and one addressing a nested path like `locations/x/name` isn't flattened; the occurrence keeps the master's value for it. A moved time or a changed title comes across fine.
Worth being straight about one gap: the writer is well covered by tests — zones, UNTIL in both forms, escaping, folding, attendees, alarms — and two of them feed the output back through ihasmail's own iCalendar parser. But I haven't round-tripped a real export into Thunderbird or SOGo. If your colleagues do that before I do, I'd genuinely like to hear what those two make of it.
One practical thing I hit while testing this, worth knowing before your colleagues do.
Exporting two calendars in a row, Chrome silently swallows the second file. The toast says "Exported 4 events" and no file arrives. It's Chrome's guard against a page starting multiple automatic downloads — the second one waits on a permission prompt that is easy to miss, or is suppressed outright until you allow it. A page reload clears it, and clicking Allow when the prompt does appear makes it stick for the site.
Nothing in ihasmail can see that happen: the download is handed to the browser and the browser decides. So the toast is telling the truth about what was exported, and the file is simply not being written. I'm mentioning it because "it said it worked and there's no file" is a bug report waiting to happen, and the answer is in the address bar rather than in the app.
If several of you are exporting a set of calendars in one go, that's exactly the pattern that trips it.
One practical thing I hit while testing this, worth knowing before your colleagues do.
**Exporting two calendars in a row, Chrome silently swallows the second file.** The toast says "Exported 4 events" and no file arrives. It's Chrome's guard against a page starting multiple automatic downloads — the second one waits on a permission prompt that is easy to miss, or is suppressed outright until you allow it. A page reload clears it, and clicking *Allow* when the prompt does appear makes it stick for the site.
Nothing in ihasmail can see that happen: the download is handed to the browser and the browser decides. So the toast is telling the truth about what was exported, and the file is simply not being written. I'm mentioning it because "it said it worked and there's no file" is a bug report waiting to happen, and the answer is in the address bar rather than in the app.
If several of you are exporting a set of calendars in one go, that's exactly the pattern that trips it.
I need to correct something I told you above, before anyone tests against it.
I said no VTIMEZONE components were needed because "every client that matters resolves IANA names", and that generating them would mean shipping a zone database. I also said it was the one decision here I'd change on evidence. The evidence turned up within the hour, and both halves of that reasoning were wrong.
I went to check an export in Thunderbird, found it wasn't installed, and used ical.js instead — which is Mozilla's own iCalendar library, and the one Thunderbird's calendar is built on. A TZID with nothing defining it does not resolve there. It falls back to floating time:
as exported | zone: floating | UTC: 2026-09-02T09:00:00Z
with a VTIMEZONE added | zone: America/Phoenix | UTC: 2026-09-02T16:00:00Z
Same file, same parser, seven hours apart. Every timed event in every export, silently wrong for anyone not already sitting in the exporting user's zone — and wrong in precisely the client your colleagues use. "A weekly 09:00 that becomes 08:00 for half the year is a wrong calendar rather than a pedantic one" is what I wrote arguing against converting to UTC. I then shipped something worse.
The second half was wrong too, and more embarrassingly so: the browser already has the IANA database, behind Intl. An offset for an instant is a formatting question — format the instant into the zone, read the clock back, and the difference is the offset. Transitions are found by walking month by month for the ones where that answer changes and bisecting inside them. No zone database to ship, and no rules known, so no rules to get wrong.
Fixed in #229, live now as 2026.9.2+pr229. Every zone an export names is now defined in the file. Each transition is written as its own dated sub-component rather than as an RRULE — a derived rule that's subtly wrong moves somebody's meeting, while a list of dates can only be incomplete at its ends, which is what the window is for: from the year before the earliest event to ten years past the latest, sized for an open-ended weekly meeting.
Checked the way it was found. Berlin now resolves to +0200 in September and +0100 in December, so the transitions are being applied and not just a single offset. Phoenix, which has no DST, comes out as one standing rule. All-day events still carry no zone, which is correct.
If anyone already pulled an export before this afternoon, throw it away and take a fresh one — the old ones will import at the wrong time of day.
Sorry for the noise on your issue. You asked for a calendar export and got a lecture about time zones followed by a retraction of it.
I need to correct something I told you above, before anyone tests against it.
I said no VTIMEZONE components were needed because "every client that matters resolves IANA names", and that generating them would mean shipping a zone database. I also said it was the one decision here I'd change on evidence. The evidence turned up within the hour, and both halves of that reasoning were wrong.
I went to check an export in Thunderbird, found it wasn't installed, and used ical.js instead — which is Mozilla's own iCalendar library, and the one Thunderbird's calendar is built on. A `TZID` with nothing defining it does not resolve there. It falls back to **floating** time:
```
as exported | zone: floating | UTC: 2026-09-02T09:00:00Z
with a VTIMEZONE added | zone: America/Phoenix | UTC: 2026-09-02T16:00:00Z
```
Same file, same parser, seven hours apart. Every timed event in every export, silently wrong for anyone not already sitting in the exporting user's zone — and wrong in precisely the client your colleagues use. "A weekly 09:00 that becomes 08:00 for half the year is a wrong calendar rather than a pedantic one" is what I wrote arguing *against* converting to UTC. I then shipped something worse.
The second half was wrong too, and more embarrassingly so: the browser already has the IANA database, behind `Intl`. An offset for an instant is a formatting question — format the instant into the zone, read the clock back, and the difference is the offset. Transitions are found by walking month by month for the ones where that answer changes and bisecting inside them. No zone database to ship, and no rules known, so no rules to get wrong.
Fixed in #229, live now as `2026.9.2+pr229`. Every zone an export names is now defined in the file. Each transition is written as its own dated sub-component rather than as an RRULE — a derived rule that's subtly wrong moves somebody's meeting, while a list of dates can only be incomplete at its ends, which is what the window is for: from the year before the earliest event to ten years past the latest, sized for an open-ended weekly meeting.
Checked the way it was found. Berlin now resolves to +0200 in September and +0100 in December, so the transitions are being applied and not just a single offset. Phoenix, which has no DST, comes out as one standing rule. All-day events still carry no zone, which is correct.
If anyone already pulled an export before this afternoon, throw it away and take a fresh one — the old ones will import at the wrong time of day.
Sorry for the noise on your issue. You asked for a calendar export and got a lecture about time zones followed by a retraction of it.
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 original description was not recovered.
Referenced by commits
Rebuilt from: session transcript. Opening date is approximate.
Done, in #227 — live now as
2026.9.2+pr227. Minor priority or not, it was the last thing calendars were missing that address books already had, which made it hard to leave alone.It's in the calendar's own menu beside the import, so "which calendar?" is answered by where you clicked. No rights check on it: exporting is reading, and a calendar you can't read isn't in your list to begin with.
Unlike the import, this one is written in the browser. The import hands parsing to Stalwart because
CalendarEvent/parseexists and reimplementing an.icsreader would be foolish — but there's no method the other way, in Stalwart or in the JMAP calendar drafts, so the file is built from the RFC 8984 objects the server already returns. That turns out to be mostly renaming, since 8984 was written as a restatement of 5545.Three things I'd rather tell you than have you discover in whatever you import it into:
Recurring events export as the rule, not as the occurrences. A weekly meeting leaves as one VEVENT with its RRULE. A changed occurrence gets its own VEVENT with the same UID and a RECURRENCE-ID, a cancelled one becomes an EXDATE. An export that had flattened the rule into a year of identical events would import somewhere else as something nobody can maintain.
No VTIMEZONE components. A TZID names the IANA zone —
Europe/Berlin— with nothing defining it beside it. Writing the definition means shipping a zone database to describe rules the reader's own system already knows, and every client that matters resolves IANA names. A strict validator will complain about this, so you should know it's deliberate. The alternative is converting everything to UTC, which is worse than a complaint: a weekly 09:00 that becomes 08:00 for half the year is a wrong calendar, not a pedantic one. If something in your estate actually refuses it, tell me and I'll reconsider — that's the one decision here I'd change on evidence.Recurrence overrides are applied at the top level only. An override is a JSON patch, and one addressing a nested path like
locations/x/nameisn't flattened; the occurrence keeps the master's value for it. A moved time or a changed title comes across fine.Worth being straight about one gap: the writer is well covered by tests — zones, UNTIL in both forms, escaping, folding, attendees, alarms — and two of them feed the output back through ihasmail's own iCalendar parser. But I haven't round-tripped a real export into Thunderbird or SOGo. If your colleagues do that before I do, I'd genuinely like to hear what those two make of it.
One practical thing I hit while testing this, worth knowing before your colleagues do.
Exporting two calendars in a row, Chrome silently swallows the second file. The toast says "Exported 4 events" and no file arrives. It's Chrome's guard against a page starting multiple automatic downloads — the second one waits on a permission prompt that is easy to miss, or is suppressed outright until you allow it. A page reload clears it, and clicking Allow when the prompt does appear makes it stick for the site.
Nothing in ihasmail can see that happen: the download is handed to the browser and the browser decides. So the toast is telling the truth about what was exported, and the file is simply not being written. I'm mentioning it because "it said it worked and there's no file" is a bug report waiting to happen, and the answer is in the address bar rather than in the app.
If several of you are exporting a set of calendars in one go, that's exactly the pattern that trips it.
I need to correct something I told you above, before anyone tests against it.
I said no VTIMEZONE components were needed because "every client that matters resolves IANA names", and that generating them would mean shipping a zone database. I also said it was the one decision here I'd change on evidence. The evidence turned up within the hour, and both halves of that reasoning were wrong.
I went to check an export in Thunderbird, found it wasn't installed, and used ical.js instead — which is Mozilla's own iCalendar library, and the one Thunderbird's calendar is built on. A
TZIDwith nothing defining it does not resolve there. It falls back to floating time:Same file, same parser, seven hours apart. Every timed event in every export, silently wrong for anyone not already sitting in the exporting user's zone — and wrong in precisely the client your colleagues use. "A weekly 09:00 that becomes 08:00 for half the year is a wrong calendar rather than a pedantic one" is what I wrote arguing against converting to UTC. I then shipped something worse.
The second half was wrong too, and more embarrassingly so: the browser already has the IANA database, behind
Intl. An offset for an instant is a formatting question — format the instant into the zone, read the clock back, and the difference is the offset. Transitions are found by walking month by month for the ones where that answer changes and bisecting inside them. No zone database to ship, and no rules known, so no rules to get wrong.Fixed in #229, live now as
2026.9.2+pr229. Every zone an export names is now defined in the file. Each transition is written as its own dated sub-component rather than as an RRULE — a derived rule that's subtly wrong moves somebody's meeting, while a list of dates can only be incomplete at its ends, which is what the window is for: from the year before the earliest event to ten years past the latest, sized for an open-ended weekly meeting.Checked the way it was found. Berlin now resolves to +0200 in September and +0100 in December, so the transitions are being applied and not just a single offset. Phoenix, which has no DST, comes out as one standing rule. All-day events still carry no zone, which is correct.
If anyone already pulled an export before this afternoon, throw it away and take a fresh one — the old ones will import at the wrong time of day.
Sorry for the noise on your issue. You asked for a calendar export and got a lecture about time zones followed by a retraction of it.