diff --git a/package.json b/package.json index c657240..6b2b772 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "handlebars": "^4.7.8", "imapflow": "^1.1.1", "jsonwebtoken": "^9.0.2", + "mailparser": "^3.9.0", "nodemailer": "^7.0.6", "openai": "^6.10.0", "pdf-lib": "^1.17.1", diff --git a/src/modules/cron/dokuboximport.service.ts b/src/modules/cron/dokuboximport.service.ts new file mode 100644 index 0000000..dd1aaec --- /dev/null +++ b/src/modules/cron/dokuboximport.service.ts @@ -0,0 +1,259 @@ +import axios from "axios" +import dayjs from "dayjs" +import { ImapFlow } from "imapflow" +import { simpleParser } from "mailparser" +import { FastifyInstance } from "fastify" + +import {saveFile} from "../../utils/files"; +import { secrets } from "../../utils/secrets" + +// Drizzle Imports +import { + tenants, + folders, + filetags, +} from "../../../db/schema" + +import { + eq, + and, +} from "drizzle-orm" + +let badMessageDetected = false +let badMessageMessageSent = false + +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") + + 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() + } + } + + + +// ------------------------------------------------------------- +// 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 + } +}