E-Mail Anhänge ohne Fetch herunterladen

KI-AGENT: Der E-Mail Anhang-Download nutzt jetzt einen nativen Browser-Link statt Cross-Origin-Fetch und erlaubt dafür den bestehenden JWT gezielt als Download-Token.
This commit is contained in:
2026-05-23 21:04:53 +02:00
parent a34bf43756
commit 154d7060f8
2 changed files with 25 additions and 15 deletions

View File

@@ -68,6 +68,15 @@ export default fp(async (server: FastifyInstance) => {
return return
} }
const urlPath = req.url.split("?")[0]
const queryToken = (req.query as any)?.downloadToken
const downloadToken =
typeof queryToken === "string"
&& urlPath.startsWith("/api/email/attachments/")
&& urlPath.endsWith("/download")
? queryToken
: null
// 1⃣ Token aus Header oder Cookie lesen // 1⃣ Token aus Header oder Cookie lesen
const cookieToken = req.cookies?.token const cookieToken = req.cookies?.token
const authHeader = req.headers.authorization const authHeader = req.headers.authorization
@@ -78,7 +87,7 @@ export default fp(async (server: FastifyInstance) => {
const token = const token =
headerToken && headerToken.length > 10 headerToken && headerToken.length > 10
? headerToken ? headerToken
: cookieToken || null : cookieToken || downloadToken || null
if (!token) { if (!token) {
return reply.code(401).send({ error: "Authentication required" }) return reply.code(401).send({ error: "Authentication required" })

View File

@@ -55,6 +55,7 @@ type EmailMessage = {
} }
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const runtimeConfig = useRuntimeConfig()
const toast = useToast() const toast = useToast()
const accounts = ref<EmailAccount[]>([]) const accounts = ref<EmailAccount[]>([])
@@ -456,24 +457,22 @@ async function moveSelectedMessage() {
async function downloadAttachment(attachment: NonNullable<EmailMessage["attachments"]>[number]) { async function downloadAttachment(attachment: NonNullable<EmailMessage["attachments"]>[number]) {
actionLoading.value = `attachment-${attachment.id}` actionLoading.value = `attachment-${attachment.id}`
try { try {
const response = await $api.raw(`/api/email/attachments/${attachment.id}/download`, { const apiBase = String(runtimeConfig.public.apiBase || "").replace(/\/$/, "")
responseType: "arrayBuffer", const path = `/api/email/attachments/${attachment.id}/download`
timeout: 60_000, const downloadUrl = new URL(apiBase ? `${apiBase}${path}` : path, window.location.origin)
}) const token = useCookie("token").value
const contentType = response.headers.get("content-type") || attachment.contentType || "application/octet-stream"
const blob = new Blob([response._data as ArrayBuffer], { type: contentType }) if (token) {
const disposition = response.headers.get("content-disposition") || "" downloadUrl.searchParams.set("downloadToken", token)
const dispositionFilename = disposition.match(/filename="([^"]+)"/)?.[1] }
const filename = dispositionFilename || attachment.filename || "anhang"
const url = URL.createObjectURL(blob)
const link = document.createElement("a") const link = document.createElement("a")
link.href = url link.href = downloadUrl.toString()
link.download = filename link.download = attachment.filename || "anhang"
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
link.remove() link.remove()
URL.revokeObjectURL(url)
} catch (err: any) { } catch (err: any) {
toast.add({ toast.add({
title: "Download fehlgeschlagen", title: "Download fehlgeschlagen",
@@ -481,7 +480,9 @@ async function downloadAttachment(attachment: NonNullable<EmailMessage["attachme
color: "error", color: "error",
}) })
} finally { } finally {
actionLoading.value = "" window.setTimeout(() => {
actionLoading.value = ""
}, 750)
} }
} }