diff --git a/apps/api/src/dkv/dkv.controller.ts b/apps/api/src/dkv/dkv.controller.ts index fb7e798..391fe2d 100644 --- a/apps/api/src/dkv/dkv.controller.ts +++ b/apps/api/src/dkv/dkv.controller.ts @@ -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; } /** diff --git a/apps/api/src/dkv/providers/imap.provider.ts b/apps/api/src/dkv/providers/imap.provider.ts index 893fd78..ae17abf 100644 --- a/apps/api/src/dkv/providers/imap.provider.ts +++ b/apps/api/src/dkv/providers/imap.provider.ts @@ -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 = { 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 { diff --git a/apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx b/apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx index c865bc3..65d4145 100644 --- a/apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx +++ b/apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/InboxConfigForm.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useTranslations } from 'next-intl'; import { type DkvConfig, + checkNow, fetchConfig, saveConfig, testConnection, @@ -154,14 +155,24 @@ export function InboxConfigForm() { const [saveError, setSaveError] = useState(null); const [saveSuccess, setSaveSuccess] = useState(false); + // Check-now state + const [isChecking, setIsChecking] = useState(false); + const [checkResult, setCheckResult] = useState<{ status: string; message: string } | null>(null); + + // Whether config exists in DB (null = not yet saved) + const [configExists, setConfigExists] = useState(false); + // Load config on mount useEffect(() => { fetchConfig() .then((config) => { - setForm(configToForm(config)); + if (config) { + setForm(configToForm(config)); + setConfigExists(true); + } }) .catch(() => { - // Keep defaults if config not yet created + // Keep defaults if fetch failed }) .finally(() => { setIsLoading(false); @@ -201,6 +212,7 @@ export function InboxConfigForm() { try { await saveConfig(formToPayload(form)); setSaveSuccess(true); + setConfigExists(true); // Clear password field after save (T-07-12) setForm((f) => ({ ...f, password: '' })); } catch (err) { @@ -212,6 +224,22 @@ export function InboxConfigForm() { } }; + const handleCheckNow = async () => { + setIsChecking(true); + setCheckResult(null); + try { + const result = await checkNow(); + setCheckResult(result); + } catch (err) { + setCheckResult({ + status: 'error', + message: err instanceof Error ? err.message : 'Fehler beim Abrufen', + }); + } finally { + setIsChecking(false); + } + }; + const inputCls = 'h-9 w-full max-w-md rounded border border-border bg-background px-3 text-sm text-foreground'; const labelCls = 'mb-1 block text-sm text-foreground'; @@ -231,6 +259,12 @@ export function InboxConfigForm() { return (
+ {/* Banner: config not yet saved */} + {!configExists && ( +
+ Noch nicht gespeichert — Konfiguration ausfüllen und speichern. +
+ )} {/* Protokoll */}
+ {/* Check-now result */} + {checkResult && ( +

+ {checkResult.status === 'ok' ? `✓ ${checkResult.message}` : `✗ ${checkResult.message}`} +

+ )} + {/* Inline test feedback */} {testResult !== null && (
diff --git a/apps/web/src/lib/dkv-api.ts b/apps/web/src/lib/dkv-api.ts index 60c60ce..682b9a0 100644 --- a/apps/web/src/lib/dkv-api.ts +++ b/apps/web/src/lib/dkv-api.ts @@ -95,10 +95,11 @@ export interface DkvHistoryPage { * Fetch the current DKV module inbox configuration. * GET /dkv/config */ -export async function fetchConfig(): Promise { +export async function fetchConfig(): Promise { const res = await fetch(`${API_URL}/dkv/config`, { credentials: 'include', }); + if (res.status === 404) return null; if (!res.ok) throw new Error('Failed to fetch DKV config'); return res.json(); } @@ -143,12 +144,13 @@ export async function testConnection( * Trigger an immediate inbox check (manual poll). * POST /dkv/check-now */ -export async function checkNow(): Promise { +export async function checkNow(): Promise<{ status: string; message: string; checkedAt: string }> { const res = await fetch(`${API_URL}/dkv/check-now`, { method: 'POST', credentials: 'include', }); if (!res.ok) throw new Error('Failed to trigger DKV inbox check'); + return res.json(); } /**