From d2b70e588379b5bf24c153ae11d99e97393b5779 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Thu, 22 Jan 2026 17:05:22 +0100 Subject: [PATCH] Added Dokubox Sync Service and Button Fix #12 --- .../src/modules/cron/dokuboximport.service.ts | 408 +++++++++--------- backend/src/plugins/services.ts | 6 +- backend/src/routes/functions.ts | 5 + frontend/pages/files/index.vue | 40 ++ 4 files changed, 260 insertions(+), 199 deletions(-) diff --git a/backend/src/modules/cron/dokuboximport.service.ts b/backend/src/modules/cron/dokuboximport.service.ts index dd1aaec..e1222a6 100644 --- a/backend/src/modules/cron/dokuboximport.service.ts +++ b/backend/src/modules/cron/dokuboximport.service.ts @@ -27,233 +27,249 @@ let client: ImapFlow | null = null // ------------------------------------------------------------- // IMAP CLIENT INITIALIZEN // ------------------------------------------------------------- -export async function initDokuboxClient() { - client = new ImapFlow({ - host: secrets.DOKUBOX_IMAP_HOST, - port: secrets.DOKUBOX_IMAP_PORT, - secure: secrets.DOKUBOX_IMAP_SECURE, - auth: { - user: secrets.DOKUBOX_IMAP_USER, - pass: secrets.DOKUBOX_IMAP_PASSWORD - }, - logger: false - }) - console.log("Dokubox E-Mail Client Initialized") - - await client.connect() -} // ------------------------------------------------------------- // MAIN SYNC FUNCTION (DRIZZLE VERSION) // ------------------------------------------------------------- -export const syncDokubox = (server: FastifyInstance) => - async () => { - console.log("Perform Dokubox Sync") +export function syncDokuboxService (server: FastifyInstance) { + async function initDokuboxClient() { + client = new ImapFlow({ + host: secrets.DOKUBOX_IMAP_HOST, + port: secrets.DOKUBOX_IMAP_PORT, + secure: secrets.DOKUBOX_IMAP_SECURE, + auth: { + user: secrets.DOKUBOX_IMAP_USER, + pass: secrets.DOKUBOX_IMAP_PASSWORD + }, + logger: false + }) - await initDokuboxClient() + console.log("Dokubox E-Mail Client Initialized") - if (!client?.usable) { - throw new Error("E-Mail Client not usable") + await client.connect() + } + + const syncDokubox = async () => { + + console.log("Perform Dokubox Sync") + + await initDokuboxClient() + + if (!client?.usable) { + throw new Error("E-Mail Client not usable") + } + + // ------------------------------- + // TENANTS LADEN (DRIZZLE) + // ------------------------------- + const tenantList = await server.db + .select({ + id: tenants.id, + name: tenants.name, + emailAddresses: tenants.dokuboxEmailAddresses, + key: tenants.dokuboxkey + }) + .from(tenants) + + const lock = await client.getMailboxLock("INBOX") + + try { + + for await (let msg of client.fetch({ seen: false }, { envelope: true, source: true })) { + + const parsed = await simpleParser(msg.source) + + const message = { + id: msg.uid, + subject: parsed.subject, + to: parsed.to?.value || [], + cc: parsed.cc?.value || [], + attachments: parsed.attachments || [] + } + + // ------------------------------------------------- + // MAPPING / FIND TENANT + // ------------------------------------------------- + const config = await getMessageConfigDrizzle(server, message, tenantList) + + if (!config) { + badMessageDetected = true + + if (!badMessageMessageSent) { + badMessageMessageSent = true + } + return + } + + if (message.attachments.length > 0) { + for (const attachment of message.attachments) { + await saveFile( + server, + config.tenant, + message.id, + attachment, + config.folder, + config.filetype + ) + } + } + } + + if (!badMessageDetected) { + badMessageDetected = false + badMessageMessageSent = false + } + + await client.messageFlagsAdd({ seen: false }, ["\\Seen"]) + await client.messageDelete({ seen: true }) + + } finally { + lock.release() + client.close() + } } - // ------------------------------- - // TENANTS LADEN (DRIZZLE) - // ------------------------------- - const tenantList = await server.db - .select({ - id: tenants.id, - name: tenants.name, - emailAddresses: tenants.dokuboxEmailAddresses, - key: tenants.dokuboxkey - }) - .from(tenants) + const getMessageConfigDrizzle = async ( + server: FastifyInstance, + message, + tenantsList: any[] + ) => { - const lock = await client.getMailboxLock("INBOX") + let possibleKeys: string[] = [] - try { + if (message.to) { + message.to.forEach((item) => + possibleKeys.push(item.address.split("@")[0].toLowerCase()) + ) + } - for await (let msg of client.fetch({ seen: false }, { envelope: true, source: true })) { + if (message.cc) { + message.cc.forEach((item) => + possibleKeys.push(item.address.split("@")[0].toLowerCase()) + ) + } - const parsed = await simpleParser(msg.source) + // ------------------------------------------- + // TENANT IDENTIFY + // ------------------------------------------- + let tenant = tenantsList.find((t) => possibleKeys.includes(t.key)) - const message = { - id: msg.uid, - subject: parsed.subject, - to: parsed.to?.value || [], - cc: parsed.cc?.value || [], - attachments: parsed.attachments || [] - } + if (!tenant && message.to?.length) { + const address = message.to[0].address.toLowerCase() - // ------------------------------------------------- - // MAPPING / FIND TENANT - // ------------------------------------------------- - const config = await getMessageConfigDrizzle(server, message, tenantList) + tenant = tenantsList.find((t) => + (t.emailAddresses || []).map((m) => m.toLowerCase()).includes(address) + ) + } - if (!config) { - badMessageDetected = true + if (!tenant) return null - if (!badMessageMessageSent) { - badMessageMessageSent = true - } - return - } + // ------------------------------------------- + // FOLDER + FILETYPE VIA SUBJECT + // ------------------------------------------- + let folderId = null + let filetypeId = null - if (message.attachments.length > 0) { - for (const attachment of message.attachments) { - await saveFile( - server, - config.tenant, - message.id, - attachment, - config.folder, - config.filetype + // ------------------------------------------- + // Rechnung / Invoice + // ------------------------------------------- + if (message.subject?.match(/(Rechnung|Beleg|Invoice|Quittung)/gi)) { + + const folder = await server.db + .select({ id: folders.id }) + .from(folders) + .where( + and( + eq(folders.tenant, tenant.id), + and( + eq(folders.function, "incomingInvoices"), + //@ts-ignore + eq(folders.year, dayjs().format("YYYY")) ) - } - } - } + ) + ) + .limit(1) - if (!badMessageDetected) { - badMessageDetected = false - badMessageMessageSent = false - } + folderId = folder[0]?.id ?? null - await client.messageFlagsAdd({ seen: false }, ["\\Seen"]) - await client.messageDelete({ seen: true }) + const tag = await server.db + .select({ id: filetags.id }) + .from(filetags) + .where( + and( + eq(filetags.tenant, tenant.id), + eq(filetags.incomingDocumentType, "invoices") + ) + ) + .limit(1) - } finally { - lock.release() - client.close() + filetypeId = tag[0]?.id ?? null + } + + // ------------------------------------------- + // Mahnung + // ------------------------------------------- + else if (message.subject?.match(/(Mahnung|Zahlungsaufforderung|Zahlungsverzug)/gi)) { + + const tag = await server.db + .select({ id: filetags.id }) + .from(filetags) + .where( + and( + eq(filetags.tenant, tenant.id), + eq(filetags.incomingDocumentType, "reminders") + ) + ) + .limit(1) + + filetypeId = tag[0]?.id ?? null + } + + // ------------------------------------------- + // Sonstige Dokumente → Deposit Folder + // ------------------------------------------- + else { + + const folder = await server.db + .select({ id: folders.id }) + .from(folders) + .where( + and( + eq(folders.tenant, tenant.id), + eq(folders.function, "deposit") + ) + ) + .limit(1) + + folderId = folder[0]?.id ?? null + } + + + return { + tenant: tenant.id, + folder: folderId, + filetype: filetypeId } } + return { + run: async () => { + await initDokuboxClient() + await syncDokubox() + console.log("Service: Dokubox sync finished") + } + } +} + + + // ------------------------------------------------------------- // TENANT ERKENNEN + FOLDER/FILETYPES (DRIZZLE VERSION) // ------------------------------------------------------------- -const getMessageConfigDrizzle = async ( - server: FastifyInstance, - message, - tenantsList: any[] -) => { - let possibleKeys: string[] = [] - - if (message.to) { - message.to.forEach((item) => - possibleKeys.push(item.address.split("@")[0].toLowerCase()) - ) - } - - if (message.cc) { - message.cc.forEach((item) => - possibleKeys.push(item.address.split("@")[0].toLowerCase()) - ) - } - - // ------------------------------------------- - // TENANT IDENTIFY - // ------------------------------------------- - let tenant = tenantsList.find((t) => possibleKeys.includes(t.key)) - - if (!tenant && message.to?.length) { - const address = message.to[0].address.toLowerCase() - - tenant = tenantsList.find((t) => - (t.emailAddresses || []).map((m) => m.toLowerCase()).includes(address) - ) - } - - if (!tenant) return null - - // ------------------------------------------- - // FOLDER + FILETYPE VIA SUBJECT - // ------------------------------------------- - let folderId = null - let filetypeId = null - - // ------------------------------------------- - // Rechnung / Invoice - // ------------------------------------------- - if (message.subject?.match(/(Rechnung|Beleg|Invoice|Quittung)/gi)) { - - const folder = await server.db - .select({ id: folders.id }) - .from(folders) - .where( - and( - eq(folders.tenant, tenant.id), - and( - eq(folders.function, "incomingInvoices"), - //@ts-ignore - eq(folders.year, dayjs().format("YYYY")) - ) - ) - ) - .limit(1) - - folderId = folder[0]?.id ?? null - - const tag = await server.db - .select({ id: filetags.id }) - .from(filetags) - .where( - and( - eq(filetags.tenant, tenant.id), - eq(filetags.incomingDocumentType, "invoices") - ) - ) - .limit(1) - - filetypeId = tag[0]?.id ?? null - } - - // ------------------------------------------- - // Mahnung - // ------------------------------------------- - else if (message.subject?.match(/(Mahnung|Zahlungsaufforderung|Zahlungsverzug)/gi)) { - - const tag = await server.db - .select({ id: filetags.id }) - .from(filetags) - .where( - and( - eq(filetags.tenant, tenant.id), - eq(filetags.incomingDocumentType, "reminders") - ) - ) - .limit(1) - - filetypeId = tag[0]?.id ?? null - } - - // ------------------------------------------- - // Sonstige Dokumente → Deposit Folder - // ------------------------------------------- - else { - - const folder = await server.db - .select({ id: folders.id }) - .from(folders) - .where( - and( - eq(folders.tenant, tenant.id), - eq(folders.function, "deposit") - ) - ) - .limit(1) - - folderId = folder[0]?.id ?? null - } - - - return { - tenant: tenant.id, - folder: folderId, - filetype: filetypeId - } -} diff --git a/backend/src/plugins/services.ts b/backend/src/plugins/services.ts index c5788b7..e170c49 100644 --- a/backend/src/plugins/services.ts +++ b/backend/src/plugins/services.ts @@ -1,7 +1,7 @@ // /plugins/services.ts import fp from "fastify-plugin"; import { bankStatementService } from "../modules/cron/bankstatementsync.service"; -//import {initDokuboxClient, syncDokubox} from "../modules/cron/dokuboximport.service"; +import {syncDokuboxService} from "../modules/cron/dokuboximport.service"; import { FastifyInstance } from "fastify"; import {prepareIncomingInvoices} from "../modules/cron/prepareIncomingInvoices"; @@ -9,7 +9,7 @@ declare module "fastify" { interface FastifyInstance { services: { bankStatements: ReturnType; - //dokuboxSync: ReturnType; + dokuboxSync: ReturnType; prepareIncomingInvoices: ReturnType; }; } @@ -18,7 +18,7 @@ declare module "fastify" { export default fp(async function servicePlugin(server: FastifyInstance) { server.decorate("services", { bankStatements: bankStatementService(server), - //dokuboxSync: syncDokubox(server), + dokuboxSync: syncDokuboxService(server), prepareIncomingInvoices: prepareIncomingInvoices(server), }); }); diff --git a/backend/src/routes/functions.ts b/backend/src/routes/functions.ts index d37963b..c39da62 100644 --- a/backend/src/routes/functions.ts +++ b/backend/src/routes/functions.ts @@ -179,6 +179,11 @@ export default async function functionRoutes(server: FastifyInstance) { await server.services.prepareIncomingInvoices.run(req.user.tenant_id) }) + server.post('/functions/services/syncdokubox', async (req, reply) => { + + await server.services.dokuboxSync.run() + }) + /*server.post('/print/zpl/preview', async (req, reply) => { const { zpl, widthMm = 50, heightMm = 30, dpmm = 8, asBase64 = false } = req.body as {zpl:string,widthMm:number,heightMm:number,dpmm:number,asBase64:string} diff --git a/frontend/pages/files/index.vue b/frontend/pages/files/index.vue index 2e49088..2d57146 100644 --- a/frontend/pages/files/index.vue +++ b/frontend/pages/files/index.vue @@ -11,6 +11,7 @@ const router = useRouter() const route = useRoute() const modal = useModal() const toast = useToast() +const { $api } = useNuxtApp() const files = useFiles() // --- State --- @@ -222,11 +223,50 @@ defineShortcuts({ 'arrowdown': () => { if (selectedFileIndex.value < renderedFileList.value.length - 1) selectedFileIndex.value++ }, 'arrowup': () => { if (selectedFileIndex.value > 0) selectedFileIndex.value-- } }) + +const isSyncing = ref(false) + +const syncdokubox = async () => { + isSyncing.value = true + try { + await $api('/api/functions/services/syncdokubox', { method: 'POST' }) + + toast.add({ + title: 'Erfolg', + description: 'Dokubox wurde synchronisiert.', + icon: 'i-heroicons-check-circle', + color: 'green' + }) + + // Liste neu laden + await setupPage() + } catch (error) { + console.error(error) + toast.add({ + title: 'Fehler', + description: 'Beim Synchronisieren der Dokubox ist ein Fehler aufgetreten.', + icon: 'i-heroicons-exclamation-circle', + color: 'red' + }) + } finally { + isSyncing.value = false + } +} +