Skip to content

Build1 publisher3 min readPublished

Calendar clients ignore an update whose SEQUENCE is not higher than the one they already hold

A Node.js walkthrough traces duplicate events and cancellations that never land to three fields in RFC 5546. Getting them right costs a UID and a sequence counter stored per event, plus one regex that reads METHOD back out of the file.

The Engineer · Build desk

Illustration accompanying Calendar clients ignore an update whose SEQUENCE is not higher than the one they already hold

What happened

  • A Node.js walkthrough starts from a familiar failure: you move the meeting by an hour, and every attendee is left holding two events instead of one.
  • It puts the cause in RFC 5546, the iTIP scheduling rules that sit on top of the RFC 5545 format describing the .ics file itself.
  • An update or a cancellation has to reuse the original event's UID, because to a calendar client a new UID is a new event.
  • The example module reads the METHOD line out of the generated file with a regex and hands that value to Nodemailer, defaulting to PUBLISH when the match fails.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint iTIP makes a stateless mailer stateful: code that does not store the UID and the last sequence number per event cannot later move or cancel what it sent.
  • exposure With PUBLISH as the fallback, a file whose METHOD line does not parse still gets mailed, so the defect surfaces in attendees' clients rather than in the sender's error log.
  • decision Choosing the hosted generator puts a token and a network call on the cancellation path, and the helper throws when the response is not ok, so a failed call leaves the meeting sitting in every calendar.
  • capability Because the whole contract is three fields in a text file, a unit test using Nodemailer's JSON transport can verify it without an SMTP server or a real calendar client.

An invite you can update needs two things kept in your own storage: the UID it was created with, and the last SEQUENCE you sent [6]. RFC 5546 requires the organizer to increment SEQUENCE whenever the start, end, recurrence or status changes [7], and clients ignore an update whose sequence is not higher than the one they already have [8]. So the number has to be read back from storage, not recomputed from the event. The ladder in the post runs 0 on create, 1 on the move, 2 on the cancel, all under one UID [11]. Nothing in the published code sets it; the listing stops mid-function [25].

Two of the three bugs the post opens with are the same bug. Duplicate events and an event that survives its own cancellation both come from a client failing to match the incoming file to the one it holds, and that match is the UID plus the sequence number [6][8]. Missing Accept and Decline buttons in Gmail are a different failure [3]: the file can be correct and the buttons still will not appear if the mail's calendar part declares a different method than the METHOD line inside the attachment [10]. That one is a mail-layer mismatch, one level up from iTIP [26].

The module handles it well. It parses the method out of the generated file and passes whatever it finds into `icalEvent` [24]. The comment in the source says why: Nodemailer's method has to match the METHOD line inside the file, so the code reads it from the file instead of hardcoding it [21].

The fallback is where I would change it. The regex is `/^METHOD:(\w+)/m` and the default on no match is `PUBLISH` [20]. METHOD:REQUEST is an invitation and METHOD:CANCEL is a cancellation [9]; PUBLISH is neither of those [27]. If the generator ever returns a file whose METHOD line is absent, the regex misses, the mail goes out as a publish, and the defect lands in an attendee's client. RFC 5545 mistakes already tend to fail silently in one client only [12]. I would throw on a missing METHOD.

Route choice is the other thing to decide. The example posts each event to api.eu.apyhub.com with an `apy-token` header and gets the .ics text back [18], and the same request body with `?event_type=cancel` returns the cancellation [16]. There is a second endpoint that returns a signed download link for "Add to calendar" buttons [17]. Hand-writing the text skips the network hop, but then you own the 75-octet line folding and the character escapes that RFC 5545 requires [12]. The `ics` package sits in between, and the post recommends it when your times are already in UTC [14].

Three fields, three assertions. Build the create, update and cancel artifacts, then check that the UID is identical across all three, that SEQUENCE strictly increases, and that the method handed to `icalEvent` equals the METHOD line in the file [24]. Node 18 or later, for the built-in `fetch` [23].

What to watch

  • Whether the rest of invite.mjs is published, including where the sequence counter is stored and read between sends.
  • Whether the cancel path assigns SEQUENCE itself or expects the caller to pass the next number in the request body.
  • Whether the three assertions survive a client-by-client test, given that RFC 5545 mistakes can fail in only one client.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories