feat: message and attachment download
Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue'
|
|||||||
import { useUser } from '@KTXC'
|
import { useUser } from '@KTXC'
|
||||||
import type { EntityObject, MessageObject } from '@MailManager/models'
|
import type { EntityObject, MessageObject } from '@MailManager/models'
|
||||||
import { SecurityLevel } from '@/utile/emailSanitizer'
|
import { SecurityLevel } from '@/utile/emailSanitizer'
|
||||||
|
import { useMailStore } from '@/stores/mailStore'
|
||||||
import ReaderEmpty from './reader/ReaderEmpty.vue'
|
import ReaderEmpty from './reader/ReaderEmpty.vue'
|
||||||
import ReaderToolbar from './reader/ReaderToolbar.vue'
|
import ReaderToolbar from './reader/ReaderToolbar.vue'
|
||||||
import ReaderHeader from './reader/ReaderHeader.vue'
|
import ReaderHeader from './reader/ReaderHeader.vue'
|
||||||
@@ -25,6 +26,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
// User settings
|
// User settings
|
||||||
const { getSetting } = useUser()
|
const { getSetting } = useUser()
|
||||||
|
const mailStore = useMailStore()
|
||||||
|
|
||||||
// Per-message overrides
|
// Per-message overrides
|
||||||
const allowImages = ref(false)
|
const allowImages = ref(false)
|
||||||
@@ -94,6 +96,14 @@ const handleMove = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDownload = async (partIndex?: number) => {
|
||||||
|
if (!props.entity) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mailStore.downloadMessage(props.entity, partIndex)
|
||||||
|
}
|
||||||
|
|
||||||
const handleFlag = () => {
|
const handleFlag = () => {
|
||||||
if (props.entity) {
|
if (props.entity) {
|
||||||
emit('flag', props.entity)
|
emit('flag', props.entity)
|
||||||
@@ -125,13 +135,17 @@ const handleCompose = () => {
|
|||||||
@move="handleMove"
|
@move="handleMove"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
@flag="handleFlag"
|
@flag="handleFlag"
|
||||||
|
@download="handleDownload()"
|
||||||
@toggle-images="toggleImages"
|
@toggle-images="toggleImages"
|
||||||
@set-security-level="setSecurityLevel"
|
@set-security-level="setSecurityLevel"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Message content -->
|
<!-- Message content -->
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
<ReaderHeader :message="message!" />
|
<ReaderHeader
|
||||||
|
:entity="props.entity"
|
||||||
|
@download-attachment="handleDownload"
|
||||||
|
/>
|
||||||
|
|
||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MessageObject } from '@MailManager/models/message'
|
import { computed } from 'vue'
|
||||||
import RecipientDetails from '@/components/common/RecipientDetails.vue'
|
import RecipientDetails from '@/components/common/RecipientDetails.vue'
|
||||||
|
import type { EntityObject } from '@MailManager/models';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: MessageObject
|
entity: EntityObject | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const addressKey = (address: { address?: string | null; label?: string | null } | null | undefined, index: number): string => {
|
const emit = defineEmits<{
|
||||||
return `${address?.address || address?.label || 'address'}-${index}`
|
downloadAttachment: [index: number]
|
||||||
}
|
}>()
|
||||||
|
|
||||||
|
const message = computed(() => {
|
||||||
|
return props.entity?.properties ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const randomKey = computed(() => {
|
||||||
|
return Math.random().toString(36).substring(2, 15)
|
||||||
|
})
|
||||||
|
|
||||||
// Format date for display
|
// Format date for display
|
||||||
const formatDate = (date: Date | string | null | undefined): string => {
|
const formatDate = (date: Date | string | null | undefined): string => {
|
||||||
@@ -36,6 +45,10 @@ const formatFileSize = (bytes: number | undefined): string => {
|
|||||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const download = async (index: number): Promise<void> => {
|
||||||
|
emit('downloadAttachment', index)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -66,7 +79,7 @@ const formatFileSize = (bytes: number | undefined): string => {
|
|||||||
<!-- Recipients -->
|
<!-- Recipients -->
|
||||||
<div v-if="message?.to && message?.to.length > 0" class="text-body-2 mb-1">
|
<div v-if="message?.to && message?.to.length > 0" class="text-body-2 mb-1">
|
||||||
<span class="text-medium-emphasis">To:</span>
|
<span class="text-medium-emphasis">To:</span>
|
||||||
<template v-for="(recipient, index) in message.to" :key="addressKey(recipient, index)">
|
<template v-for="(recipient, index) in message.to" :key="randomKey">
|
||||||
<RecipientDetails :address="recipient">
|
<RecipientDetails :address="recipient">
|
||||||
<template #default="{ label }">
|
<template #default="{ label }">
|
||||||
<span class="contact-link">{{ label }}</span>
|
<span class="contact-link">{{ label }}</span>
|
||||||
@@ -78,7 +91,7 @@ const formatFileSize = (bytes: number | undefined): string => {
|
|||||||
|
|
||||||
<div v-if="message?.cc && message?.cc.length > 0" class="text-body-2 mb-1">
|
<div v-if="message?.cc && message?.cc.length > 0" class="text-body-2 mb-1">
|
||||||
<span class="text-medium-emphasis">Cc:</span>
|
<span class="text-medium-emphasis">Cc:</span>
|
||||||
<template v-for="(recipient, index) in message.cc" :key="addressKey(recipient, index)">
|
<template v-for="(recipient, index) in message.cc" :key="randomKey">
|
||||||
<RecipientDetails :address="recipient">
|
<RecipientDetails :address="recipient">
|
||||||
<template #default="{ label }">
|
<template #default="{ label }">
|
||||||
<span class="contact-link">{{ label }}</span>
|
<span class="contact-link">{{ label }}</span>
|
||||||
@@ -94,19 +107,24 @@ const formatFileSize = (bytes: number | undefined): string => {
|
|||||||
Attachments ({{ message?.attachments.length }})
|
Attachments ({{ message?.attachments.length }})
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<v-chip
|
<div
|
||||||
v-for="(attachment, index) in message?.attachments"
|
v-for="(attachment, index) in message?.attachments"
|
||||||
:key="index"
|
:key="randomKey"
|
||||||
prepend-icon="mdi-paperclip"
|
class="attachment-item"
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
class="attachment-chip"
|
|
||||||
>
|
>
|
||||||
<span class="attachment-name">{{ attachment.name || 'Untitled' }}</span>
|
<v-chip
|
||||||
<span v-if="attachment.size" class="text-caption text-medium-emphasis ml-1">
|
prepend-icon="mdi-paperclip"
|
||||||
({{ formatFileSize(attachment.size) }})
|
size="small"
|
||||||
</span>
|
variant="outlined"
|
||||||
</v-chip>
|
class="attachment-chip"
|
||||||
|
@click="download(index)"
|
||||||
|
>
|
||||||
|
<span class="attachment-name">{{ attachment.name || 'Untitled' }}</span>
|
||||||
|
<span v-if="attachment.size" class="text-caption text-medium-emphasis ml-1">
|
||||||
|
({{ formatFileSize(attachment.size ?? undefined) }})
|
||||||
|
</span>
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,8 +139,15 @@ const formatFileSize = (bytes: number | undefined): string => {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.attachment-chip {
|
.attachment-chip {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
.attachment-name {
|
.attachment-name {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -131,6 +156,11 @@ const formatFileSize = (bytes: number | undefined): string => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachment-error {
|
||||||
|
color: rgb(var(--v-theme-error));
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.contact-link {
|
.contact-link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const emit = defineEmits<{
|
|||||||
move: []
|
move: []
|
||||||
delete: []
|
delete: []
|
||||||
flag: []
|
flag: []
|
||||||
|
download: []
|
||||||
toggleImages: []
|
toggleImages: []
|
||||||
setSecurityLevel: [level: SecurityLevel]
|
setSecurityLevel: [level: SecurityLevel]
|
||||||
}>()
|
}>()
|
||||||
@@ -161,6 +162,28 @@ const currentSecurityLevel = computed(() => {
|
|||||||
<v-icon>mdi-delete-outline</v-icon>
|
<v-icon>mdi-delete-outline</v-icon>
|
||||||
<v-tooltip activator="parent" location="bottom">Delete</v-tooltip>
|
<v-tooltip activator="parent" location="bottom">Delete</v-tooltip>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
|
<v-menu>
|
||||||
|
<template #activator="{ props: menuProps }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="menuProps"
|
||||||
|
icon="mdi-dots-vertical"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-dots-vertical</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="bottom">More Actions</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-download"
|
||||||
|
@click="emit('download')"
|
||||||
|
>
|
||||||
|
<v-list-item-title>Download</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -633,6 +633,33 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadMessage(entity: EntityObject, index?: number) {
|
||||||
|
const target = entity.identifier
|
||||||
|
let part = null
|
||||||
|
|
||||||
|
if (index !== undefined) {
|
||||||
|
part = entity.properties.attachments?.[Number(index)] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await entitiesStore.download(target, part)
|
||||||
|
} catch (error) {
|
||||||
|
const messageText = error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: index === undefined
|
||||||
|
? 'Failed to download message'
|
||||||
|
: 'Failed to download attachment'
|
||||||
|
console.error(
|
||||||
|
index === undefined
|
||||||
|
? '[Mail][Operations] Failed to download message:'
|
||||||
|
: '[Mail][Operations] Failed to download attachment:',
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
notify(messageText, 'error')
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function notify(message: string, color: 'success' | 'error' | 'info' | 'warning' = 'success') {
|
function notify(message: string, color: 'success' | 'error' | 'info' | 'warning' = 'success') {
|
||||||
showSnackbar({ message, color })
|
showSnackbar({ message, color })
|
||||||
}
|
}
|
||||||
@@ -672,6 +699,7 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
deleteMessages,
|
deleteMessages,
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
moveMessages,
|
moveMessages,
|
||||||
|
downloadMessage,
|
||||||
moveFolder,
|
moveFolder,
|
||||||
renameFolder,
|
renameFolder,
|
||||||
notify,
|
notify,
|
||||||
|
|||||||
Reference in New Issue
Block a user