From 33c44d62db9d1d947474ad7b76cb83e4f5cccd81 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 28 Aug 2026 21:23:50 -0400 Subject: [PATCH] feat(core): add CLI management for tenant system stores Signed-off-by: Sebastian --- lib/Controllers/CompositionController.php | 8 ++--- src/components/MessageReader.vue | 8 +++++ src/components/composer/ComposerToolbar.vue | 2 +- src/components/reader/ReaderToolbar.vue | 3 +- src/pages/Main.vue | 3 ++ src/stores/mailCompositionStore.ts | 33 ++++++++++++++++----- src/types/composer.ts | 1 + 7 files changed, 44 insertions(+), 14 deletions(-) diff --git a/lib/Controllers/CompositionController.php b/lib/Controllers/CompositionController.php index 073750a..267c928 100644 --- a/lib/Controllers/CompositionController.php +++ b/lib/Controllers/CompositionController.php @@ -80,8 +80,8 @@ final class CompositionController extends ControllerAbstract { if (!isset($data['action']) || !is_string($data['action'])) { throw new InvalidArgumentException('Invalid parameter: action must be a string'); } - if (!in_array($data['action'], ['fresh', 'reply', 'forward'], true)) { - throw new InvalidArgumentException('Invalid parameter: action must be one of fresh, reply, forward'); + if (!in_array($data['action'], ['fresh', 'reply', 'replyAll', 'forward'], true)) { + throw new InvalidArgumentException('Invalid parameter: action must be one of fresh, reply, replyAll, forward'); } if (!isset($data['sender']) || !is_array($data['sender'])) { throw new InvalidArgumentException('Invalid parameter: sender must be an array'); @@ -89,8 +89,8 @@ final class CompositionController extends ControllerAbstract { if (!isset($data['message']) || !is_array($data['message'])) { throw new InvalidArgumentException('Invalid parameter: message must be an array'); } - if (($data['action'] === 'reply' || $data['action'] === 'forward') && (!isset($data['source']) || !is_string($data['source']) || $data['source'] === '')) { - throw new InvalidArgumentException('Invalid parameter: source must be a non-empty string for reply or forward action'); + if (($data['action'] === 'reply' || $data['action'] === 'replyAll' || $data['action'] === 'forward') && (!isset($data['source']) || !is_string($data['source']) || $data['source'] === '')) { + throw new InvalidArgumentException('Invalid parameter: source must be a non-empty string for reply, replyAll, or forward action'); } return $this->compositionManager->stage($tenantId, $userId, $data['identifier'], $data['action'], $data['sender'], $data['message'], $data['source'] ?? null); diff --git a/src/components/MessageReader.vue b/src/components/MessageReader.vue index 0467ffc..cb44019 100644 --- a/src/components/MessageReader.vue +++ b/src/components/MessageReader.vue @@ -19,6 +19,7 @@ const props = defineProps<{ // Emits const emit = defineEmits<{ reply: [entity: EntityObject] + replyAll: [entity: EntityObject] forward: [entity: EntityObject] move: [entity: EntityObject] delete: [entity: EntityObject] @@ -107,6 +108,12 @@ const handleReply = () => { } } +const handleReplyAll = () => { + if (props.entity) { + emit('replyAll', props.entity) + } +} + const handleForward = () => { if (props.entity) { emit('forward', props.entity) @@ -176,6 +183,7 @@ const handleViewerDownload = (item: MediaViewerItem) => { :security-level="effectiveSecurityLevel" :is-security-overridden="overrideSecurityLevel !== null" @reply="handleReply" + @reply-all="handleReplyAll" @forward="handleForward" @move="handleMove" @delete="handleDelete" diff --git a/src/components/composer/ComposerToolbar.vue b/src/components/composer/ComposerToolbar.vue index 1279639..6732080 100644 --- a/src/components/composer/ComposerToolbar.vue +++ b/src/components/composer/ComposerToolbar.vue @@ -30,7 +30,7 @@ defineEmits<{ - {{ mode === ComposerMode.Reply ? 'Reply' : mode === ComposerMode.Forward ? 'Forward' : 'New Message' }} + {{ mode === ComposerMode.Reply ? 'Reply' : mode === ComposerMode.ReplyAll ? 'Reply All' : mode === ComposerMode.Forward ? 'Forward' : 'New Message' }} diff --git a/src/components/reader/ReaderToolbar.vue b/src/components/reader/ReaderToolbar.vue index 73ad720..39fa816 100644 --- a/src/components/reader/ReaderToolbar.vue +++ b/src/components/reader/ReaderToolbar.vue @@ -13,6 +13,7 @@ const props = defineProps() const emit = defineEmits<{ reply: [] + replyAll: [] forward: [] move: [] delete: [] @@ -47,7 +48,7 @@ const currentSecurityLevel = computed(() => { mdi-reply-all Reply All diff --git a/src/pages/Main.vue b/src/pages/Main.vue index 454cb5d..b7cbb8f 100644 --- a/src/pages/Main.vue +++ b/src/pages/Main.vue @@ -167,6 +167,8 @@ const handleMessageComposeFresh = () => mailUiStore.openComposer() const handleMessageComposeReply = (message: EntityObject) => mailUiStore.openComposer(message, ComposerMode.Reply) +const handleMessageComposeReplyAll = (message: EntityObject) => mailUiStore.openComposer(message, ComposerMode.ReplyAll) + const handleMessageComposeForward = (message: EntityObject) => mailUiStore.openComposer(message, ComposerMode.Forward) const handleMessageComposeClose = () => mailUiStore.closeComposer() @@ -339,6 +341,7 @@ const handleMessageSelectionDelete = () => mailUiStore.deleteSelectedMessages() :entity="selectedMessage" @compose="handleMessageComposeFresh" @reply="handleMessageComposeReply" + @reply-all="handleMessageComposeReplyAll" @forward="handleMessageComposeForward" @move="handleMessageMove" @delete="handleMessageDelete" diff --git a/src/stores/mailCompositionStore.ts b/src/stores/mailCompositionStore.ts index 718a34a..e09ed3e 100644 --- a/src/stores/mailCompositionStore.ts +++ b/src/stores/mailCompositionStore.ts @@ -118,7 +118,8 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () => return } - activeDraft.value = buildDraft(mode, sender, source) + const ownAddresses = senderIdentities.value.map(identity => identity.address) + activeDraft.value = buildDraft(mode, sender, source, ownAddresses) activeDraft.value.action = mode stageDraft() @@ -558,9 +559,9 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () => } }) -function buildDraft(mode: ComposerMode, sender: ComposerSenderIdentity, source?: EntityObject | MessageAddressObject | null): ComposerDraft { +function buildDraft(mode: ComposerMode, sender: ComposerSenderIdentity, source: EntityObject | MessageAddressObject | null | undefined, ownAddresses: string[]): ComposerDraft { const composition = createIdentifier() - const freshMessage = buildMessage(mode, source) + const freshMessage = buildMessage(mode, source, ownAddresses) const freshAttachments = buildAttachments(composition, mode, source) const sourceIdentifier = source instanceof EntityObject ? source.identifier : null return { @@ -576,7 +577,7 @@ function buildDraft(mode: ComposerMode, sender: ComposerSenderIdentity, source?: } } -function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressObject | null | undefined): ComposerDraftMessage { +function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressObject | null | undefined, ownAddresses: string[] = []): ComposerDraftMessage { if (!source) { return emptyMessage() @@ -603,12 +604,28 @@ function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressO const sentAt = sourceMessage.sent || source.created || '' const sentLabel = sentAt ? new Date(sentAt).toLocaleString() : 'an unknown time' - if (mode === ComposerMode.Reply) { + if (mode === ComposerMode.Reply || mode === ComposerMode.ReplyAll) { const replyToAddr = sourceMessage.replyTo?.[0] ?? sourceMessage.from ?? null const replyToAddress = replyToAddr?.address || '' + const to: MessageAddressInterface[] = replyToAddress ? [{ address: replyToAddress, label: replyToAddr?.label }] : [] + + let cc: MessageAddressInterface[] = [] + if (mode === ComposerMode.ReplyAll) { + const excluded = new Set([replyToAddress.toLowerCase(), ...ownAddresses.map(addr => addr.toLowerCase())]) + const seen = new Set() + cc = [...(sourceMessage.to || []), ...(sourceMessage.cc || [])].filter(recipient => { + const address = recipient.address?.toLowerCase() || '' + if (!address || excluded.has(address) || seen.has(address)) { + return false + } + seen.add(address) + return true + }) + } + return { - to: replyToAddress ? [{ address: replyToAddress, label: replyToAddr?.label }] : [], - cc: [], + to, + cc, bcc: [], subject: /^Re:/i.test(originalSubject) ? originalSubject : `Re: ${originalSubject}`, body: { @@ -631,7 +648,7 @@ function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressO } function buildAttachments(composition: string, mode: ComposerMode, source?: EntityObject | MessageAddressObject | null): Record { - if (mode === ComposerMode.Fresh || mode === ComposerMode.Reply || !(source instanceof EntityObject)) { + if (mode === ComposerMode.Fresh || mode === ComposerMode.Reply || mode === ComposerMode.ReplyAll || !(source instanceof EntityObject)) { return {} } diff --git a/src/types/composer.ts b/src/types/composer.ts index 2e8953f..8df36e4 100644 --- a/src/types/composer.ts +++ b/src/types/composer.ts @@ -5,6 +5,7 @@ import type { MessageAddressInterface } from "@MailManager/types/message" export enum ComposerMode { Fresh = 'fresh', Reply = 'reply', + ReplyAll = 'replyAll', Forward = 'forward', }