TW-4881: Air folder counts, hardening, and Gmail-style invite card#67
Merged
qasim-nylas merged 3 commits intomainfrom Apr 30, 2026
Merged
TW-4881: Air folder counts, hardening, and Gmail-style invite card#67qasim-nylas merged 3 commits intomainfrom
qasim-nylas merged 3 commits intomainfrom
Conversation
…ing, sync
Single-commit roll-up of the Air audit work driven by the Sent-folder bug
(folder counts wrong on real Gmail accounts) plus the surrounding
hardening uncovered along the way. Splits into three buckets:
Folder counts and per-folder coverage (the headline fix)
* handlers_email.go: handleListEmails short-circuits the cache only
when a folder filter is paired with a full-page hit. Partial coverage
falls through to the API so non-Inbox folders no longer render a
1-message stub. from/search filters keep prior behavior — they
operate on the full cached dataset and are authoritative.
* static/js/email-core.js: shared setFolderBadge plus
updateActiveFolderBadge / refreshFolderBadge / refreshPrimaryFolderBadges
so the sidebar reconciles to per-message unread instead of trusting
Nylas's stale folder.unread_count aggregate (the SENT label on Gmail
routinely showed 1 when 39 were truly unread).
* static/js/email-folders.js: loadFolders fires the eager primary-folder
badge refresh in the background so the initial paint is correct
without waiting for a click.
* server_sync.go: syncAccount runs syncFolders first and feeds the list
into syncEmails, which now fans out per primary system folder
(Inbox -> Sent -> Drafts -> Archive -> Trash -> Spam, priority order,
custom labels skipped) via GetMessagesWithParams. Falls back to
unfiltered top-100 on folder-API outage; per-folder failures don't
abort the iteration; honors ctx.Err() under deadline.
Security and correctness hardening
* cache/photos.go: validateContactID rejects path traversal on Put/Get/
IsValid/Delete (control bytes, /, \\, .., over-length, NUL).
* cache/cache.go, cache/encryption.go: cache files/dirs use 0750/0600.
* handlers_bundles.go: validateBundleRule + RW-locked compiledBundleRegex
cache prevents catastrophic-backtracking DoS on rule updates.
* handlers_read_receipts.go: User-Agent capped at 256 bytes;
GetTrackingPixelURL URL-escapes the email id; Cache-Control: no-store
contract pinned by tests.
* handlers_screener.go: IsSenderAllowed only returns true for explicit
"allowed" status; pending senders no longer bypass screening.
* handlers_focus_mode.go: IsFocusModeActive / ShouldAllowNotification
honor EndsAt at call time so expired sessions don't keep silencing
notifications.
* handlers_scheduled_send.go: 1-year ceiling on send_at; tightened
"too-soon" rejection.
* handlers_email_invite.go (new): /api/emails/{id}/invite parses iCal
payloads with strict CRLF tolerance + speakable RSVP buttons in the
preview pane.
* static/js/notetaker-ui.js, notetaker-actions.js: notetaker render
pipeline routes nt.summary / meetingTitle / attendees through
sanitizeHtml (DOMParser) plus escapeHtml on every interpolated field;
fixes pre-existing "this.escapeHtml is not a function" + closes XSS
via attacker-supplied subjects/HTML.
* static/js/email-folders.js: escapeHtml uses explicit replaceAll chain
(no .innerHTML round-trip) so attribute and element contexts are both
safe.
* static/js/utils.js, contacts-utils.js, email-renderer.js, shortcuts.js:
consistent isSafeUrl / sanitizeHtml usage; templates audited.
* static/css/calendar-grid.css: .event-relative-time pill no longer
blocks pointer events on .event-edit-btn (z-index + pointer-events).
Server lifecycle, sync recovery, demo-mode UX
* server.go, server_lifecycle.go, server_stores.go: deferred panic
recovery and clean shutdown of sync goroutines; offline-queue init
only opens the default grant.
* server_sync.go: runSyncIteration + recoverSyncPanic isolate panics so
one failure can't kill the loop or the process.
* handlers_email.go: demo mode filters its dataset by
folder/unread/starred so the sidebar exercises every system folder
without a real account.
* Various templates/css polish (calendar event card, header, modals)
and a new email-invite preview block.
Tests added
* Go: TestHandleListEmails_FolderPartialCache_FallsThroughToAPI,
TestHandleListEmails_FolderFullCache_ShortCircuits,
TestSyncEmails_PerFolderHydration,
TestSyncEmails_FallsBackWhenFoldersEmpty,
TestPrimarySystemFolderIDs_OrdersAndFilters,
TestPrimarySystemFolderIDs_EmptyInput, plus dedicated suites for
photos validation, cache file modes, bundle validation, focus mode,
screener concurrency, scheduled send bounds, email invite parsing,
and sync panic recovery.
* Playwright: email-operations-folders (3 new badge cases),
feature-handlers, folders-and-invite, round4-regressions,
security-handlers, security-xss.
Verification: full Air Go suite green with -race, golangci-lint 0
issues, all email Playwright specs passing. Manually verified on a real
Gmail account: Sent badge 1 -> 39 within ~1s of opening Air; Sent list
shows real contents.
…llback
Air's email viewer fails to render calendar invitations sent through
Gmail / Nylas-managed grants. Two compounding causes:
1. Nylas v3's single-message API surfaces inline calendar parts as
synthetic attachments[] entries with IDs like
"v0:base64(filename):base64(content-type):size". They look real
but the attachments download endpoint 404s on them — the previous
handler turned that 404 into a 5xx, hiding the card entirely.
2. Gmail invitations frequently ship the ICS as an inline body part
under multipart/alternative. Nylas's attachments[] list omits
those, so the previous attachments-only detection missed them.
Fix:
- Replace the hand-rolled ICS parser with github.com/arran4/golang-ical
for real RFC 5545 line-folding, TZID resolution, VALUE=DATE all-day
detection, ATTENDEE list, and METHOD parsing.
- New net/mail + mime/multipart walker (handlers_email_invite_mime.go)
finds text/calendar parts in raw_mime, including nested
multipart/alternative and quoted-printable / base64 bodies, with
depth + size caps.
- Handler now: try attachments[] first; if download fails OR no
calendar in attachments[], fall through to raw_mime walker. Errors
silently degrade to has_invite=false instead of 5xx.
- CalendarInviteResponse gains Method, Attendees, RecurrenceRule.
New InviteAttendee carries name/email/status/role/is_organizer.
- Frontend: looksLikeInviteSubject heuristic triggers /invite for
Gmail-style subjects ("Invitation:", "Event Invitation:",
"Updated event:", "Canceled event:") even when attachments[] is
empty. Card renders attendee chips with PARTSTAT colour-coding,
"X going · Y declined" tally, and a cancellation banner that
replaces RSVP buttons when METHOD=CANCEL.
- ensureInviteAttachmentRow injects an "invite.ics" attachment row
in the detail pane when one isn't already rendered, mirroring
Gmail's UX for inline calendar parts.
- Bump service worker CACHE_NAME (v1 → v2) so deployed clients
pick up the new JS/CSS instead of stale-while-revalidate-ing
the old assets.
MockClient gains GetMessageWithFieldsFunc so tests can pin raw_mime
responses; default behaviour preserved.
Tests:
- Go: 5 MIME walker tests (Gmail shape, nested, no-calendar,
oversize, quoted-printable), 4 handler tests (raw_mime fallback,
synthetic-ID fallback regression, no-calendar, real-attachment
priority), 2 parser tests (attendees+method, cancel), inline-id
helpers, plus existing parser tests adapted for golang-ical.
- Playwright: attendee chip + summary tally, METHOD=CANCEL banner,
looksLikeInviteSubject matrix, ensureInviteAttachmentRow injection.
Verified live against a real Google account email where Nylas
returned a synthetic v0: attachment ID — card renders end-to-end
with title, time, organizer, attendee, and RSVP buttons.
CodeQL flagged stripEmbeddedStyles in notetaker-ui.js for incomplete multi-character sanitization — the <style> regex is bypassable by nested patterns like <sty<style>le>. The runtime-active copy in notetaker-actions.js (which shadows ui.js via Object.assign load order) and stripEmailCruft had the same vulnerability. All three now re-parse via DOMParser and remove <style> elements / style="" attrs through DOM operations.
agtang96
approved these changes
Apr 30, 2026
AaronDDM
approved these changes
Apr 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
raw_mimefallback for Nylas synthetic attachment IDsTest plan
make ci-fullpasses--workers=1)