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:
2026-05-23 20:38:01 +02:00
parent be4a5caaec
commit 358cd906ae
3 changed files with 413 additions and 3 deletions

View File

@@ -479,6 +479,198 @@ export function emailSyncService(server: FastifyInstance) {
return updated
}
const loadMessageForAction = async (tenantId: number, userId: string, messageId: string) => {
const rows = await server.db
.select()
.from(emailMessages)
.where(and(
eq(emailMessages.tenantId, tenantId),
eq(emailMessages.userId, userId),
eq(emailMessages.id, messageId),
))
.limit(1)
return rows[0] || null
}
const deleteCachedMessage = async (messageId: string) => {
await server.db
.delete(emailMessages)
.where(eq(emailMessages.id, messageId))
}
const moveMessage = async (
tenantId: number,
userId: string,
messageId: string,
destinationMailboxPath: string,
) => {
const message = await loadMessageForAction(tenantId, userId, messageId)
if (!message) return null
if (message.mailboxPath === destinationMailboxPath) {
return message
}
const account = await getAccount(tenantId, userId, message.accountId)
if (!account) {
throw new Error("E-Mail Konto nicht gefunden")
}
const targetRows = await server.db
.select({ path: emailMailboxes.path })
.from(emailMailboxes)
.where(and(
eq(emailMailboxes.tenantId, tenantId),
eq(emailMailboxes.userId, userId),
eq(emailMailboxes.accountId, message.accountId),
eq(emailMailboxes.path, destinationMailboxPath),
))
.limit(1)
if (!targetRows[0]) {
throw new Error("Zielordner nicht gefunden")
}
const client = createClient(account)
await client.connect()
try {
const lock = await client.getMailboxLock(message.mailboxPath)
try {
await client.mailboxOpen(message.mailboxPath)
await client.messageMove(message.uid, destinationMailboxPath, { uid: true })
} finally {
lock.release()
}
} finally {
await client.logout().catch(() => client.close())
}
await deleteCachedMessage(messageId)
return { success: true, destinationMailboxPath }
}
const archiveMessage = async (tenantId: number, userId: string, messageId: string) => {
const message = await loadMessageForAction(tenantId, userId, messageId)
if (!message) return null
const mailboxes = await server.db
.select()
.from(emailMailboxes)
.where(and(
eq(emailMailboxes.tenantId, tenantId),
eq(emailMailboxes.userId, userId),
eq(emailMailboxes.accountId, message.accountId),
))
const archiveMailbox = mailboxes.find((mailbox) => mailbox.specialUse === "\\Archive")
|| mailboxes.find((mailbox) => ["archive", "archiv"].includes(mailbox.name.toLowerCase()))
|| mailboxes.find((mailbox) => ["archive", "archiv"].includes(mailbox.path.toLowerCase()))
if (!archiveMailbox) {
throw new Error("Kein Archivordner gefunden")
}
return await moveMessage(tenantId, userId, messageId, archiveMailbox.path)
}
const deleteMessage = async (tenantId: number, userId: string, messageId: string) => {
const message = await loadMessageForAction(tenantId, userId, messageId)
if (!message) return null
const account = await getAccount(tenantId, userId, message.accountId)
if (!account) {
throw new Error("E-Mail Konto nicht gefunden")
}
const client = createClient(account)
await client.connect()
try {
const lock = await client.getMailboxLock(message.mailboxPath)
try {
await client.mailboxOpen(message.mailboxPath)
await client.messageDelete(message.uid, { uid: true })
} finally {
lock.release()
}
} finally {
await client.logout().catch(() => client.close())
}
await deleteCachedMessage(messageId)
return { success: true }
}
const getAttachmentContent = async (
tenantId: number,
userId: string,
attachmentId: string,
) => {
const rows = await server.db
.select({
attachment: emailAttachments,
message: emailMessages,
})
.from(emailAttachments)
.innerJoin(emailMessages, eq(emailMessages.id, emailAttachments.messageId))
.where(and(
eq(emailAttachments.id, attachmentId),
eq(emailMessages.tenantId, tenantId),
eq(emailMessages.userId, userId),
))
.limit(1)
const row = rows[0]
if (!row) return null
const account = await getAccount(tenantId, userId, row.message.accountId)
if (!account) {
throw new Error("E-Mail Konto nicht gefunden")
}
const client = createClient(account)
await client.connect()
try {
const lock = await client.getMailboxLock(row.message.mailboxPath)
try {
await client.mailboxOpen(row.message.mailboxPath)
for await (const fetched of client.fetch([row.message.uid], {
source: true,
}, { uid: true })) {
if (!fetched.source) break
const parsed = await simpleParser(fetched.source)
const attachment = parsed.attachments.find((item) =>
(row.attachment.checksum && item.checksum === row.attachment.checksum)
|| (
item.filename === row.attachment.filename
&& item.contentType === row.attachment.contentType
&& Number(item.size || 0) === Number(row.attachment.size || 0)
)
)
if (!attachment) break
return {
filename: attachment.filename || row.attachment.filename || "anhang",
contentType: attachment.contentType || row.attachment.contentType || "application/octet-stream",
content: attachment.content,
}
}
} finally {
lock.release()
}
} finally {
await client.logout().catch(() => client.close())
}
return null
}
const listMailboxes = async (tenantId: number, userId: string, accountId: string) => {
return await server.db
.select()
@@ -546,5 +738,9 @@ export function emailSyncService(server: FastifyInstance) {
listMessages,
getMessage,
setMessageSeen,
moveMessage,
archiveMessage,
deleteMessage,
getAttachmentContent,
}
}