feat(mail): tenant alias and distribution-list management via Stalwart

Customer-admin Mail settings backed by Stalwart JMAP: per-tenant aliases
(extra addresses routing to a mailbox) and distribution lists (one address
fanning out to many recipients). Adds StalwartClient x:Alias/x:MailingList
methods, a tenant-scoped MailController/MailService, the portal Mail settings
page and its proxy routes, and the mailboxAddress field on TenantUserDoc.
Removes the old mock mail data now that the page reads live data.
This commit is contained in:
Ronni Baslund
2026-06-07 00:16:30 +02:00
parent 04191193c2
commit aee8f13899
18 changed files with 1035 additions and 461 deletions
@@ -0,0 +1,13 @@
// List the tenant's email aliases. Proxies GET /tenants/:slug/mail/aliases.
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
export default defineEventHandler(async (event) => {
const session = await getUserSession(event).catch(() => null)
const accessToken = (session as { accessToken?: string } | null)?.accessToken
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
const slug = getRouterParam(event, 'slug')
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
return $fetch(`${base}/tenants/${slug}/mail/aliases`, {
headers: { Authorization: `Bearer ${accessToken}` },
})
})
@@ -0,0 +1,16 @@
// Create an email alias. Proxies POST /tenants/:slug/mail/aliases.
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
export default defineEventHandler(async (event) => {
const session = await getUserSession(event).catch(() => null)
const accessToken = (session as { accessToken?: string } | null)?.accessToken
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
const slug = getRouterParam(event, 'slug')
const body = await readBody(event)
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
return $fetch(`${base}/tenants/${slug}/mail/aliases`, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
body,
})
})
@@ -0,0 +1,16 @@
// Delete an email alias. Proxies DELETE /tenants/:slug/mail/aliases/:address.
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
export default defineEventHandler(async (event) => {
const session = await getUserSession(event).catch(() => null)
const accessToken = (session as { accessToken?: string } | null)?.accessToken
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
const slug = getRouterParam(event, 'slug')
const address = getRouterParam(event, 'address')
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
await $fetch(`${base}/tenants/${slug}/mail/aliases/${encodeURIComponent(address ?? '')}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${accessToken}` },
})
return { ok: true }
})
@@ -0,0 +1,13 @@
// List the tenant's distribution lists. Proxies GET /tenants/:slug/mail/lists.
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
export default defineEventHandler(async (event) => {
const session = await getUserSession(event).catch(() => null)
const accessToken = (session as { accessToken?: string } | null)?.accessToken
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
const slug = getRouterParam(event, 'slug')
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
return $fetch(`${base}/tenants/${slug}/mail/lists`, {
headers: { Authorization: `Bearer ${accessToken}` },
})
})
@@ -0,0 +1,16 @@
// Create a distribution list. Proxies POST /tenants/:slug/mail/lists.
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
export default defineEventHandler(async (event) => {
const session = await getUserSession(event).catch(() => null)
const accessToken = (session as { accessToken?: string } | null)?.accessToken
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
const slug = getRouterParam(event, 'slug')
const body = await readBody(event)
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
return $fetch(`${base}/tenants/${slug}/mail/lists`, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
body,
})
})
@@ -0,0 +1,16 @@
// Delete a distribution list. Proxies DELETE /tenants/:slug/mail/lists/:id.
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
export default defineEventHandler(async (event) => {
const session = await getUserSession(event).catch(() => null)
const accessToken = (session as { accessToken?: string } | null)?.accessToken
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
const slug = getRouterParam(event, 'slug')
const id = getRouterParam(event, 'id')
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
await $fetch(`${base}/tenants/${slug}/mail/lists/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${accessToken}` },
})
return { ok: true }
})
@@ -0,0 +1,17 @@
// Update a distribution list's recipients. Proxies PATCH /tenants/:slug/mail/lists/:id.
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
export default defineEventHandler(async (event) => {
const session = await getUserSession(event).catch(() => null)
const accessToken = (session as { accessToken?: string } | null)?.accessToken
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
const slug = getRouterParam(event, 'slug')
const id = getRouterParam(event, 'id')
const body = await readBody(event)
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
return $fetch(`${base}/tenants/${slug}/mail/lists/${id}`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${accessToken}` },
body,
})
})