Added Dokubox Sync Service and Button Fix #12
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 30s
Build and Push Docker Images / build-frontend (push) Successful in 1m10s

This commit is contained in:
2026-01-22 17:05:22 +01:00
parent 1a065b649c
commit d2b70e5883
4 changed files with 260 additions and 199 deletions

View File

@@ -27,233 +27,249 @@ let client: ImapFlow | null = null
// ------------------------------------------------------------- // -------------------------------------------------------------
// IMAP CLIENT INITIALIZEN // 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) // 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) { await client.connect()
throw new Error("E-Mail Client not usable") }
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()
}
} }
// ------------------------------- const getMessageConfigDrizzle = async (
// TENANTS LADEN (DRIZZLE) server: FastifyInstance,
// ------------------------------- message,
const tenantList = await server.db tenantsList: any[]
.select({ ) => {
id: tenants.id,
name: tenants.name,
emailAddresses: tenants.dokuboxEmailAddresses,
key: tenants.dokuboxkey
})
.from(tenants)
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 = { if (!tenant && message.to?.length) {
id: msg.uid, const address = message.to[0].address.toLowerCase()
subject: parsed.subject,
to: parsed.to?.value || [],
cc: parsed.cc?.value || [],
attachments: parsed.attachments || []
}
// ------------------------------------------------- tenant = tenantsList.find((t) =>
// MAPPING / FIND TENANT (t.emailAddresses || []).map((m) => m.toLowerCase()).includes(address)
// ------------------------------------------------- )
const config = await getMessageConfigDrizzle(server, message, tenantList) }
if (!config) { if (!tenant) return null
badMessageDetected = true
if (!badMessageMessageSent) { // -------------------------------------------
badMessageMessageSent = true // FOLDER + FILETYPE VIA SUBJECT
} // -------------------------------------------
return let folderId = null
} let filetypeId = null
if (message.attachments.length > 0) { // -------------------------------------------
for (const attachment of message.attachments) { // Rechnung / Invoice
await saveFile( // -------------------------------------------
server, if (message.subject?.match(/(Rechnung|Beleg|Invoice|Quittung)/gi)) {
config.tenant,
message.id, const folder = await server.db
attachment, .select({ id: folders.id })
config.folder, .from(folders)
config.filetype .where(
and(
eq(folders.tenant, tenant.id),
and(
eq(folders.function, "incomingInvoices"),
//@ts-ignore
eq(folders.year, dayjs().format("YYYY"))
) )
} )
} )
} .limit(1)
if (!badMessageDetected) { folderId = folder[0]?.id ?? null
badMessageDetected = false
badMessageMessageSent = false
}
await client.messageFlagsAdd({ seen: false }, ["\\Seen"]) const tag = await server.db
await client.messageDelete({ seen: true }) .select({ id: filetags.id })
.from(filetags)
.where(
and(
eq(filetags.tenant, tenant.id),
eq(filetags.incomingDocumentType, "invoices")
)
)
.limit(1)
} finally { filetypeId = tag[0]?.id ?? null
lock.release() }
client.close()
// -------------------------------------------
// 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) // 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
}
}

View File

@@ -1,7 +1,7 @@
// /plugins/services.ts // /plugins/services.ts
import fp from "fastify-plugin"; import fp from "fastify-plugin";
import { bankStatementService } from "../modules/cron/bankstatementsync.service"; 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 { FastifyInstance } from "fastify";
import {prepareIncomingInvoices} from "../modules/cron/prepareIncomingInvoices"; import {prepareIncomingInvoices} from "../modules/cron/prepareIncomingInvoices";
@@ -9,7 +9,7 @@ declare module "fastify" {
interface FastifyInstance { interface FastifyInstance {
services: { services: {
bankStatements: ReturnType<typeof bankStatementService>; bankStatements: ReturnType<typeof bankStatementService>;
//dokuboxSync: ReturnType<typeof syncDokubox>; dokuboxSync: ReturnType<typeof syncDokuboxService>;
prepareIncomingInvoices: ReturnType<typeof prepareIncomingInvoices>; prepareIncomingInvoices: ReturnType<typeof prepareIncomingInvoices>;
}; };
} }
@@ -18,7 +18,7 @@ declare module "fastify" {
export default fp(async function servicePlugin(server: FastifyInstance) { export default fp(async function servicePlugin(server: FastifyInstance) {
server.decorate("services", { server.decorate("services", {
bankStatements: bankStatementService(server), bankStatements: bankStatementService(server),
//dokuboxSync: syncDokubox(server), dokuboxSync: syncDokuboxService(server),
prepareIncomingInvoices: prepareIncomingInvoices(server), prepareIncomingInvoices: prepareIncomingInvoices(server),
}); });
}); });

View File

@@ -179,6 +179,11 @@ export default async function functionRoutes(server: FastifyInstance) {
await server.services.prepareIncomingInvoices.run(req.user.tenant_id) 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) => { /*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} const { zpl, widthMm = 50, heightMm = 30, dpmm = 8, asBase64 = false } = req.body as {zpl:string,widthMm:number,heightMm:number,dpmm:number,asBase64:string}

View File

@@ -11,6 +11,7 @@ const router = useRouter()
const route = useRoute() const route = useRoute()
const modal = useModal() const modal = useModal()
const toast = useToast() const toast = useToast()
const { $api } = useNuxtApp()
const files = useFiles() const files = useFiles()
// --- State --- // --- State ---
@@ -222,11 +223,50 @@ defineShortcuts({
'arrowdown': () => { if (selectedFileIndex.value < renderedFileList.value.length - 1) selectedFileIndex.value++ }, 'arrowdown': () => { if (selectedFileIndex.value < renderedFileList.value.length - 1) selectedFileIndex.value++ },
'arrowup': () => { if (selectedFileIndex.value > 0) 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
}
}
</script> </script>
<template> <template>
<UDashboardNavbar title="Dateien"> <UDashboardNavbar title="Dateien">
<template #right> <template #right>
<UButton
label="Dokubox Sync"
icon="i-heroicons-sparkles"
color="primary"
variant="solid"
:loading="isSyncing"
@click="syncdokubox"
class="mr-2"
/>
<UInput id="searchinput" v-model="searchString" icon="i-heroicons-magnifying-glass" placeholder="Suche..." class="w-64" /> <UInput id="searchinput" v-model="searchString" icon="i-heroicons-magnifying-glass" placeholder="Suche..." class="w-64" />
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>