KI-AGENT: Mailantworten und Entitätsverknüpfungen ergänzen
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
import nodemailer from "nodemailer"
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { and, eq } from "drizzle-orm"
|
||||
import { and, desc, eq } from "drizzle-orm"
|
||||
|
||||
import { encrypt, decrypt } from "../utils/crypt"
|
||||
import { userCredentials } from "../../db/schema"
|
||||
import {
|
||||
customers,
|
||||
emailEntityLinks,
|
||||
emailMessages,
|
||||
plants,
|
||||
projects,
|
||||
userCredentials,
|
||||
vendors,
|
||||
} from "../../db/schema"
|
||||
import { emailSyncService } from "../modules/email/email.sync.service"
|
||||
|
||||
// @ts-ignore
|
||||
@@ -39,6 +47,53 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
|
||||
const bodyValue = (body: any, camelKey: string, snakeKey: string) => body[camelKey] ?? body[snakeKey]
|
||||
|
||||
const entityDefinitions = {
|
||||
customers: { table: customers, tenantColumn: customers.tenant, labelColumn: customers.name, label: "Kunde" },
|
||||
vendors: { table: vendors, tenantColumn: vendors.tenant, labelColumn: vendors.name, label: "Lieferant" },
|
||||
projects: { table: projects, tenantColumn: projects.tenant, labelColumn: projects.name, label: "Projekt" },
|
||||
plants: { table: plants, tenantColumn: plants.tenant, labelColumn: plants.name, label: "Objekt" },
|
||||
} as const
|
||||
|
||||
type EmailEntityType = keyof typeof entityDefinitions
|
||||
|
||||
const getEntityDefinition = (entityType: string) =>
|
||||
entityDefinitions[entityType as EmailEntityType] || null
|
||||
|
||||
const loadEntity = async (tenantId: number, entityType: string, entityId: number) => {
|
||||
const definition = getEntityDefinition(entityType)
|
||||
if (!definition) return null
|
||||
|
||||
const rows = await server.db
|
||||
.select({ id: definition.table.id, name: definition.labelColumn })
|
||||
.from(definition.table)
|
||||
.where(and(
|
||||
eq(definition.tenantColumn, tenantId),
|
||||
eq(definition.table.id, entityId),
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
return rows[0] ? { ...rows[0], typeLabel: definition.label } : null
|
||||
}
|
||||
|
||||
const listMessageEntityLinks = async (tenantId: number, messageId: string) => {
|
||||
const links = await server.db
|
||||
.select()
|
||||
.from(emailEntityLinks)
|
||||
.where(and(
|
||||
eq(emailEntityLinks.tenantId, tenantId),
|
||||
eq(emailEntityLinks.messageId, messageId),
|
||||
))
|
||||
|
||||
return (await Promise.all(links.map(async (link) => {
|
||||
const entity = await loadEntity(tenantId, link.entityType, link.entityId)
|
||||
return entity ? {
|
||||
...link,
|
||||
entityName: entity.name,
|
||||
entityTypeLabel: entity.typeLabel,
|
||||
} : null
|
||||
}))).filter(Boolean)
|
||||
}
|
||||
|
||||
const applyDownloadCorsHeaders = (req: any, reply: any) => {
|
||||
const origin = req.headers.origin
|
||||
if (
|
||||
@@ -221,6 +276,8 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
html?: string
|
||||
attachments?: any
|
||||
account: string
|
||||
sourceMessageId?: string
|
||||
composeMode?: "reply" | "replyAll" | "forward"
|
||||
}
|
||||
|
||||
// Fetch email credentials
|
||||
@@ -248,6 +305,35 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
},
|
||||
})
|
||||
|
||||
const sourceMessage = body.sourceMessageId
|
||||
? await emailSync.getMessage(req.user.tenant_id, req.user.user_id, body.sourceMessageId)
|
||||
: null
|
||||
|
||||
if (body.sourceMessageId && !sourceMessage) {
|
||||
return reply.code(404).send({ error: "Ursprüngliche E-Mail nicht gefunden" })
|
||||
}
|
||||
|
||||
const attachments = [...(Array.isArray(body.attachments) ? body.attachments : [])]
|
||||
|
||||
if (body.composeMode === "forward" && sourceMessage?.attachments?.length) {
|
||||
for (const sourceAttachment of sourceMessage.attachments) {
|
||||
const attachment = await emailSync.getAttachmentContent(
|
||||
req.user.tenant_id,
|
||||
req.user.user_id,
|
||||
sourceAttachment.id,
|
||||
)
|
||||
if (!attachment) continue
|
||||
|
||||
attachments.push({
|
||||
filename: attachment.filename,
|
||||
content: attachment.content,
|
||||
contentType: attachment.contentType,
|
||||
contentDisposition: "attachment",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const isReply = body.composeMode === "reply" || body.composeMode === "replyAll"
|
||||
const message = {
|
||||
from: accountData.email,
|
||||
to: body.to,
|
||||
@@ -256,7 +342,9 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
subject: body.subject,
|
||||
html: body.html,
|
||||
text: body.text,
|
||||
attachments: body.attachments,
|
||||
attachments,
|
||||
inReplyTo: isReply ? sourceMessage?.messageId || undefined : undefined,
|
||||
references: isReply && sourceMessage?.messageId ? [sourceMessage.messageId] : undefined,
|
||||
}
|
||||
|
||||
const info = await transporter.sendMail(message)
|
||||
@@ -381,13 +469,144 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const message = await emailSync.getMessage(req.user.tenant_id, req.user.user_id, id)
|
||||
if (!message) return reply.code(404).send({ error: "E-Mail nicht gefunden" })
|
||||
|
||||
return reply.send(message)
|
||||
return reply.send({
|
||||
...message,
|
||||
entityLinks: await listMessageEntityLinks(req.user.tenant_id, id),
|
||||
})
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mail konnte nicht geladen werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/email/messages/:id/entity-links", 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 { entityType?: string; entityId?: number | string }
|
||||
const entityId = Number(body.entityId)
|
||||
|
||||
if (!body.entityType || !getEntityDefinition(body.entityType)) {
|
||||
return reply.code(400).send({ error: "Nicht unterstützter Entitätstyp" })
|
||||
}
|
||||
if (!Number.isSafeInteger(entityId) || entityId <= 0) {
|
||||
return reply.code(400).send({ error: "Ungültige Entitäts-ID" })
|
||||
}
|
||||
|
||||
const message = await emailSync.getMessage(req.user.tenant_id, req.user.user_id, id)
|
||||
if (!message) return reply.code(404).send({ error: "E-Mail nicht gefunden" })
|
||||
|
||||
const entity = await loadEntity(req.user.tenant_id, body.entityType, entityId)
|
||||
if (!entity) return reply.code(404).send({ error: "Entität nicht gefunden" })
|
||||
|
||||
await server.db
|
||||
.insert(emailEntityLinks)
|
||||
.values({
|
||||
tenantId: req.user.tenant_id,
|
||||
messageId: id,
|
||||
entityType: body.entityType,
|
||||
entityId,
|
||||
linkedBy: req.user.user_id,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
entityLinks: await listMessageEntityLinks(req.user.tenant_id, id),
|
||||
})
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mail konnte nicht verknüpft werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.delete("/email/messages/:id/entity-links/:entityType/:entityId", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id, entityType, entityId: rawEntityId } = req.params as {
|
||||
id: string
|
||||
entityType: string
|
||||
entityId: string
|
||||
}
|
||||
const entityId = Number(rawEntityId)
|
||||
|
||||
const message = await emailSync.getMessage(req.user.tenant_id, req.user.user_id, id)
|
||||
if (!message) return reply.code(404).send({ error: "E-Mail nicht gefunden" })
|
||||
|
||||
await server.db
|
||||
.delete(emailEntityLinks)
|
||||
.where(and(
|
||||
eq(emailEntityLinks.tenantId, req.user.tenant_id),
|
||||
eq(emailEntityLinks.messageId, id),
|
||||
eq(emailEntityLinks.entityType, entityType),
|
||||
eq(emailEntityLinks.entityId, entityId),
|
||||
))
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
entityLinks: await listMessageEntityLinks(req.user.tenant_id, id),
|
||||
})
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "Verknüpfung konnte nicht entfernt werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.get("/email/entity-links/:entityType/:entityId", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { entityType, entityId: rawEntityId } = req.params as {
|
||||
entityType: string
|
||||
entityId: string
|
||||
}
|
||||
const entityId = Number(rawEntityId)
|
||||
|
||||
if (!getEntityDefinition(entityType)) {
|
||||
return reply.code(400).send({ error: "Nicht unterstützter Entitätstyp" })
|
||||
}
|
||||
if (!Number.isSafeInteger(entityId) || entityId <= 0) {
|
||||
return reply.code(400).send({ error: "Ungültige Entitäts-ID" })
|
||||
}
|
||||
if (!await loadEntity(req.user.tenant_id, entityType, entityId)) {
|
||||
return reply.code(404).send({ error: "Entität nicht gefunden" })
|
||||
}
|
||||
|
||||
const rows = await server.db
|
||||
.select({
|
||||
message: emailMessages,
|
||||
linkId: emailEntityLinks.id,
|
||||
linkedAt: emailEntityLinks.createdAt,
|
||||
})
|
||||
.from(emailEntityLinks)
|
||||
.innerJoin(emailMessages, eq(emailMessages.id, emailEntityLinks.messageId))
|
||||
.where(and(
|
||||
eq(emailEntityLinks.tenantId, req.user.tenant_id),
|
||||
eq(emailEntityLinks.entityType, entityType),
|
||||
eq(emailEntityLinks.entityId, entityId),
|
||||
))
|
||||
.orderBy(desc(emailMessages.receivedAt), desc(emailMessages.sentAt))
|
||||
|
||||
return reply.send(rows.map((row) => ({
|
||||
...row.message,
|
||||
linkId: row.linkId,
|
||||
linkedAt: row.linkedAt,
|
||||
canOpen: row.message.userId === req.user.user_id,
|
||||
})))
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "Verknüpfte E-Mails konnten nicht geladen werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/email/messages/:id/read", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
|
||||
Reference in New Issue
Block a user