feat(core): add CLI management for tenant system stores

Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
2026-08-28 21:23:50 -04:00
parent a5eb104bd7
commit 33c44d62db
7 changed files with 44 additions and 14 deletions
+4 -4
View File
@@ -80,8 +80,8 @@ final class CompositionController extends ControllerAbstract {
if (!isset($data['action']) || !is_string($data['action'])) { if (!isset($data['action']) || !is_string($data['action'])) {
throw new InvalidArgumentException('Invalid parameter: action must be a string'); throw new InvalidArgumentException('Invalid parameter: action must be a string');
} }
if (!in_array($data['action'], ['fresh', 'reply', 'forward'], true)) { if (!in_array($data['action'], ['fresh', 'reply', 'replyAll', 'forward'], true)) {
throw new InvalidArgumentException('Invalid parameter: action must be one of fresh, reply, forward'); throw new InvalidArgumentException('Invalid parameter: action must be one of fresh, reply, replyAll, forward');
} }
if (!isset($data['sender']) || !is_array($data['sender'])) { if (!isset($data['sender']) || !is_array($data['sender'])) {
throw new InvalidArgumentException('Invalid parameter: sender must be an array'); 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'])) { if (!isset($data['message']) || !is_array($data['message'])) {
throw new InvalidArgumentException('Invalid parameter: message must be an array'); 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'] === '')) { 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 or forward action'); 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); return $this->compositionManager->stage($tenantId, $userId, $data['identifier'], $data['action'], $data['sender'], $data['message'], $data['source'] ?? null);
+8
View File
@@ -19,6 +19,7 @@ const props = defineProps<{
// Emits // Emits
const emit = defineEmits<{ const emit = defineEmits<{
reply: [entity: EntityObject] reply: [entity: EntityObject]
replyAll: [entity: EntityObject]
forward: [entity: EntityObject] forward: [entity: EntityObject]
move: [entity: EntityObject] move: [entity: EntityObject]
delete: [entity: EntityObject] delete: [entity: EntityObject]
@@ -107,6 +108,12 @@ const handleReply = () => {
} }
} }
const handleReplyAll = () => {
if (props.entity) {
emit('replyAll', props.entity)
}
}
const handleForward = () => { const handleForward = () => {
if (props.entity) { if (props.entity) {
emit('forward', props.entity) emit('forward', props.entity)
@@ -176,6 +183,7 @@ const handleViewerDownload = (item: MediaViewerItem) => {
:security-level="effectiveSecurityLevel" :security-level="effectiveSecurityLevel"
:is-security-overridden="overrideSecurityLevel !== null" :is-security-overridden="overrideSecurityLevel !== null"
@reply="handleReply" @reply="handleReply"
@reply-all="handleReplyAll"
@forward="handleForward" @forward="handleForward"
@move="handleMove" @move="handleMove"
@delete="handleDelete" @delete="handleDelete"
+1 -1
View File
@@ -30,7 +30,7 @@ defineEmits<{
</v-btn> </v-btn>
<v-toolbar-title> <v-toolbar-title>
{{ 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' }}
</v-toolbar-title> </v-toolbar-title>
<v-spacer /> <v-spacer />
+2 -1
View File
@@ -13,6 +13,7 @@ const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
reply: [] reply: []
replyAll: []
forward: [] forward: []
move: [] move: []
delete: [] delete: []
@@ -47,7 +48,7 @@ const currentSecurityLevel = computed(() => {
<v-btn <v-btn
icon="mdi-reply-all" icon="mdi-reply-all"
variant="text" variant="text"
@click="emit('reply')" @click="emit('replyAll')"
> >
<v-icon>mdi-reply-all</v-icon> <v-icon>mdi-reply-all</v-icon>
<v-tooltip activator="parent" location="bottom">Reply All</v-tooltip> <v-tooltip activator="parent" location="bottom">Reply All</v-tooltip>
+3
View File
@@ -167,6 +167,8 @@ const handleMessageComposeFresh = () => mailUiStore.openComposer()
const handleMessageComposeReply = (message: EntityObject) => mailUiStore.openComposer(message, ComposerMode.Reply) 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 handleMessageComposeForward = (message: EntityObject) => mailUiStore.openComposer(message, ComposerMode.Forward)
const handleMessageComposeClose = () => mailUiStore.closeComposer() const handleMessageComposeClose = () => mailUiStore.closeComposer()
@@ -339,6 +341,7 @@ const handleMessageSelectionDelete = () => mailUiStore.deleteSelectedMessages()
:entity="selectedMessage" :entity="selectedMessage"
@compose="handleMessageComposeFresh" @compose="handleMessageComposeFresh"
@reply="handleMessageComposeReply" @reply="handleMessageComposeReply"
@reply-all="handleMessageComposeReplyAll"
@forward="handleMessageComposeForward" @forward="handleMessageComposeForward"
@move="handleMessageMove" @move="handleMessageMove"
@delete="handleMessageDelete" @delete="handleMessageDelete"
+25 -8
View File
@@ -118,7 +118,8 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
return 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 activeDraft.value.action = mode
stageDraft() 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 composition = createIdentifier()
const freshMessage = buildMessage(mode, source) const freshMessage = buildMessage(mode, source, ownAddresses)
const freshAttachments = buildAttachments(composition, mode, source) const freshAttachments = buildAttachments(composition, mode, source)
const sourceIdentifier = source instanceof EntityObject ? source.identifier : null const sourceIdentifier = source instanceof EntityObject ? source.identifier : null
return { 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) { if (!source) {
return emptyMessage() return emptyMessage()
@@ -603,12 +604,28 @@ function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressO
const sentAt = sourceMessage.sent || source.created || '' const sentAt = sourceMessage.sent || source.created || ''
const sentLabel = sentAt ? new Date(sentAt).toLocaleString() : 'an unknown time' 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 replyToAddr = sourceMessage.replyTo?.[0] ?? sourceMessage.from ?? null
const replyToAddress = replyToAddr?.address || '' 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<string>()
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 { return {
to: replyToAddress ? [{ address: replyToAddress, label: replyToAddr?.label }] : [], to,
cc: [], cc,
bcc: [], bcc: [],
subject: /^Re:/i.test(originalSubject) ? originalSubject : `Re: ${originalSubject}`, subject: /^Re:/i.test(originalSubject) ? originalSubject : `Re: ${originalSubject}`,
body: { body: {
@@ -631,7 +648,7 @@ function buildMessage(mode: ComposerMode, source: EntityObject | MessageAddressO
} }
function buildAttachments(composition: string, mode: ComposerMode, source?: EntityObject | MessageAddressObject | null): Record<string, ComposerDraftAttachment> { function buildAttachments(composition: string, mode: ComposerMode, source?: EntityObject | MessageAddressObject | null): Record<string, ComposerDraftAttachment> {
if (mode === ComposerMode.Fresh || mode === ComposerMode.Reply || !(source instanceof EntityObject)) { if (mode === ComposerMode.Fresh || mode === ComposerMode.Reply || mode === ComposerMode.ReplyAll || !(source instanceof EntityObject)) {
return {} return {}
} }
+1
View File
@@ -5,6 +5,7 @@ import type { MessageAddressInterface } from "@MailManager/types/message"
export enum ComposerMode { export enum ComposerMode {
Fresh = 'fresh', Fresh = 'fresh',
Reply = 'reply', Reply = 'reply',
ReplyAll = 'replyAll',
Forward = 'forward', Forward = 'forward',
} }