E-Mail Aktionen und Anhang-Download ergänzen
KI-AGENT: Ergänzt IMAP-basierte Aktionen zum Verschieben, Archivieren und Löschen von E-Mails sowie den Download von Anhängen aus der Originalmail. Die E-Mail Oberfläche bietet dafür Zielordner-Auswahl, Aktionsbuttons und Anhang-Downloads.
This commit is contained in:
@@ -55,6 +55,7 @@ type EmailMessage = {
|
||||
}
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
const config = useRuntimeConfig()
|
||||
const toast = useToast()
|
||||
|
||||
const accounts = ref<EmailAccount[]>([])
|
||||
@@ -71,6 +72,8 @@ const loadingMessage = ref(false)
|
||||
const syncing = ref(false)
|
||||
const expandedMailboxPaths = ref<string[]>([])
|
||||
const syncedMailboxPaths = ref<string[]>([])
|
||||
const actionLoading = ref("")
|
||||
const moveTargetMailboxPath = ref("")
|
||||
|
||||
const selectedAccount = computed(() =>
|
||||
accounts.value.find((account) => account.id === selectedAccountId.value) || null
|
||||
@@ -171,6 +174,15 @@ const filteredMessages = computed(() => {
|
||||
].some((value) => String(value || "").toLowerCase().includes(needle)))
|
||||
})
|
||||
|
||||
const moveMailboxOptions = computed(() =>
|
||||
mailboxes.value
|
||||
.filter((mailbox) => mailbox.path !== selectedMessage.value?.mailboxPath)
|
||||
.map((mailbox) => ({
|
||||
label: mailboxLabel(mailbox),
|
||||
value: mailbox.path,
|
||||
}))
|
||||
)
|
||||
|
||||
const mailboxIcon = (mailbox: EmailMailbox) => {
|
||||
if (mailbox.specialUse === "\\Inbox" || mailbox.path.toUpperCase() === "INBOX") return "i-heroicons-inbox"
|
||||
if (mailbox.specialUse === "\\Sent") return "i-heroicons-paper-airplane"
|
||||
@@ -354,6 +366,7 @@ async function selectMessage(message: EmailMessage) {
|
||||
loadingMessage.value = true
|
||||
try {
|
||||
selectedMessage.value = await $api(`/api/email/messages/${message.id}`)
|
||||
moveTargetMailboxPath.value = ""
|
||||
if (!message.seen) {
|
||||
await setMessageSeen(message.id, true)
|
||||
}
|
||||
@@ -362,6 +375,89 @@ async function selectMessage(message: EmailMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
function removeMessageFromCurrentList(messageId: string) {
|
||||
const currentIndex = messages.value.findIndex((message) => message.id === messageId)
|
||||
messages.value = messages.value.filter((message) => message.id !== messageId)
|
||||
|
||||
const nextMessage = messages.value[currentIndex] || messages.value[currentIndex - 1] || null
|
||||
selectedMessage.value = null
|
||||
|
||||
if (nextMessage) {
|
||||
selectMessage(nextMessage)
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveSelectedMessage() {
|
||||
if (!selectedMessage.value) return
|
||||
|
||||
const messageId = selectedMessage.value.id
|
||||
actionLoading.value = "archive"
|
||||
|
||||
try {
|
||||
await $api(`/api/email/messages/${messageId}/archive`, { method: "POST" })
|
||||
removeMessageFromCurrentList(messageId)
|
||||
toast.add({ title: "E-Mail archiviert", color: "success" })
|
||||
} catch (err: any) {
|
||||
toast.add({
|
||||
title: "Archivieren fehlgeschlagen",
|
||||
description: err?.data?.error || err?.message || "Die E-Mail konnte nicht archiviert werden.",
|
||||
color: "error",
|
||||
})
|
||||
} finally {
|
||||
actionLoading.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSelectedMessage() {
|
||||
if (!selectedMessage.value) return
|
||||
|
||||
const messageId = selectedMessage.value.id
|
||||
actionLoading.value = "delete"
|
||||
|
||||
try {
|
||||
await $api(`/api/email/messages/${messageId}`, { method: "DELETE" })
|
||||
removeMessageFromCurrentList(messageId)
|
||||
toast.add({ title: "E-Mail gelöscht", color: "success" })
|
||||
} catch (err: any) {
|
||||
toast.add({
|
||||
title: "Löschen fehlgeschlagen",
|
||||
description: err?.data?.error || err?.message || "Die E-Mail konnte nicht gelöscht werden.",
|
||||
color: "error",
|
||||
})
|
||||
} finally {
|
||||
actionLoading.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
async function moveSelectedMessage() {
|
||||
if (!selectedMessage.value || !moveTargetMailboxPath.value) return
|
||||
|
||||
const messageId = selectedMessage.value.id
|
||||
actionLoading.value = "move"
|
||||
|
||||
try {
|
||||
await $api(`/api/email/messages/${messageId}/move`, {
|
||||
method: "POST",
|
||||
body: { mailbox: moveTargetMailboxPath.value },
|
||||
})
|
||||
removeMessageFromCurrentList(messageId)
|
||||
toast.add({ title: "E-Mail verschoben", color: "success" })
|
||||
moveTargetMailboxPath.value = ""
|
||||
} catch (err: any) {
|
||||
toast.add({
|
||||
title: "Verschieben fehlgeschlagen",
|
||||
description: err?.data?.error || err?.message || "Die E-Mail konnte nicht verschoben werden.",
|
||||
color: "error",
|
||||
})
|
||||
} finally {
|
||||
actionLoading.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
function downloadAttachment(attachmentId: string) {
|
||||
window.open(`${config.public.apiBase}/api/email/attachments/${attachmentId}/download`, "_blank")
|
||||
}
|
||||
|
||||
async function setMessageSeen(messageId: string, seen: boolean) {
|
||||
const previousMessage = messages.value.find((message) => message.id === messageId)
|
||||
|
||||
@@ -634,6 +730,40 @@ onMounted(loadAccounts)
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<USelectMenu
|
||||
v-model="moveTargetMailboxPath"
|
||||
:items="moveMailboxOptions"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
size="sm"
|
||||
class="w-48"
|
||||
placeholder="Verschieben nach"
|
||||
/>
|
||||
<UButton
|
||||
icon="i-heroicons-folder-arrow-down"
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
size="sm"
|
||||
:loading="actionLoading === 'move'"
|
||||
:disabled="!moveTargetMailboxPath"
|
||||
@click="moveSelectedMessage"
|
||||
/>
|
||||
<UButton
|
||||
icon="i-heroicons-archive-box-arrow-down"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:loading="actionLoading === 'archive'"
|
||||
@click="archiveSelectedMessage"
|
||||
/>
|
||||
<UButton
|
||||
icon="i-heroicons-trash"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:loading="actionLoading === 'delete'"
|
||||
@click="deleteSelectedMessage"
|
||||
/>
|
||||
<UButton
|
||||
:icon="selectedMessage.seen ? 'i-heroicons-envelope' : 'i-heroicons-envelope-open'"
|
||||
color="neutral"
|
||||
@@ -680,15 +810,17 @@ onMounted(loadAccounts)
|
||||
</div>
|
||||
|
||||
<div v-if="selectedMessage.attachments?.length" class="mt-4 flex flex-wrap gap-2">
|
||||
<div
|
||||
<button
|
||||
v-for="attachment in selectedMessage.attachments"
|
||||
:key="attachment.id"
|
||||
class="flex items-center gap-2 rounded-md border border-(--ui-border) px-3 py-2 text-sm"
|
||||
class="flex items-center gap-2 rounded-md border border-(--ui-border) px-3 py-2 text-left text-sm hover:bg-(--ui-bg-muted)"
|
||||
@click="downloadAttachment(attachment.id)"
|
||||
>
|
||||
<UIcon name="i-heroicons-paper-clip" class="size-4 text-dimmed" />
|
||||
<span>{{ attachment.filename || 'Anhang' }}</span>
|
||||
<span class="text-xs text-dimmed">{{ formatAttachmentSize(attachment.size) }}</span>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-down-tray" class="size-4 text-dimmed" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user