E-Mail Cache und Konto-Synchronisation vorbereiten
KI-AGENT: Ergänzt Tabellen für lokalen E-Mail-Cache, IMAP-Sync-Service und Inbox-API. Überarbeitet außerdem die E-Mail-Konto-Seiten mit sicherer Passwortbehandlung und manuellem Sync.
This commit is contained in:
@@ -1,17 +1,53 @@
|
||||
import nodemailer from "nodemailer"
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { and, eq } from "drizzle-orm"
|
||||
|
||||
import { sendMailAsUser } from "../utils/emailengine"
|
||||
import { encrypt, decrypt } from "../utils/crypt"
|
||||
import { userCredentials } from "../../db/schema"
|
||||
// Pfad ggf. anpassen
|
||||
import { emailSyncService } from "../modules/email/email.sync.service"
|
||||
|
||||
// @ts-ignore
|
||||
import MailComposer from "nodemailer/lib/mail-composer/index.js"
|
||||
import { ImapFlow } from "imapflow"
|
||||
|
||||
export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const emailSync = emailSyncService(server)
|
||||
|
||||
const encryptedValue = (value: unknown) => value ? decrypt(value as any) : null
|
||||
|
||||
const accountResponse = (row: any) => ({
|
||||
id: row.id,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
userId: row.userId,
|
||||
tenantId: row.tenantId,
|
||||
type: row.type,
|
||||
email: encryptedValue(row.emailEncrypted),
|
||||
smtpHost: encryptedValue(row.smtpHostEncrypted),
|
||||
smtpPort: row.smtpPort ? Number(row.smtpPort) : null,
|
||||
smtpSsl: row.smtpSsl,
|
||||
imapHost: encryptedValue(row.imapHostEncrypted),
|
||||
imapPort: row.imapPort ? Number(row.imapPort) : null,
|
||||
imapSsl: row.imapSsl,
|
||||
hasPassword: Boolean(row.passwordEncrypted),
|
||||
})
|
||||
|
||||
const accountCredentials = (row: any) => ({
|
||||
...accountResponse(row),
|
||||
password: encryptedValue(row.passwordEncrypted),
|
||||
})
|
||||
|
||||
const bodyValue = (body: any, camelKey: string, snakeKey: string) => body[camelKey] ?? body[snakeKey]
|
||||
|
||||
const accountWhere = (tenantId: number, userId: string, id?: string) => {
|
||||
const conditions = [
|
||||
eq(userCredentials.tenantId, tenantId),
|
||||
eq(userCredentials.userId, userId),
|
||||
eq(userCredentials.type, "mail"),
|
||||
]
|
||||
if (id) conditions.push(eq(userCredentials.id, id))
|
||||
return and(...conditions)
|
||||
}
|
||||
|
||||
|
||||
// ======================================================================
|
||||
@@ -28,34 +64,49 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const body = req.body as {
|
||||
email: string
|
||||
password: string
|
||||
smtp_host: string
|
||||
smtp_port: number
|
||||
smtp_ssl: boolean
|
||||
imap_host: string
|
||||
imap_port: number
|
||||
imap_ssl: boolean
|
||||
smtpHost?: string
|
||||
smtpPort?: number
|
||||
smtpSsl?: boolean
|
||||
imapHost?: string
|
||||
imapPort?: number
|
||||
imapSsl?: boolean
|
||||
smtp_host?: string
|
||||
smtp_port?: number
|
||||
smtp_ssl?: boolean
|
||||
imap_host?: string
|
||||
imap_port?: number
|
||||
imap_ssl?: boolean
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// UPDATE EXISTING
|
||||
// -----------------------------
|
||||
if (id) {
|
||||
const rows = await server.db
|
||||
.select({ id: userCredentials.id })
|
||||
.from(userCredentials)
|
||||
.where(accountWhere(req.user.tenant_id, req.user.user_id, id))
|
||||
.limit(1)
|
||||
|
||||
if (!rows[0]) return reply.code(404).send({ error: "Account not found" })
|
||||
|
||||
const saveData = {
|
||||
emailEncrypted: body.email ? encrypt(body.email) : undefined,
|
||||
passwordEncrypted: body.password ? encrypt(body.password) : undefined,
|
||||
smtpHostEncrypted: body.smtp_host ? encrypt(body.smtp_host) : undefined,
|
||||
smtpPort: body.smtp_port,
|
||||
smtpSsl: body.smtp_ssl,
|
||||
imapHostEncrypted: body.imap_host ? encrypt(body.imap_host) : undefined,
|
||||
imapPort: body.imap_port,
|
||||
imapSsl: body.imap_ssl,
|
||||
smtpHostEncrypted: bodyValue(body, "smtpHost", "smtp_host") ? encrypt(bodyValue(body, "smtpHost", "smtp_host")) : undefined,
|
||||
smtpPort: bodyValue(body, "smtpPort", "smtp_port"),
|
||||
smtpSsl: bodyValue(body, "smtpSsl", "smtp_ssl"),
|
||||
imapHostEncrypted: bodyValue(body, "imapHost", "imap_host") ? encrypt(bodyValue(body, "imapHost", "imap_host")) : undefined,
|
||||
imapPort: bodyValue(body, "imapPort", "imap_port"),
|
||||
imapSsl: bodyValue(body, "imapSsl", "imap_ssl"),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
await server.db
|
||||
.update(userCredentials)
|
||||
//@ts-ignore
|
||||
.set(saveData)
|
||||
.where(eq(userCredentials.id, id))
|
||||
.where(accountWhere(req.user.tenant_id, req.user.user_id, id))
|
||||
|
||||
return reply.send({ success: true })
|
||||
}
|
||||
@@ -71,13 +122,13 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
emailEncrypted: encrypt(body.email),
|
||||
passwordEncrypted: encrypt(body.password),
|
||||
|
||||
smtpHostEncrypted: encrypt(body.smtp_host),
|
||||
smtpPort: body.smtp_port,
|
||||
smtpSsl: body.smtp_ssl,
|
||||
smtpHostEncrypted: encrypt(bodyValue(body, "smtpHost", "smtp_host")),
|
||||
smtpPort: bodyValue(body, "smtpPort", "smtp_port"),
|
||||
smtpSsl: bodyValue(body, "smtpSsl", "smtp_ssl"),
|
||||
|
||||
imapHostEncrypted: encrypt(body.imap_host),
|
||||
imapPort: body.imap_port,
|
||||
imapSsl: body.imap_ssl,
|
||||
imapHostEncrypted: encrypt(bodyValue(body, "imapHost", "imap_host")),
|
||||
imapPort: bodyValue(body, "imapPort", "imap_port"),
|
||||
imapSsl: bodyValue(body, "imapSsl", "imap_ssl"),
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
@@ -110,24 +161,13 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(userCredentials)
|
||||
.where(eq(userCredentials.id, id))
|
||||
.where(accountWhere(req.user.tenant_id, req.user.user_id, id))
|
||||
.limit(1)
|
||||
|
||||
const row = rows[0]
|
||||
if (!row) return reply.code(404).send({ error: "Not found" })
|
||||
|
||||
const returnData: any = {}
|
||||
|
||||
Object.entries(row).forEach(([key, val]) => {
|
||||
if (key.endsWith("Encrypted")) {
|
||||
const cleanKey = key.replace("Encrypted", "")
|
||||
// @ts-ignore
|
||||
returnData[cleanKey] = decrypt(val as string)
|
||||
} else {
|
||||
returnData[key] = val
|
||||
}
|
||||
})
|
||||
|
||||
return reply.send(returnData)
|
||||
return reply.send(accountResponse(row))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -136,24 +176,9 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(userCredentials)
|
||||
.where(eq(userCredentials.tenantId, req.user.tenant_id))
|
||||
.where(accountWhere(req.user.tenant_id, req.user.user_id))
|
||||
|
||||
const accounts = rows.map(row => {
|
||||
const temp: any = {}
|
||||
console.log(row)
|
||||
Object.entries(row).forEach(([key, val]) => {
|
||||
console.log(key,val)
|
||||
if (key.endsWith("Encrypted") && val) {
|
||||
// @ts-ignore
|
||||
temp[key.replace("Encrypted", "")] = decrypt(val)
|
||||
} else {
|
||||
temp[key] = val
|
||||
}
|
||||
})
|
||||
return temp
|
||||
})
|
||||
|
||||
return reply.send(accounts)
|
||||
return reply.send(rows.map(accountResponse))
|
||||
|
||||
} catch (err) {
|
||||
console.error("GET /email/accounts error:", err)
|
||||
@@ -183,21 +208,13 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(userCredentials)
|
||||
.where(eq(userCredentials.id, body.account))
|
||||
.where(accountWhere(req.user.tenant_id, req.user.user_id, body.account))
|
||||
.limit(1)
|
||||
|
||||
const row = rows[0]
|
||||
if (!row) return reply.code(404).send({ error: "Account not found" })
|
||||
|
||||
const accountData: any = {}
|
||||
|
||||
Object.entries(row).forEach(([key, val]) => {
|
||||
if (key.endsWith("Encrypted") && val) {
|
||||
// @ts-ignore
|
||||
accountData[key.replace("Encrypted", "")] = decrypt(val as string)
|
||||
} else {
|
||||
accountData[key] = val
|
||||
}
|
||||
})
|
||||
const accountData = accountCredentials(row)
|
||||
|
||||
// -------------------------
|
||||
// SEND EMAIL VIA SMTP
|
||||
@@ -243,14 +260,31 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const mail = new MailComposer(message)
|
||||
const raw = await mail.compile().build()
|
||||
|
||||
let savedToSent = false
|
||||
for await (const mailbox of await imap.list()) {
|
||||
if (mailbox.specialUse === "\\Sent") {
|
||||
await imap.mailboxOpen(mailbox.path)
|
||||
await imap.append(mailbox.path, raw, ["\\Seen"])
|
||||
await imap.logout()
|
||||
savedToSent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!savedToSent) {
|
||||
const sentFallbacks = ["Sent", "Gesendet", "INBOX.Sent"]
|
||||
for (const path of sentFallbacks) {
|
||||
try {
|
||||
await imap.append(path, raw, ["\\Seen"])
|
||||
savedToSent = true
|
||||
break
|
||||
} catch (err) {
|
||||
// Fallback wird nur genutzt, wenn der Ordner existiert.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await imap.logout()
|
||||
|
||||
return reply.send({ success: true })
|
||||
|
||||
} catch (err) {
|
||||
@@ -259,4 +293,80 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/email/accounts/:id/sync", 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; limit?: number }
|
||||
|
||||
const result = await emailSync.syncAccount(
|
||||
req.user.tenant_id,
|
||||
req.user.user_id,
|
||||
id,
|
||||
body,
|
||||
)
|
||||
|
||||
return reply.send({ success: true, ...result })
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mail Sync fehlgeschlagen" })
|
||||
}
|
||||
})
|
||||
|
||||
server.get("/email/accounts/:id/mailboxes", 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 }
|
||||
return reply.send(await emailSync.listMailboxes(req.user.tenant_id, req.user.user_id, id))
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "Postfächer konnten nicht geladen werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.get("/email/accounts/:id/messages", 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 query = req.query as { mailbox?: string; limit?: string }
|
||||
|
||||
return reply.send(await emailSync.listMessages(
|
||||
req.user.tenant_id,
|
||||
req.user.user_id,
|
||||
id,
|
||||
query.mailbox || "INBOX",
|
||||
Number(query.limit || 50),
|
||||
))
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mails konnten nicht geladen werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.get("/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 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)
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mail konnte nicht geladen werden" })
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user