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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,4 +393,86 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/email/messages/:id/move", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string }
|
||||
const body = (req.body || {}) as { mailbox?: string }
|
||||
|
||||
if (!body.mailbox) {
|
||||
return reply.code(400).send({ error: "Zielordner fehlt" })
|
||||
}
|
||||
|
||||
const result = await emailSync.moveMessage(
|
||||
req.user.tenant_id,
|
||||
req.user.user_id,
|
||||
id,
|
||||
body.mailbox,
|
||||
)
|
||||
|
||||
if (!result) return reply.code(404).send({ error: "E-Mail nicht gefunden" })
|
||||
return reply.send({ success: true, ...result })
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mail konnte nicht verschoben werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/email/messages/:id/archive", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string }
|
||||
const result = await emailSync.archiveMessage(req.user.tenant_id, req.user.user_id, id)
|
||||
|
||||
if (!result) return reply.code(404).send({ error: "E-Mail nicht gefunden" })
|
||||
return reply.send({ success: true, ...result })
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mail konnte nicht archiviert werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.delete("/email/messages/:id", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string }
|
||||
const result = await emailSync.deleteMessage(req.user.tenant_id, req.user.user_id, id)
|
||||
|
||||
if (!result) return reply.code(404).send({ error: "E-Mail nicht gefunden" })
|
||||
return reply.send({ success: true })
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mail konnte nicht gelöscht werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.get("/email/attachments/:id/download", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string }
|
||||
const attachment = await emailSync.getAttachmentContent(req.user.tenant_id, req.user.user_id, id)
|
||||
|
||||
if (!attachment) return reply.code(404).send({ error: "Anhang nicht gefunden" })
|
||||
|
||||
reply.header("Content-Type", attachment.contentType)
|
||||
reply.header("Content-Disposition", `attachment; filename="${attachment.filename.replace(/"/g, "")}"`)
|
||||
return reply.send(attachment.content)
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "Anhang konnte nicht geladen werden" })
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user