fix(07): propagate Exchange connection error, add folder selection for Exchange
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 09:33:13 +02:00
parent 01ff137f43
commit 9b453e3ed3
8 changed files with 68 additions and 49 deletions
+1 -2
View File
@@ -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 ───────────────────────────────────────────────────────────────
+1 -1
View File
@@ -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<boolean> {
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
@@ -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<boolean> {
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<string, string> = {
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;
}
}
+6 -7
View File
@@ -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<boolean> {
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 };
}
}
@@ -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<boolean>;
testConnection(config: InboxConfig): Promise<{ success: boolean; message?: string }>;
}