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',
}