Skills / Email provider auth / References

iCloud, Fastmail, Yahoo — and the TLS trap they exposed

6 min read

Tags as in SKILL.md.

The short version

[MEASURED] None of these publishes an OAuth flow for third-party mail clients. All three take an app password over ordinary IMAP and SMTP. No client ID exists or is needed, so there is nothing to register and nothing to ship.

Hosts and ports are [VENDOR]; the app-password forms are [VENDOR] too.

ProviderIMAPSMTPApp password form
iCloudimap.mail.me.com:993 TLSsmtp.mail.me.com:587 STARTTLSxxxx-xxxx-xxxx-xxxx — hyphens are part of the value
Fastmailimap.fastmail.com:993 TLSsmtp.fastmail.com:465 TLSFour groups of four alphanumerics, spacing is display only
Yahooimap.mail.yahoo.com:993 TLSsmtp.mail.yahoo.com:465 TLSFour groups of four alphanumerics, spacing is display only

[RULE] Normalise a pasted secret by stripping zero-width characters always, and whitespace only when the value matches the "four groups of four alphanumerics" display form. iCloud's hyphenated form must survive untouched — there the separators are what the provider expects back.

[VENDOR] None of these publishes a strict app-password pattern the way Google does. [RULE] So after a refusal the most you can honestly say is that this provider requires an app password rather than the account password — guessing at a shape would let you reject a valid credential.

[MEASURED 2026-09-14] Apple's real refusal, once the socket actually connects, looks like this — note the capability list, which is what a working connection to iCloud looks like:

S: * OK [CAPABILITY XAPPLEPUSHSERVICE IMAP4 IMAP4rev1 SASL-IR AUTH=ATOKEN
        AUTH=PLAIN AUTH=ATOKEN2 AUTH=XOAUTH2] …
S: 1 NO [AUTHENTICATIONFAILED] Authentication Failed

[MEASURED 2026-09-14] Fastmail's, at 599 ms RTT with the fix in place:

S: * OK IMAP4 ready
S: 2 NO Incorrect username, password or access token.

The trap: a TLS error on a socket that never connected

This is the one that matters, because it wastes a day pointing at Apple.

Symptom. Pairing an iCloud or Fastmail address fails instantly with:

The server's TLS certificate could not be verified
(Hostname/IP does not match certificate's altnames: Cert does not contain a DNS name).
code=ERR_TLS_CERT_ALTNAME_INVALID

[MEASURED 2026-09-14] The dialogue is one line and it is the error. Zero S: lines — the server's IMAP greeting never arrived, so no byte was ever exchanged. [RULE] A wire log with no server output at all is telling you the connection never happened. Read that before reading the error text; it is the fastest discriminator there is.

Cause. The general shape first, because it is not specific to one runtime: any client that races several addresses for a host (Happy Eyeballs, RFC 8305) may abandon an attempt and still run the TLS identity callback against that dead socket. The callback is then handed an empty certificate, correctly reports that it contains no matching name, and the message blames a certificate that never existed. If your platform has an option to connect to one address at a time, that option is the fix, whatever it is called.

The instance measured here: [MEASURED 2026-09-14] Bun's Happy Eyeballs connect path (1.3.13, still present in 1.4.2) fires the secureConnect callback on a socket that never connected, handing the TLS layer an empty certificate object. The identity check then correctly reports that an empty certificate contains no DNS name — and the message names a certificate that never arrived.

Trigger conditions, both needed: the host resolves to ≥2 addresses, and the first attempt takes more than 250 ms (the Happy Eyeballs timer).

[MEASURED 2026-09-14] The latency matrix that proved it:

                         TCP RTT   default   attemptTimeout:5000   autoSelectFamily:false
imap.mail.me.com          1542ms    FAIL            OK                     OK
imap.fastmail.com          516ms    FAIL            OK                     OK
imap.mail.yahoo.com         49ms      OK            OK                     OK
imap.gmail.com              53ms      OK            OK                     OK
outlook.office365.com       43ms      OK            OK                     OK

[MEASURED 2026-09-14] What a failing socket looks like, all at once: remoteAddress undefined, getProtocol() null, getCipher() undefined, Object.keys(getPeerCertificate()).length === 0, callback fired at 257–270 ms.

Ruled out by measurement: Apple's certificate (CN=imap.mail.me.com, SAN carries the right DNS name among 100, Verify return code: 0 (ok)), the SAN's size, the presence of AAAA records, Bun version skew, the provider config, and the app password.

Fix. [MEASURED 2026-09-14] Two settings each turn FAIL into OK: autoSelectFamily: false and raising the per-attempt timeout. Prefer autoSelectFamily: false — a longer timer only moves the threshold that triggers the race, while connecting one address at a time removes the race. This is also what a mainstream mail client does for mail sockets.

Distinguish "no certificate arrived" from "the certificate failed". A certificate object with neither a subject nor any SAN was never received, not verified — report it with its own error code so nothing downstream re-wraps it as a verification failure, and defer every other case to the platform's default check, unchanged.

[RULE] If your platform lets you override the hostname check, overriding it REPLACES the default — it does not run alongside it. So you must call the default yourself and return its verdict. Get this wrong and you have silently disabled hostname verification for every host that does present a certificate — a far worse bug than the one you set out to fix. Node/Bun code for both rules: runtime-node.md §1.

Where it goes. One options object on the IMAP client, used for both the implicit-TLS connect and the STARTTLS upgrade — a STARTTLS session that skips it is unprotected. [UNVERIFIED] Whether the SMTP leg needs the same: the SMTP client measured here connected by IP, which would side-step the bug, but that has never been confirmed against a host slower than 250 ms. If your SMTP client resolves names itself, apply it there too.

[RULE] Do not "fix" this by disabling certificate verification. The failure is a connection failure wearing a certificate failure's clothes; loosening the check would hide a real MITM later.

[RULE] Attach the app-password advice only to a failure where the server actually refused the credentials. Before this was separated, a dead port, a DNS failure and this TLS misfire all told the user to go generate an app password.

A trade-off this fix makes, deliberately: with autoSelectFamily off, a dead first address waits out the connection timeout instead of falling back. [MEASURED 2026-09-14] It failed instantly with the wrong error before, so nothing regresses — it is slower and correct rather than fast and wrong.

How to verify, and how you will fool yourself.

[MEASURED 2026-09-14] imap.mail.me.com sits behind an Akamai CNAME and its answers rotate, so the bug is intermittent by construction. One green run proves nothing — a run whose first address happened to answer in under 250 ms passes on completely unfixed code.

  1. Confirm the preconditions still hold on your network before testing: dig +short imap.mail.me.com returns ≥2 addresses, and a TCP connect to the first takes >250 ms. If they do not, you cannot reproduce it today and a pass means nothing.
  2. Pair the slow host repeatedly, not once. Pair Fastmail too.
  3. Pair with a deliberately wrong app password. You should now get Apple's real refusal after a real greeting — * OK [CAPABILITY …] then NO [AUTHENTICATIONFAILED] Authentication Failed. Server output at all is the proof the socket now connects.
  4. Prove you did not weaken the check: a host with a genuinely bad chain must still fail with self signed certificate in chain, and a certificate for the wrong hostname must still be rejected.
  5. Regression-test the classifier, not the connect path: assert that a cert object with no subject and no subjectaltname produces the no-certificate code and not the "could not be verified" wording. The connect-path bug itself is a Bun behaviour and cannot be reproduced against a local socket server.

Upstream bug status and upgrade result for Bun: runtime-node.md §1.