From 9b453e3ed3d9358bc6dac9eb3f784faf2822ed44 Mon Sep 17 00:00:00 2001 From: Schalli Date: Mon, 29 Jun 2026 09:33:13 +0200 Subject: [PATCH] fix(07): propagate Exchange connection error, add folder selection for Exchange - testConnection now returns { success, message? } instead of boolean so admins see the actual EWS/IMAP error in the UI rather than "Unbekannter Fehler" - Exchange provider: resolveFolder() maps folder string to WellKnownFolderName (Inbox, SentItems, DeletedItems, Drafts, JunkEmail + German aliases) - InboxConfigForm: folder field now shown for both IMAP and Exchange protocols with Exchange-specific help text listing valid well-known names - Controller returns testConnection result directly (no more redundant wrapping) Co-Authored-By: Claude Sonnet 4.6 --- apps/api/src/dkv/dkv.controller.ts | 3 +- apps/api/src/dkv/dkv.service.ts | 2 +- .../dkv/providers/exchange-inbox.provider.ts | 56 ++++++++++++------- apps/api/src/dkv/providers/imap.provider.ts | 13 ++--- .../dkv/providers/inbox-provider.interface.ts | 4 +- .../settings/components/InboxConfigForm.tsx | 33 +++++------ apps/web/src/messages/de.json | 3 +- apps/web/src/messages/en.json | 3 +- 8 files changed, 68 insertions(+), 49 deletions(-) diff --git a/apps/api/src/dkv/dkv.controller.ts b/apps/api/src/dkv/dkv.controller.ts index c3f8466..fb7e798 100644 --- a/apps/api/src/dkv/dkv.controller.ts +++ b/apps/api/src/dkv/dkv.controller.ts @@ -103,8 +103,7 @@ export class DkvController { @Roles(Role.ADMIN, Role.SUPER_ADMIN) async testConnection(@Req() req: any, @Body() dto: DkvConfigDto) { const tenantId = this._requireTenant(req); - const success = await this.dkvService.testConnection(tenantId, dto); - return { success }; + return this.dkvService.testConnection(tenantId, dto); } // ─── History ─────────────────────────────────────────────────────────────── diff --git a/apps/api/src/dkv/dkv.service.ts b/apps/api/src/dkv/dkv.service.ts index 89bb356..8c821fc 100644 --- a/apps/api/src/dkv/dkv.service.ts +++ b/apps/api/src/dkv/dkv.service.ts @@ -175,7 +175,7 @@ export class DkvService { * * T-05-13: Decrypted password used only within this method scope — never logged. */ - async testConnection(tenantId: string, dto: DkvConfigDto): Promise { + async testConnection(tenantId: string, dto: DkvConfigDto): Promise<{ success: boolean; message?: string }> { let password: string | undefined = dto.password; // If no password in DTO, fall back to the stored one diff --git a/apps/api/src/dkv/providers/exchange-inbox.provider.ts b/apps/api/src/dkv/providers/exchange-inbox.provider.ts index f618f93..a5673e6 100644 --- a/apps/api/src/dkv/providers/exchange-inbox.provider.ts +++ b/apps/api/src/dkv/providers/exchange-inbox.provider.ts @@ -54,22 +54,22 @@ export class ExchangeInboxProvider implements InboxProvider { * @param config Decrypted inbox connection parameters * @returns true on success, false on any auth/network failure */ - async testConnection(config: InboxConfig): Promise { + async testConnection(config: InboxConfig): Promise<{ success: boolean; message?: string }> { try { // Dynamic import — ews-javascript-api is JS-only, no .d.ts (follows ExchangeProvider pattern) const ews: any = await import('ews-javascript-api'); const service = this.buildService(ews, config); - // Minimal FindItems on Inbox — just confirm we can connect + // Minimal FindItems — just confirm we can connect const itemView = new ews.ItemView(1); - await service.FindItems(ews.WellKnownFolderName.Inbox, itemView); - return true; + const folder = this.resolveFolder(ews, config.folder); + await service.FindItems(folder, itemView); + return { success: true }; } catch (err) { - // T-07-03: generic error message — no credential details - this.logger.error( - `EWS connection test failed: ${(err as Error).message}`, - ); - return false; + const message = (err as Error).message; + // T-07-03: log without credentials; return message to admin for diagnosis + this.logger.error(`EWS connection test failed: ${message}`); + return { success: false, message }; } } @@ -83,9 +83,10 @@ export class ExchangeInboxProvider implements InboxProvider { const ews: any = await import('ews-javascript-api'); const service = this.buildService(ews, config); - // Pitfall 6: use WellKnownFolderName.Inbox with FindItems (NOT FindAppointments) + // Pitfall 6: use WellKnownFolderName with FindItems (NOT FindAppointments) // FindAppointments is Calendar-only — inbox emails use FindItems const itemView = new ews.ItemView(50); + const folder = this.resolveFolder(ews, config.folder); let findResults: any; if (config.senderFilter) { @@ -94,16 +95,9 @@ export class ExchangeInboxProvider implements InboxProvider { ews.EmailMessageSchema.From, config.senderFilter, ); - findResults = await service.FindItems( - ews.WellKnownFolderName.Inbox, - senderFilter, - itemView, - ); + findResults = await service.FindItems(folder, senderFilter, itemView); } else { - findResults = await service.FindItems( - ews.WellKnownFolderName.Inbox, - itemView, - ); + findResults = await service.FindItems(folder, itemView); } const results: InboxEmail[] = []; @@ -226,4 +220,28 @@ export class ExchangeInboxProvider implements InboxProvider { ); return service; } + + /** + * Maps a folder name string to an EWS WellKnownFolderName enum value. + * Supports common German and English names. Defaults to Inbox. + */ + private resolveFolder(ews: any, folder?: string): any { + const name = (folder ?? 'INBOX').toLowerCase().replace(/[\s_-]/g, ''); + const map: Record = { + inbox: 'Inbox', + posteingang: 'Inbox', + deleteditems: 'DeletedItems', + gelöschteelemente: 'DeletedItems', + trash: 'DeletedItems', + sentitems: 'SentItems', + gesendet: 'SentItems', + drafts: 'Drafts', + entwürfe: 'Drafts', + junk: 'JunkEmail', + junkemail: 'JunkEmail', + spam: 'JunkEmail', + }; + const key = map[name] ?? 'Inbox'; + return ews.WellKnownFolderName[key] ?? ews.WellKnownFolderName.Inbox; + } } diff --git a/apps/api/src/dkv/providers/imap.provider.ts b/apps/api/src/dkv/providers/imap.provider.ts index f039b39..893fd78 100644 --- a/apps/api/src/dkv/providers/imap.provider.ts +++ b/apps/api/src/dkv/providers/imap.provider.ts @@ -188,18 +188,17 @@ export class ImapProvider implements InboxProvider { * @param config Decrypted inbox connection parameters * @returns true on success, false on any network/auth failure */ - async testConnection(config: InboxConfig): Promise { + async testConnection(config: InboxConfig): Promise<{ success: boolean; message?: string }> { const client = this.buildClient(config); try { await client.connect(); await client.logout(); - return true; + return { success: true }; } catch (err) { - // T-07-03: generic error message — no credential details - this.logger.error( - `IMAP connection test failed: ${(err as Error).message}`, - ); - return false; + const message = (err as Error).message; + // T-07-03: log without credentials; return message to admin for diagnosis + this.logger.error(`IMAP connection test failed: ${message}`); + return { success: false, message }; } } diff --git a/apps/api/src/dkv/providers/inbox-provider.interface.ts b/apps/api/src/dkv/providers/inbox-provider.interface.ts index 4a14a5c..e372da7 100644 --- a/apps/api/src/dkv/providers/inbox-provider.interface.ts +++ b/apps/api/src/dkv/providers/inbox-provider.interface.ts @@ -26,7 +26,7 @@ export interface InboxProvider { * Tests whether the inbox connection can be established. * * @param config Decrypted inbox connection parameters - * @returns true if connection succeeded, false on any auth/network error + * @returns { success: true } on success, { success: false, message } on failure */ - testConnection(config: InboxConfig): Promise; + testConnection(config: InboxConfig): Promise<{ success: boolean; message?: string }>; } 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 f44614d..8784ac6 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 @@ -303,22 +303,23 @@ export function InboxConfigForm() { )} - {/* IMAP-only: Ordner */} - {form.protocol === 'imap' && ( -
- - update('folder', e.target.value)} - /> -
- )} + {/* Ordner / Postfach */} +
+ + update('folder', e.target.value)} + /> + {form.protocol === 'exchange' && ( +

{t('form.folderExchangeHelp')}

+ )} +
{/* Exchange-only: Domain */} {form.protocol === 'exchange' && ( diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index d92d640..eeeee96 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -318,7 +318,8 @@ "port": "Port", "encryption": "Verschluesselung", "encryptionNone": "Keine", - "folder": "Ordner", + "folder": "Ordner / Postfach", + "folderExchangeHelp": "Standard-Postfaecher: Inbox, SentItems, DeletedItems, Drafts, JunkEmail", "senderFilter": "Absenderfilter", "pollInterval": "Abrufintervall", "pollIntervalSuffix": "Minuten", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index f8a96b7..9523932 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -318,7 +318,8 @@ "port": "Port", "encryption": "Encryption", "encryptionNone": "None", - "folder": "Folder", + "folder": "Folder / Mailbox", + "folderExchangeHelp": "Common mailboxes: Inbox, SentItems, DeletedItems, Drafts, JunkEmail", "senderFilter": "Sender Filter", "pollInterval": "Poll Interval", "pollIntervalSuffix": "Minutes",