Skip to content

feat: route mailto links through Chorus services - #35

Open
jpagh wants to merge 1 commit into
nicojan:mainfrom
jpagh:feature/mailto-links
Open

jpagh wants to merge 1 commit into
nicojan:mainfrom
jpagh:feature/mailto-links

Conversation

@jpagh

@jpagh jpagh commented Sep 30, 2026

Copy link
Copy Markdown

Summary

Chorus now handles mailto: links itself instead of handing them to the system mail app.

  • Each service discovers a mail handler from its own registerProtocolHandler call or web app manifest. Chorus validates the handler against the service's own origin over HTTPS and stores it on that service instance.
  • Mail links queue and route through an account chooser. The chosen account opens a compose window scoped to its website data store, and the queue keeps its order across errors, locking, and dismissal.
  • Sites that only call registerProtocolHandler when they see a Chromium browser still declare a handler through a short, invisible probe with a Chromium user agent. The probe runs at most once per service per five minutes, so a handler declared after sign-in is still found without a hidden page load on every navigation.
  • An invalid registration no longer hides a valid one for the same service, and a declaration from a subframe cannot register a handler.

Test plan

  • Full Debug suite: 349 tests, 1 skipped (the real-store-copy migration test), 0 failures
  • Focused runs: router, AppState routing, compose window session, manifest discovery
  • xcodegen generate reproduces the committed project file byte for byte
  • Live-provider sending, desktop focus, accessibility, and the real-store-copy migration remain manual checks

This edits EditServiceSheet.swift, a settings view, so it can collide with another change to a settings view.

@jpagh
jpagh force-pushed the feature/mailto-links branch from 5221e1e to 01b8f1b Compare September 30, 2026 23:52
Discover standards-declared mail handlers from each service's own
registerProtocolHandler call or web app manifest, validate them against the
service's origin over HTTPS, and store them on that instance. Queue mail links
through an account chooser; the chosen account opens a compose window scoped
to its own website data store, and the queue keeps its order across errors,
locking, and dismissal.

Sites that only call registerProtocolHandler when they see a Chromium browser
still declare a handler through a serial, invisible Chromium-UA probe. The
probe runs at most once per service per five minutes, so a declaration made
after sign-in is still found without a hidden page load on every navigation.

An invalid registration no longer shadows a valid one for the same service,
and a declaration from a subframe is rejected by the router instead of being
trusted by the bridge. The stored template and declaring origin now travel as
one value; the chooser keeps account identity, provider, and space context.

Validation: full Debug suite passed (349 tests, 1 skipped real-store-copy
migration, 0 failures), with focused router, AppState routing, compose-session,
and manifest-discovery runs. xcodegen reproduces the committed project file
byte for byte. Live-provider sending, focus, accessibility, and the real-store
copy remain manual checks.
@jpagh
jpagh force-pushed the feature/mailto-links branch from 01b8f1b to a77dd91 Compare October 4, 2026 01:43

@nicojan nicojan left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Jack. This is a lot of careful work. The Info.plist handler, the routing through application(_:open:), the account chooser and the schema stage are all good, and I checked the frozen ChorusSchemaV1_5_19 against the live models field by field. But three choices change Chorus for everyone, and I can't take them as they stand.

Blocking

  1. Mail links inside a service no longer go to the user's mail app. .routeMail in WebViewCoordinator sends every mail link click to the router. Someone who uses Apple Mail and has no mail service in Chorus now gets "None of your added services has declared support…", and the link is lost. Someone whose default is Outlook gets Gmail. If Chorus is the system default, openExternally already comes back through application(_:open:), so there is no loop to prevent. Please keep openExternally for clicks inside services.
  2. A page can claim mail links without asking. acceptMailHandlerDeclaration saves and enables any template a main-frame page on the service's origin passes in, and a single eligible service skips the chooser. Browsers ask first. Please ask the user the first time, and whenever the template changes, showing the origin and path. Skip the save when nothing changed, too, since a page that calls it in a loop writes the store each time.
  3. Please drop the hidden Chrome probe. It runs for every HTTPS service without a handler: Slack, Teams, WhatsApp, Discord. That's a second WebContent process loading the whole app with the user's cookies, at every launch and every five minutes after a load. It writes into the same data store as the visible page, and it shows the site one session from two browsers. Gmail is the only reason for it, so a compose template in ServiceCatalog for the few providers that need one would do the job. The same goes for the manifest fetch on every page load of every service.

Would make it smaller

  1. Main now has service tabs (opensAsTab, ServiceTabs.swift). Opening the compose URL as a tab in the service's card would get attachments, popups, downloads, the content blocker and the app lock for free, and you could delete MailComposeWindowSession, both close scripts and most of the window tests. As it is, the window has no open panel and no createWebViewWith, and it stays usable while Chorus is locked.
  2. The auto-close guess (no editable field for 500ms) can close a draft when Gmail's compose box is minimized or re-renders. I'd let the user close it.
  3. Please leave out the unrelated changes: AppDependencies, the second AppState.init that points the store at /dev/null, and DataStoreManager.makeStore.
  4. The branch is based on 49eb983, eight releases back, and touches the same code as the tabs work, so it needs a rebase.

Smaller things

  • The subframe test passes for the wrong reason: an iframe with no src is about:blank, so the origin check rejects it before the isMainFrame check runs. Give it a same-origin src.
  • Use frameInfo.securityOrigin rather than frameInfo.request.url ?? webView.url.
  • Name the schema version after the release it ships in, not 1.5.20.
  • The loopback private key under ChorusTests/Fixtures is harmless, but the repo's secret scanners will flag it. Generating it at test time avoids that.
  • In-app text: "declared support" and "register" won't mean much to most people. I'd say what to do instead, such as "Open Gmail in Chorus first."

Happy to talk through any of this before you start.

@jpagh

jpagh commented Oct 6, 2026

Copy link
Copy Markdown
Author

Thanks. I'll take a look. I'm actually really unhappy with how Gmail operates. The fact that it won't even offer mailto support without thinking you're using Chrome is super annoying.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants