E-Mail Anhänge gezielt per IMAP-Part laden
KI-AGENT: Der Anhang-Download lädt jetzt bevorzugt den passenden MIME-Part über die IMAP-BODYSTRUCTURE und bekommt ein Timeout, damit der Download nicht dauerhaft lädt.
This commit is contained in:
@@ -49,6 +49,84 @@ const previewText = (text?: string | false | null) => {
|
||||
return text.replace(/\s+/g, " ").trim().slice(0, 240) || null
|
||||
}
|
||||
|
||||
const attachmentFilename = (node: any) =>
|
||||
node?.dispositionParameters?.filename
|
||||
|| node?.parameters?.name
|
||||
|| null
|
||||
|
||||
const normalizeContentId = (value?: string | null) =>
|
||||
value ? value.replace(/^<|>$/g, "") : null
|
||||
|
||||
const collectAttachmentParts = (node: any, parts: any[] = []) => {
|
||||
if (!node) return parts
|
||||
|
||||
if (node.part && !node.childNodes?.length) {
|
||||
const filename = attachmentFilename(node)
|
||||
const disposition = String(node.disposition || "").toLowerCase()
|
||||
|
||||
if (filename || node.id || ["attachment", "inline"].includes(disposition)) {
|
||||
parts.push(node)
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of node.childNodes || []) {
|
||||
collectAttachmentParts(child, parts)
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
const findAttachmentPart = (bodyStructure: any, attachment: any) => {
|
||||
const parts = collectAttachmentParts(bodyStructure)
|
||||
if (!parts.length) return null
|
||||
|
||||
const scored = parts
|
||||
.map((part) => {
|
||||
let score = 0
|
||||
if (attachmentFilename(part) && attachmentFilename(part) === attachment.filename) score += 4
|
||||
if (part.type && part.type === attachment.contentType) score += 3
|
||||
if (Number(part.size || 0) === Number(attachment.size || 0)) score += 2
|
||||
if (
|
||||
normalizeContentId(part.id)
|
||||
&& normalizeContentId(part.id) === normalizeContentId(attachment.contentId)
|
||||
) {
|
||||
score += 3
|
||||
}
|
||||
return { part, score }
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)
|
||||
|
||||
if (scored[0]?.score > 0) return scored[0].part
|
||||
return parts.length === 1 ? parts[0] : null
|
||||
}
|
||||
|
||||
const streamToBuffer = async (stream: any, timeoutMs = 45_000) => {
|
||||
const chunks: Buffer[] = []
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
(async () => {
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
}
|
||||
})(),
|
||||
new Promise((_, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
if (typeof stream.destroy === "function") {
|
||||
stream.destroy()
|
||||
}
|
||||
reject(new Error("Anhang-Download hat zu lange gedauert"))
|
||||
}, timeoutMs)
|
||||
}),
|
||||
])
|
||||
} finally {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
}
|
||||
|
||||
return Buffer.concat(chunks)
|
||||
}
|
||||
|
||||
const flagsFromMessage = (flags: Set<string> | string[] | undefined) => {
|
||||
if (!flags) return []
|
||||
return Array.isArray(flags) ? flags : Array.from(flags)
|
||||
@@ -645,10 +723,37 @@ export function emailSyncService(server: FastifyInstance) {
|
||||
try {
|
||||
await client.mailboxOpen(row.message.mailboxPath)
|
||||
|
||||
for await (const fetched of client.fetch([row.message.uid], {
|
||||
const structureMessage = await client.fetchOne(String(row.message.uid), {
|
||||
uid: true,
|
||||
bodyStructure: true,
|
||||
}, { uid: true })
|
||||
const attachmentPart = structureMessage
|
||||
? findAttachmentPart(structureMessage.bodyStructure, row.attachment)
|
||||
: null
|
||||
|
||||
if (attachmentPart?.part) {
|
||||
const downloaded = await client.download(String(row.message.uid), attachmentPart.part, { uid: true })
|
||||
const content = await streamToBuffer(downloaded.content)
|
||||
|
||||
if (content.length) {
|
||||
return {
|
||||
filename: downloaded.meta?.filename
|
||||
|| attachmentFilename(attachmentPart)
|
||||
|| row.attachment.filename
|
||||
|| "anhang",
|
||||
contentType: downloaded.meta?.contentType
|
||||
|| attachmentPart.type
|
||||
|| row.attachment.contentType
|
||||
|| "application/octet-stream",
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fetched = await client.fetchOne(String(row.message.uid), {
|
||||
source: true,
|
||||
}, { uid: true })) {
|
||||
if (!fetched.source) break
|
||||
}, { uid: true })
|
||||
if (!fetched || !fetched.source) return null
|
||||
|
||||
const parsed = await simpleParser(fetched.source)
|
||||
const matchedAttachment = parsed.attachments.find((item) =>
|
||||
@@ -663,7 +768,9 @@ export function emailSyncService(server: FastifyInstance) {
|
||||
|| parsed.attachments[attachmentIndex]
|
||||
|| (parsed.attachments.length === 1 ? parsed.attachments[0] : null)
|
||||
|
||||
if (!attachment) break
|
||||
if (!attachment) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
filename: attachment.filename || row.attachment.filename || "anhang",
|
||||
@@ -672,7 +779,6 @@ export function emailSyncService(server: FastifyInstance) {
|
||||
? attachment.content
|
||||
: Buffer.from(attachment.content),
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
|
||||
@@ -458,6 +458,7 @@ async function downloadAttachment(attachment: NonNullable<EmailMessage["attachme
|
||||
try {
|
||||
const response = await $api.raw(`/api/email/attachments/${attachment.id}/download`, {
|
||||
responseType: "arrayBuffer",
|
||||
timeout: 60_000,
|
||||
})
|
||||
const contentType = response.headers.get("content-type") || attachment.contentType || "application/octet-stream"
|
||||
const blob = new Blob([response._data as ArrayBuffer], { type: contentType })
|
||||
|
||||
Reference in New Issue
Block a user