From 135c9fb64d2bd3615b04a71cc8e9daaaf01f7fea Mon Sep 17 00:00:00 2001 From: Johannes Zellner Date: Tue, 17 Feb 2026 14:06:40 +0100 Subject: [PATCH] Support mailclient oidc claim Only apps with addon email have access to the claims' scopes --- src/mail.js | 30 ++++++++++++++++++++++++++++++ src/oidcserver.js | 21 +++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/mail.js b/src/mail.js index 17ca6e7c0..9ec03db03 100644 --- a/src/mail.js +++ b/src/mail.js @@ -6,6 +6,7 @@ import debugModule from 'debug'; import dig from './dig.js'; import dns from './dns.js'; import eventlog from './eventlog.js'; +import groups from './groups.js'; import mailer from './mailer.js'; import mailServer from './mailserver.js'; import net from 'node:net'; @@ -829,6 +830,34 @@ async function listMailboxes(page, perPage) { return results; } +async function listMailboxesByUserId(userId) { + assert.strictEqual(typeof userId, 'string'); + + const groupIds = await groups._getMembership(userId); + + const baseQuery = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3, m1.storageQuota AS storageQuota, m1.messagesQuota AS messagesQuota ' + + ` FROM (SELECT * FROM mailboxes WHERE type='${TYPE_MAILBOX}') AS m1` + + ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${TYPE_ALIAS}') AS m2` + + ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'; + + let whereClause = " WHERE (m1.ownerType = 'user' AND m1.ownerId = ?)"; + const args = [ userId ]; + if (groupIds.length > 0) { + const placeholders = groupIds.map(() => '?').join(','); + whereClause += ` OR (m1.ownerType = '${OWNERTYPE_GROUP}' AND m1.ownerId IN (${placeholders}))`; + args.push(...groupIds); + } + + const query = baseQuery + whereClause + ' GROUP BY m1.name, m1.domain, m1.ownerId ORDER BY name'; + + const results = await database.query(query, args); + + results.forEach(postProcessMailbox); + results.forEach(postProcessAliases); + + return results; +} + async function delByDomain(domain) { assert.strictEqual(typeof domain, 'string'); @@ -1214,6 +1243,7 @@ export default { sendTestMail, listMailboxesByDomain, listMailboxes, + listMailboxesByUserId, getMailbox, addMailbox, updateMailbox, diff --git a/src/oidcserver.js b/src/oidcserver.js index a095bf9cf..f9d495fba 100644 --- a/src/oidcserver.js +++ b/src/oidcserver.js @@ -13,6 +13,7 @@ import ejs from 'ejs'; import express from 'express'; import eventlog from './eventlog.js'; import fs from 'node:fs'; +import mail from './mail.js'; import * as marked from 'marked'; import middleware from './middleware/index.js'; import oidcClients from './oidcclients.js'; @@ -467,6 +468,15 @@ async function interactionConfirm(req, res, next) { return await gOidcProvider.interactionFinished(req, res, result, { mergeWithLastSubmission: false }); } + + if (!app.manifest.addons.email && params.scope.indexOf('mailclient')) { + const result = { + error: 'access_denied', + error_description: 'App has no access to mailclient claims', + }; + + return await gOidcProvider.interactionFinished(req, res, result, { mergeWithLastSubmission: false }); + } } let grant; @@ -522,6 +532,9 @@ async function getClaims(username/*, use, scope*/) { const [groupsError, allGroups] = await safe(groups.listWithMembers()); if (groupsError) return { error: groupsError.message }; + const [mailboxesError, mailboxes] = await safe(mail.listMailboxesByUserId(user.id)); + if (mailboxesError) return { error: mailboxesError.message }; + const displayName = user.displayName || user.username || ''; // displayName can be empty and username can be null const { firstName, lastName, middleName } = users.parseDisplayName(displayName); @@ -539,7 +552,10 @@ async function getClaims(username/*, use, scope*/) { name: user.displayName, picture: `https://${dashboardFqdn}/api/v1/profile/avatar/${user.id}`, // we always store as png preferred_username: user.username, - groups: allGroups.filter(function (g) { return g.userIds.indexOf(user.id) !== -1; }).map(function (g) { return `${g.name}`; }) + groups: allGroups.filter(function (g) { return g.userIds.indexOf(user.id) !== -1; }).map(function (g) { return `${g.name}`; }), + mailclient: { + mailboxes, + }, }; return claims; @@ -609,7 +625,8 @@ async function start() { claims: { email: ['email', 'email_verified'], profile: [ 'family_name', 'given_name', 'locale', 'name', 'preferred_username', 'picture' ], - groups: [ 'groups' ] + groups: [ 'groups' ], + mailboxes: [ 'mailboxes' ] }, features: { rpInitiatedLogout: { enabled: false },