fix(dkv): fix inbox processing pipeline — orphan tenant, MIME detection, UNSEEN filter
Tessera CI/CD / Lint & Type Check (push) Successful in 42s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m37s

- Fix orphaned DkvModuleConfig: tenantId pointed to deleted tenant, updated to Default tenant
- DKV controller: return 404 instead of HTTP 200 null when no config exists
- IMAP provider: also detect PDFs sent as application/octet-stream (check filename extension)
- IMAP provider: add seen:false filter so already-processed emails are skipped on re-poll
- IMAP provider: mark email as \Seen after successful PDF download to prevent reprocessing
- Frontend dkv-api: handle 404 from fetchConfig as "not yet configured" (returns null)
- InboxConfigForm: show warning banner when config not yet saved in DB
- InboxConfigForm: add "Jetzt prüfen" button to manually trigger POST /dkv/check-now

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 08:31:36 +02:00
parent 9a652ea440
commit 9efa3bab1d
4 changed files with 91 additions and 15 deletions
+6 -2
View File
@@ -55,12 +55,16 @@ export class DkvController {
// ─── Config ────────────────────────────────────────────────────────────────
/** GET /dkv/config — returns module config without encrypted credentials. */
/** GET /dkv/config — returns module config without encrypted credentials. 404 when not yet configured. */
@Get('config')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async getConfig(@Req() req: any) {
const tenantId = this._requireTenant(req);
return this.dkvService.loadConfig(tenantId);
const config = await this.dkvService.loadConfig(tenantId);
if (!config) {
throw new NotFoundException('DKV module not yet configured');
}
return config;
}
/**
+25 -9
View File
@@ -56,11 +56,19 @@ function collectPdfParts(
): string[] {
if (!node) return parts;
// Check this node — type is the full MIME type (e.g. "application/pdf")
if (
node.type?.toLowerCase() === 'application/pdf' &&
node.part !== undefined
) {
const type = node.type?.toLowerCase() ?? '';
// Some mail clients (e.g. Outlook) send PDFs as application/octet-stream.
// Fall back to checking the filename from Content-Disposition or Content-Type parameters.
const dispositionFilename =
((node as any).disposition?.parameters?.filename as string | undefined)?.toLowerCase() ?? '';
const typeFilename =
((node as any).parameters?.name as string | undefined)?.toLowerCase() ?? '';
const looksLikePdf =
type === 'application/pdf' ||
(type === 'application/octet-stream' &&
(dispositionFilename.endsWith('.pdf') || typeFilename.endsWith('.pdf')));
if (looksLikePdf && node.part !== undefined) {
parts.push(node.part);
}
@@ -112,10 +120,12 @@ export class ImapProvider implements InboxProvider {
try {
lock = await client.getMailboxLock(config.folder || 'INBOX');
// Search for emails from the configured sender
const searchQuery = config.senderFilter
? { from: config.senderFilter }
: {};
// Search only UNSEEN emails to avoid reprocessing already-handled messages.
// Combine with sender filter when configured.
const searchQuery: Record<string, unknown> = { seen: false };
if (config.senderFilter) {
searchQuery.from = config.senderFilter;
}
const uids = await client.search(searchQuery, { uid: true });
if (!uids || uids.length === 0) {
@@ -171,6 +181,12 @@ export class ImapProvider implements InboxProvider {
date: msg.envelope?.date ?? new Date(),
attachments,
});
// Mark as read so subsequent polls skip this message (UNSEEN filter above).
try {
await client.messageFlagsAdd(String(msg.uid), ['\\Seen'], { uid: true });
} catch {
// Non-fatal: message will simply appear again on next poll
}
}
}
} finally {