Opening a pre-filled e-mail in the user's desktop Outlook
A browser cannot detect which mail client the user has — no API exposes it, and "navigate to a protocol and watch for blur" is guesswork. Ask once and remember the answer. mailto: reliably reaches whatever client Windows has set as default (new and classic Outlook), but it cannot carry an attachment; for that, build the message as a .eml file and download it. The header that makes it work is X-Unsent: 1 — without it Outlook (especially new Outlook) opens the file read-only and there is no Send button.
Prohlížeč nemá jak zjistit, jakého poštovního klienta uživatel používá – žádné API to nedovolí. Zeptejte se jednou a volbu si zapamatujte. mailto: spolehlivě otevře klienta nastaveného ve Windows jako výchozí (nový i klasický Outlook), ale neunese přílohu; tu doručíte jako soubor .eml ke stažení. Klíčová je hlavička X-Unsent: 1 – bez ní se soubor otevře jen ke čtení.
The request
"Can it open my Outlook instead of Outlook on the web — and can it tell whether I use the new or the classic one?"
What is actually possible
1. Detection: no. There is no browser API that reports the registered mailto: handler, and none that tells you whether ms-outlook: or outlook: resolves to anything. The common workaround — navigate to a custom protocol and infer success from a blur/visibilitychange that arrives within n milliseconds — produces false negatives on a cold client start and false positives when the browser shows its own "open with?" prompt. Do not ship a guess. Ask once, store the choice (localStorage), and let the user flip it later.
2. Reaching the desktop: mailto:. It hands off to whichever client the OS has as default. That answers the "new or classic Outlook?" question implicitly and correctly — you never need to know which one it is. Keep the body short; long bodies in a URL silently fail to open.
3. An attachment cannot travel through mailto:. The attachment parameter is ignored by every modern client for obvious security reasons. Two real options remain:
| Draft via Graph | .eml file |
|
|---|---|---|
| Where it opens | Outlook on the web (webLink) |
user's default mail client |
| Permission | Mail.ReadWrite (tenant-approved) |
none |
| Needs an Exchange mailbox | yes | no |
| Practical size limit | ~3 MB (simple attachment POST) | whatever you are willing to hold in memory |
Building the .eml
const boundary = '----myapp' + blob.size;
const eml = [
'X-Unsent: 1', // ← without this: read-only
'Subject: ' + mimeWord(subject), // RFC 2047 for non-ASCII
'To: ',
'MIME-Version: 1.0',
'Content-Type: multipart/mixed; boundary="' + boundary + '"',
'',
'--' + boundary,
'Content-Type: text/html; charset="utf-8"',
'Content-Transfer-Encoding: base64',
'',
wrap76(b64Utf8(html)),
'',
'--' + boundary,
'Content-Type: application/octet-stream; name="' + mimeWord(name) + '"',
'Content-Transfer-Encoding: base64',
'Content-Disposition: attachment; filename="' + mimeWord(name) + '"; '
+ "filename*=UTF-8''" + encodeURIComponent(name),
'',
wrap76(base64OfTheFile),
'',
'--' + boundary + '--',
''
].join('\r\n');
saveBlob(new Blob([eml], { type: 'message/rfc822' }), baseName + '.eml');
Details that bite:
X-Unsent: 1first. This is the whole trick. New Outlook opens.emlin a reading view by default; the header switches it to compose.- CRLF line endings, not
\n. MIME parsers are strict about this. - Base64 wrapped at 76 characters. One enormous line is rejected or mangled by some clients.
- Non-ASCII needs encoding twice over: the subject and the attachment name via RFC 2047 encoded-words (
=?UTF-8?B?…?=), and the filename additionally as RFC 2231 (filename*=UTF-8''…) so newer clients get it right. btoaonly takes latin1 — convert through UTF-8 first (encodeURIComponent+%XX→ char). For binary content,FileReader.readAsDataURLgives you base64 without abtoaround trip;Blob.arrayBuffer()also type-checks in SPFx 1.22 (it is a DOM API, so the ES2015libdoes not hide it).- Revoke the object URL late (a timeout of ~20 s after the click), not immediately — some browsers start the download after the handler returns.
Consequences worth designing for
- Because the
.emlroute needs no Graph, check theMail.ReadWritegrant only when the user picks the web route. Probing the mailbox every time the dialog opens is a request spent on a feature most people will not use — and a missing grant must not disable a path that never needed it. - Downloading a file is not the same as a window popping open. Say so in the UI: "a ready-to-send message will be downloaded — open it and Outlook shows it as a new e-mail".
- Default the choice to where the users actually work, not to where your app runs.
Checklist
- No client detection anywhere in the code — a remembered user choice instead.
-
mailto:used for the desktop link route, with a short body. -
.emlcarriesX-Unsent: 1, CRLF endings and 76-char base64. - Subject and filename encoded for non-ASCII (RFC 2047 + RFC 2231).
- Graph permission probed only on the web route.
- UI states plainly that a file will be downloaded.
References
- MDN:
mailto:links - Microsoft Support: Open .eml, .msg, and .oft files in new Outlook
- RFC 2047 (encoded-words), RFC 2231 (parameter value continuations)