Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/components/MenuEnvelope.vue
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,20 @@
</template>
{{ t('mail', 'Print message') }}
</ActionButton>
<ActionButton
:close-after-click="false"
@click.prevent="onCopyMessageLink">
<template #icon>
<CheckIcon
v-if="copied"
:size="20" />
<ContentCopyIcon
v-else
:title="t('mail', 'Copy direct link for other user. This only works if they have received the same email.')"
:size="20" />
</template>
{{ copied ? t('mail', 'Link copied') : t('mail', 'Copy direct link for other user. This only works if they have received the same email.') }}
</ActionButton>
<ActionLink
:close-after-click="true"
:href="exportMessageLink">
Expand Down Expand Up @@ -277,6 +291,7 @@ import CalendarClock from 'vue-material-design-icons/CalendarClockOutline.vue'
import CheckIcon from 'vue-material-design-icons/Check.vue'
import TaskIcon from 'vue-material-design-icons/CheckboxMarkedCirclePlusOutline.vue'
import ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'
import ContentCopyIcon from 'vue-material-design-icons/ContentCopy.vue'
import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue'
import FilterIcon from 'vue-material-design-icons/FilterOutline.vue'
import InformationIcon from 'vue-material-design-icons/InformationOutline.vue'
Expand Down Expand Up @@ -321,6 +336,7 @@ export default {
AlarmIcon,
PrinterIcon,
FilterIcon,
ContentCopyIcon,
},

props: {
Expand Down Expand Up @@ -367,6 +383,7 @@ export default {
snoozeActionsOpen: false,
forwardMessages: this.envelope.databaseId,
customSnoozeDateTime: new Date(moment().add(2, 'hours').minute(0).second(0).valueOf()),
copied: false,
}
},

Expand Down Expand Up @@ -634,6 +651,26 @@ export default {
this.$emit('print')
},

async onCopyMessageLink() {
const trimmedMessageId = this.envelope.messageId.trim().replace(/^<|>$/g, '')
const url = window.location.origin + generateUrl('/apps/mail/open/' + encodeURIComponent(trimmedMessageId))
Comment on lines +655 to +656

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard messageId before calling trim()

Line 655 can throw when this.envelope.messageId is missing/null, which breaks the action instead of falling back gracefully.

Proposed fix
 async onCopyMessageLink() {
-	const trimmedMessageId = this.envelope.messageId.trim().replace(/^<|>$/g, '')
+	const rawMessageId = this.envelope?.messageId
+	if (typeof rawMessageId !== 'string' || rawMessageId.trim() === '') {
+		showError(t('mail', 'Could not generate direct link for this message'))
+		return
+	}
+	const trimmedMessageId = rawMessageId.trim().replace(/^<|>$/g, '')
 	const url = window.location.origin + generateUrl('/apps/mail/open/' + encodeURIComponent(trimmedMessageId))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const trimmedMessageId = this.envelope.messageId.trim().replace(/^<|>$/g, '')
const url = window.location.origin + generateUrl('/apps/mail/open/' + encodeURIComponent(trimmedMessageId))
async onCopyMessageLink() {
const rawMessageId = this.envelope?.messageId
if (typeof rawMessageId !== 'string' || rawMessageId.trim() === '') {
showError(t('mail', 'Could not generate direct link for this message'))
return
}
const trimmedMessageId = rawMessageId.trim().replace(/^<|>$/g, '')
const url = window.location.origin + generateUrl('/apps/mail/open/' + encodeURIComponent(trimmedMessageId))


try {
await navigator.clipboard.writeText(url)
this.copied = true
showSuccess(t('mail', 'Direct link copied to clipboard'))
} catch (error) {
// Fallback for cases where clipboard API is not available (e.g. non-HTTPS localhost)
// or permission is denied. This exactly matches Nextcloud's useCopy composable behavior.
window.prompt(t('mail', 'Copy direct link for other user. This only works if they have received the same email.'), url)
} finally {
setTimeout(() => {
this.copied = false
this.localMoreActionsOpen = false
}, 2000)
}
},

isSieveEnabled() {
return this.account.sieveEnabled
},
Expand Down