Message-ID and Mail.app Deep Linking on iOS and macOS
Last week, we concluded our discussion of device identifiers with a brief foray into the ways apps use device fingerprinting to work around Apple’s provided APIs to track users without their consent or awareness. In response, a few readers got in touch to explain why their use of fingerprinting to bridge between Safari and their native app was justified.
At WWDC 2018,
Apple announced that
starting in iOS 11 apps would no longer have access to a shared cookie store.
Previously,
if a user was logged into a website in Safari on iOS
and installed the native app,
the app could retrieve the session cookie from an SFSafari
to log the user in automatically.
The change was implemented as a countermeasure against
user tracking by advertisers and other third parties,
but came at the expense of certain onboarding flows used at the time.
While iCloud Keychain, Shared Web Credentials, Password Autofill, Universal Links, and Sign in with Apple have gone a long way to minimize friction for account creation and authentication, there are still a few use cases that aren’t entirely covered by these new features.
In this week’s article,
we’ll endeavor to answer one such use case, specifically:
How to do seamless “passwordless” authentication via email on iOS.
Mail and Calendar Integrations on Apple Platforms
When you view an email on macOS and iOS, Mail underlines detected dates and times. You can interact with them to create a new calendar event. If you open such an event in Calendar, you’ll see a “Show in Mail” link in its extended details. Clicking on this link takes you back to the original email message.
This functionality goes all the way back to the launch of the iPhone; its inclusion in that year’s Mac OS X release (Leopard) would mark the first of many mobile features that would make their way to the desktop.
If you were to copy this “magic” URL to the pasteboard and view in a text editor, you’d see something like this:
"message:%3C1572873882024.NSHIPSTER%40mail.example.com%3E"
Veteran iOS developers will immediately recognize this to use a custom URL scheme. And the web-savvy among them could percent-decode the host and recognize it to be something akin to an email address, but not.
So if not an email address, what are we looking at here?
It’s a different email field known as a Message-ID.
Message-ID
RFC 5322 §3.6.4
prescribes that every email message SHOULD
have a “Message-ID:” field
containing a single unique message identifier.
The syntax for this identifier is essentially an email address
with enclosing angle brackets (<>
).
Although the specification contains no normative guidance for what makes for a good Message-ID, there’s a draft IETF document from 1998 that holds up quite well.
Let’s take a look at how to do this in Swift:
Generating a Random Message ID
The first technique described in the aforementioned document
involves generating a random Message ID with a 64-bit nonce,
which is prepended by a timestamp to further reduce the chance of collision.
We can do this rather easily
using the random number generator APIs built-in to Swift 5
and the
String(_:radix:uppercase:)
initializer:
import Foundation
let timestamp = String(Int(Date().time Interval Since1970 * 1000))
let nonce = String(UInt64.random(in: 0..<UInt64.max), radix: 36, uppercase: true)
let domain = "mail.example.com"
let Message ID = "<\(timestamp).\(nonce)@\(domain)>"
//"<[email protected]>"
We could then save the generated Message-ID with the associated record in order to link to it later. However, in many cases, a simpler alternative would be to make the Message ID deterministic, computable from its existing state.
Generating a Deterministic Message ID
Consider a record structure that conforms to
Identifiable
protocol
and whose associated ID
type is a
UUID.
You could generate a Message ID like so:
import Foundation
func message ID<Value>(for value: Value, domain: String) -> String
where Value: Identifiable, Value.ID == UUID
{
return "<\(value.id.uuid String)@\(domain)>"
}
Mobile Deep Linking
The stock Mail client on both iOS and macOS
will attempt to open URLs with the custom message:
scheme
by launching to the foreground
and attempting to open the message
with the encoded Message-ID field.
Generating a Mail Deep Link with Message ID
With a Message-ID in hand,
the final task is to create a deep link that you can use to
open Mail to the associated message.
The only trick here is to
percent-encode
the Message ID in the URL.
You could do this with the
adding
method,
but we prefer to delegate this all to URLComponents
instead —
which has the further advantage of being able to
construct the URL full without a
format string.
import Foundation
var components = URLComponents()
components.scheme = "message"
components.host = Message ID
components.string!
// "message://%3C1572873882024.NSHIPSTER%40mail.example.com%3E"
Opening a Mail Deep Link
If you open a message:
URL on iOS
and the linked message is readily accessible from the INBOX
of one of your accounts,
Mail will launch immediately to that message.
If the message isn’t found,
the app will launch and asynchronously load the message in the background,
opening it once it’s available.
As an example, Flight School does this with passwordless authentication system. To access electronic copies of your books, you enter the email address you used to purchase them. Upon form submission, users on iOS will see a deep link to open the Mail app to the email containing the “magic sign-in link” ✨.
Other systems might use Message-ID to streamline passwordless authentication for their native app or website by way of Universal Links, or incorporate it as part of a 2FA strategy (since SMS is no longer considered to be secure for this purpose).
Unlike so many private integrations on Apple platforms, which remain the exclusive territory of first-party apps, the secret sauce of “Show in Mail” is something we can all get in on. Although undocumented, the feature is unlikely to be removed anytime soon due to its deep system integration and roots in fundamental web standards.
At a time when everyone from browser vendors and social media companies to governments — and even Apple itself, at times — seek to dismantle the open web and control what we can see and do, it’s comforting to know that email, nearly 50 years on, remains resolute in its capacity to keep the Internet free and decentralized.