E-Mail Anhang-Downloads stabilisieren
KI-AGENT: Lädt Anhänge nun authentifiziert als Blob herunter und macht die Backend-Zuordnung von Anhängen zur Originalmail robuster, inklusive Positions-Fallback.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
import { and, desc, eq } from "drizzle-orm"
|
import { and, asc, desc, eq } from "drizzle-orm"
|
||||||
import { ImapFlow } from "imapflow"
|
import { ImapFlow } from "imapflow"
|
||||||
import { simpleParser } from "mailparser"
|
import { simpleParser } from "mailparser"
|
||||||
|
|
||||||
@@ -625,6 +625,13 @@ export function emailSyncService(server: FastifyInstance) {
|
|||||||
const row = rows[0]
|
const row = rows[0]
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
|
||||||
|
const messageAttachments = await server.db
|
||||||
|
.select()
|
||||||
|
.from(emailAttachments)
|
||||||
|
.where(eq(emailAttachments.messageId, row.message.id))
|
||||||
|
.orderBy(asc(emailAttachments.createdAt), asc(emailAttachments.id))
|
||||||
|
const attachmentIndex = messageAttachments.findIndex((attachment) => attachment.id === attachmentId)
|
||||||
|
|
||||||
const account = await getAccount(tenantId, userId, row.message.accountId)
|
const account = await getAccount(tenantId, userId, row.message.accountId)
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error("E-Mail Konto nicht gefunden")
|
throw new Error("E-Mail Konto nicht gefunden")
|
||||||
@@ -644,7 +651,7 @@ export function emailSyncService(server: FastifyInstance) {
|
|||||||
if (!fetched.source) break
|
if (!fetched.source) break
|
||||||
|
|
||||||
const parsed = await simpleParser(fetched.source)
|
const parsed = await simpleParser(fetched.source)
|
||||||
const attachment = parsed.attachments.find((item) =>
|
const matchedAttachment = parsed.attachments.find((item) =>
|
||||||
(row.attachment.checksum && item.checksum === row.attachment.checksum)
|
(row.attachment.checksum && item.checksum === row.attachment.checksum)
|
||||||
|| (
|
|| (
|
||||||
item.filename === row.attachment.filename
|
item.filename === row.attachment.filename
|
||||||
@@ -652,6 +659,9 @@ export function emailSyncService(server: FastifyInstance) {
|
|||||||
&& Number(item.size || 0) === Number(row.attachment.size || 0)
|
&& Number(item.size || 0) === Number(row.attachment.size || 0)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
const attachment = matchedAttachment
|
||||||
|
|| parsed.attachments[attachmentIndex]
|
||||||
|
|| (parsed.attachments.length === 1 ? parsed.attachments[0] : null)
|
||||||
|
|
||||||
if (!attachment) break
|
if (!attachment) break
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ type EmailMessage = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { $api } = useNuxtApp()
|
const { $api } = useNuxtApp()
|
||||||
const config = useRuntimeConfig()
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const accounts = ref<EmailAccount[]>([])
|
const accounts = ref<EmailAccount[]>([])
|
||||||
@@ -454,8 +453,38 @@ async function moveSelectedMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadAttachment(attachmentId: string) {
|
async function downloadAttachment(attachment: NonNullable<EmailMessage["attachments"]>[number]) {
|
||||||
window.open(`${config.public.apiBase}/api/email/attachments/${attachmentId}/download`, "_blank")
|
actionLoading.value = `attachment-${attachment.id}`
|
||||||
|
try {
|
||||||
|
const response = await $fetch.raw(`/api/email/attachments/${attachment.id}/download`, {
|
||||||
|
responseType: "blob",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
...(useCookie("token").value ? { Authorization: `Bearer ${useCookie("token").value}` } : {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const blob = response._data as Blob
|
||||||
|
const disposition = response.headers.get("content-disposition") || ""
|
||||||
|
const dispositionFilename = disposition.match(/filename="([^"]+)"/)?.[1]
|
||||||
|
const filename = dispositionFilename || attachment.filename || "anhang"
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement("a")
|
||||||
|
|
||||||
|
link.href = url
|
||||||
|
link.download = filename
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.add({
|
||||||
|
title: "Download fehlgeschlagen",
|
||||||
|
description: err?.data?.error || err?.message || "Der Anhang konnte nicht geladen werden.",
|
||||||
|
color: "error",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setMessageSeen(messageId: string, seen: boolean) {
|
async function setMessageSeen(messageId: string, seen: boolean) {
|
||||||
@@ -814,12 +843,16 @@ onMounted(loadAccounts)
|
|||||||
v-for="attachment in selectedMessage.attachments"
|
v-for="attachment in selectedMessage.attachments"
|
||||||
:key="attachment.id"
|
:key="attachment.id"
|
||||||
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)"
|
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)"
|
@click="downloadAttachment(attachment)"
|
||||||
>
|
>
|
||||||
<UIcon name="i-heroicons-paper-clip" class="size-4 text-dimmed" />
|
<UIcon name="i-heroicons-paper-clip" class="size-4 text-dimmed" />
|
||||||
<span>{{ attachment.filename || 'Anhang' }}</span>
|
<span>{{ attachment.filename || 'Anhang' }}</span>
|
||||||
<span class="text-xs text-dimmed">{{ formatAttachmentSize(attachment.size) }}</span>
|
<span class="text-xs text-dimmed">{{ formatAttachmentSize(attachment.size) }}</span>
|
||||||
<UIcon name="i-heroicons-arrow-down-tray" class="size-4 text-dimmed" />
|
<UIcon
|
||||||
|
:name="actionLoading === `attachment-${attachment.id}` ? 'i-heroicons-arrow-path' : 'i-heroicons-arrow-down-tray'"
|
||||||
|
class="size-4 text-dimmed"
|
||||||
|
:class="actionLoading === `attachment-${attachment.id}` ? 'animate-spin' : ''"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user