feat(portal): one-click Apple Mail setup via .mobileconfig

Apple Mail ignores RFC 6186 SRV autodiscovery and 'Microsoft Exchange'
needs EWS/EAS that Stalwart doesn't speak — so custom-domain users were
stuck typing IMAP/SMTP servers manually. New session-gated portal route
generates an Apple configuration profile (IMAP 993 + SMTP 465 on the
runtime mail host, username = address, NO password embedded — profiles
are plaintext, Apple prompts at install). 'Add to Apple Mail' buttons on
the three credential screens (invite result, mailbox created, password
reset). CalDAV/CardDAV payloads join when DAV is reachable from outside
(the node's :443 belongs to Traefik for now).
This commit is contained in:
Ronni Baslund
2026-06-11 07:44:49 +02:00
parent 38fb0f586e
commit acf0d082e4
2 changed files with 124 additions and 0 deletions
+26
View File
@@ -1,5 +1,12 @@
<script setup lang="ts">
const mailHost = new URL(useRuntimeConfig().public.mailUrl as string).host
// One-click Apple Mail setup: downloads a .mobileconfig with the IMAP/SMTP
// settings prefilled (no password inside — Apple prompts on install).
function downloadAppleProfile(email: string, name?: string) {
const params = new URLSearchParams({ email, ...(name ? { name } : {}) })
window.location.href = `/api/apple-mailconfig?${params.toString()}`
}
// Users & groups. The Users tab is real — workspace members come from
// /api/tenants/:slug/users (platform-api UserDocument). The Groups,
// Invitations and Service-accounts tabs have no backend yet (no Group /
@@ -914,6 +921,12 @@ async function submitCreateMailbox() {
<button class="copy" @click="copyText(resetResult.tempPassword)"><UiIcon name="copy" :size="13" /></button>
</div>
</div>
<div class="apple-row">
<UiButton variant="secondary" @click="downloadAppleProfile(resetResult.email, undefined)">
<template #leading><UiIcon name="download" :size="13" /></template>
Add to Apple Mail (.mobileconfig)
</UiButton>
</div>
</div>
<template #footer>
<div style="flex: 1" />
@@ -1023,6 +1036,12 @@ async function submitCreateMailbox() {
<button class="copy" @click="copyText(mailboxResult.tempPassword)"><UiIcon name="copy" :size="13" /></button>
</div>
</div>
<div class="apple-row">
<UiButton variant="secondary" @click="downloadAppleProfile(mailboxResult.email, undefined)">
<template #leading><UiIcon name="download" :size="13" /></template>
Add to Apple Mail (.mobileconfig)
</UiButton>
</div>
</div>
<template #footer>
<div style="flex: 1" />
@@ -1057,6 +1076,12 @@ async function submitCreateMailbox() {
<button class="copy" @click="copyText(inviteResult.tempPassword)"><UiIcon name="copy" :size="13" /></button>
</div>
</div>
<div class="apple-row">
<UiButton variant="secondary" @click="downloadAppleProfile(inviteResult.email, undefined)">
<template #leading><UiIcon name="download" :size="13" /></template>
Add to Apple Mail (.mobileconfig)
</UiButton>
</div>
<div class="prov">
<Badge :tone="provTone(inviteResult.provisioning.authentik)" dot>SSO login</Badge>
<Badge :tone="provTone(inviteResult.provisioning.stalwart)" dot>Mailbox</Badge>
@@ -1401,4 +1426,5 @@ Mikkel Sørensen,mikkel@baslund.dk,admin,Engineering,business</pre>
}
.role-row.active { border-color: var(--text); background: var(--bg); }
.role-name { font-size: 13px; font-weight: 500; }
.apple-row { margin-top: 10px; }
</style>