f7cloud_client/apps/mail/lib/Service/MailboxShareService.php
root 8b6a0139db f7cloud_client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:59:26 +00:00

318 lines
10 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 F7cloud / F7cloud GmbH and F7cloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Service;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\MailboxShare;
use OCA\Mail\Db\MailboxShareMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IGroupManager;
use OCP\IUserManager;
class MailboxShareService {
public function __construct(
private MailboxShareMapper $mailboxShareMapper,
private MailboxMapper $mailboxMapper,
private AccountService $accountService,
private IMailManager $mailManager,
private IUserManager $userManager,
private IGroupManager $groupManager,
private ITimeFactory $timeFactory,
) {
}
/**
* Create a share for a mailbox. Caller must ensure the mailbox belongs to $ownerUserId.
*
* @throws \InvalidArgumentException if permission or shareType is invalid, or share already exists
*/
public function createShare(
string $ownerUserId,
int $accountId,
int $mailboxId,
string $shareType,
string $shareWith,
string $permission,
): MailboxShare {
if ($shareType !== MailboxShare::TYPE_USER && $shareType !== MailboxShare::TYPE_GROUP) {
throw new \InvalidArgumentException('Invalid share type');
}
if ($permission !== MailboxShare::PERMISSION_READ && $permission !== MailboxShare::PERMISSION_READ_WRITE) {
throw new \InvalidArgumentException('Invalid permission');
}
if ($this->mailboxShareMapper->shareExists($ownerUserId, $accountId, $mailboxId, $shareType, $shareWith)) {
throw new \InvalidArgumentException('Share already exists');
}
$share = new MailboxShare();
$share->setOwnerUserId($ownerUserId);
$share->setAccountId($accountId);
$share->setMailboxId($mailboxId);
$share->setShareType($shareType);
$share->setShareWith($shareWith);
$share->setPermission($permission);
$share->setCreatedAt($this->timeFactory->getTime());
return $this->mailboxShareMapper->insert($share);
}
/**
* Delete a share by id. Caller must ensure the current user is the owner.
*/
public function deleteShare(int $shareId, string $ownerUserId): void {
$share = $this->mailboxShareMapper->find($shareId);
if ($share->getOwnerUserId() !== $ownerUserId) {
throw new \InvalidArgumentException('Not the share owner');
}
$this->mailboxShareMapper->deleteById($shareId);
}
/**
* List shares for a mailbox (owner view: who has access).
*
* @return MailboxShare[]
*/
public function getSharesForMailbox(string $ownerUserId, int $accountId, int $mailboxId): array {
return $this->mailboxShareMapper->findByMailbox($ownerUserId, $accountId, $mailboxId);
}
/**
* Build list "shared with me" for the given user: each item includes owner info, shared mailbox, and subfolders.
* Resolves user + group ids for share_with, then loads owner's mailbox tree and attaches subfolders.
*
* @return array<int, array{shareId: int, ownerUserId: string, ownerDisplayName: string, accountId: int, mailbox: array, permission: string, subMailboxes: array}>
*/
public function getSharedWithMe(string $currentUserId): array {
$user = $this->userManager->get($currentUserId);
if ($user === null) {
return [];
}
$groupIds = $this->groupManager->getUserGroupIds($user);
$shareWithIds = array_merge([$currentUserId], $groupIds);
$shares = $this->mailboxShareMapper->findSharedWith($shareWithIds);
if ($shares === []) {
return [];
}
$result = [];
foreach ($shares as $share) {
try {
$ownerAccount = $this->accountService->find($share->getOwnerUserId(), $share->getAccountId());
} catch (DoesNotExistException $e) {
continue;
}
try {
$mailboxes = $this->mailManager->getMailboxes($ownerAccount);
} catch (\Throwable $e) {
continue;
}
$sharedMailbox = null;
foreach ($mailboxes as $mb) {
if ((int)$mb->getId() === $share->getMailboxId()) {
$sharedMailbox = $mb;
break;
}
}
if ($sharedMailbox === null) {
continue;
}
$delimiter = $sharedMailbox->getDelimiter() ?? '.';
$prefix = $sharedMailbox->getName() . $delimiter;
$subMailboxes = [];
foreach ($mailboxes as $mb) {
if ((int)$mb->getId() === (int)$sharedMailbox->getId()) {
continue;
}
$name = $mb->getName();
if ($name !== $sharedMailbox->getName() && str_starts_with($name, $prefix)) {
$subMailboxes[] = $this->mailboxToJson($mb);
}
}
$ownerDisplayName = $currentUserId;
$ownerUser = $this->userManager->get($share->getOwnerUserId());
if ($ownerUser !== null) {
$ownerDisplayName = $ownerUser->getDisplayName();
}
$result[] = [
'shareId' => $share->getId(),
'ownerUserId' => $share->getOwnerUserId(),
'ownerDisplayName' => $ownerDisplayName,
'accountId' => $share->getAccountId(),
'mailbox' => $this->mailboxToJson($sharedMailbox),
'permission' => $share->getPermission(),
'subMailboxes' => $subMailboxes,
];
}
return $result;
}
/**
* Find a share by id and verify the current user has access (as share_with user or group member).
* Returns the share entity or null.
*/
public function getShareForUser(int $shareId, string $currentUserId): ?MailboxShare {
try {
$share = $this->mailboxShareMapper->find($shareId);
} catch (DoesNotExistException $e) {
return null;
}
if ($share->getShareWith() === $currentUserId) {
return $share;
}
if ($share->getShareType() === MailboxShare::TYPE_GROUP) {
$user = $this->userManager->get($currentUserId);
if ($user !== null && $this->groupManager->isInGroup($currentUserId, $share->getShareWith())) {
return $share;
}
}
return null;
}
/**
* Check if the given mailbox is the shared root or a descendant of a shared mailbox for this user.
* Returns the MailboxShare that grants access, or null.
*/
public function getShareForMailboxAccess(string $currentUserId, int $accountId, int $mailboxId): ?MailboxShare {
$user = $this->userManager->get($currentUserId);
if ($user === null) {
return null;
}
try {
$targetMailbox = $this->mailboxMapper->findById($mailboxId);
} catch (DoesNotExistException $e) {
return null;
}
if ($targetMailbox->getAccountId() !== $accountId) {
return null;
}
$groupIds = $this->groupManager->getUserGroupIds($user);
$shareWithIds = array_merge([$currentUserId], $groupIds);
$shares = $this->mailboxShareMapper->findSharedWith($shareWithIds);
foreach ($shares as $share) {
if ($share->getAccountId() !== $accountId) {
continue;
}
if ($share->getMailboxId() === $mailboxId) {
return $share;
}
try {
$rootMailbox = $this->mailboxMapper->findById($share->getMailboxId());
} catch (DoesNotExistException $e) {
continue;
}
if ($rootMailbox->getAccountId() !== $accountId) {
continue;
}
$delimiter = $rootMailbox->getDelimiter() ?? '.';
$prefix = $rootMailbox->getName() . $delimiter;
$targetName = $targetMailbox->getName();
if ($targetName === $rootMailbox->getName() || str_starts_with($targetName, $prefix)) {
return $share;
}
}
return null;
}
/**
* Resolve mailbox and account for the current user, optionally in shared context.
* Returns ['mailbox' => Mailbox, 'account' => Account, 'share' => MailboxShare|null] or null if no access.
*
* @return array{mailbox: Mailbox, account: Account, share: MailboxShare|null}|null
*/
public function resolveMailboxForAccess(string $currentUserId, int $mailboxId, ?int $shareId = null): ?array {
if ($currentUserId === '') {
return null;
}
if ($shareId !== null) {
$share = $this->getShareForUser($shareId, $currentUserId);
if ($share === null) {
return null;
}
$shareForMailbox = $this->getShareForMailboxAccess($currentUserId, $share->getAccountId(), $mailboxId);
if ($shareForMailbox === null || $shareForMailbox->getId() !== $share->getId()) {
return null;
}
try {
$mailbox = $this->mailManager->getMailbox($share->getOwnerUserId(), $mailboxId);
$account = $this->accountService->find($share->getOwnerUserId(), $share->getAccountId());
} catch (\Throwable $e) {
return null;
}
return ['mailbox' => $mailbox, 'account' => $account, 'share' => $share];
}
try {
$mailbox = $this->mailManager->getMailbox($currentUserId, $mailboxId);
$account = $this->accountService->find($currentUserId, $mailbox->getAccountId());
} catch (\Throwable $e) {
return null;
}
return ['mailbox' => $mailbox, 'account' => $account, 'share' => null];
}
/**
* Resolve message, mailbox and account for the current user, optionally in shared context.
* Returns ['message' => Message, 'mailbox' => Mailbox, 'account' => Account, 'share' => MailboxShare|null] or null if no access.
*
* @return array{message: Message, mailbox: Mailbox, account: Account, share: MailboxShare|null}|null
*/
public function resolveMessageAccess(string $currentUserId, int $messageId, ?int $shareId = null): ?array {
if ($currentUserId === '') {
return null;
}
$ownerUserId = $currentUserId;
$share = null;
if ($shareId !== null) {
$share = $this->getShareForUser($shareId, $currentUserId);
if ($share === null) {
return null;
}
$ownerUserId = $share->getOwnerUserId();
}
try {
$message = $this->mailManager->getMessage($ownerUserId, $messageId);
} catch (DoesNotExistException $e) {
return null;
}
if ($share !== null) {
$shareForMailbox = $this->getShareForMailboxAccess($currentUserId, $share->getAccountId(), $message->getMailboxId());
if ($shareForMailbox === null || $shareForMailbox->getId() !== $share->getId()) {
return null;
}
}
try {
$mailbox = $this->mailManager->getMailbox($ownerUserId, $message->getMailboxId());
$account = $this->accountService->find($ownerUserId, $mailbox->getAccountId());
} catch (\Throwable $e) {
return null;
}
return ['message' => $message, 'mailbox' => $mailbox, 'account' => $account, 'share' => $share];
}
private function mailboxToJson(Mailbox $mailbox): array {
$json = $mailbox->jsonSerialize();
return [
'id' => $json['databaseId'],
'name' => $json['name'],
'displayName' => $json['displayName'],
'delimiter' => $json['delimiter'],
'unread' => $json['unread'],
];
}
}