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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }>;
|
||||
}
|
||||
|
||||
+17
-16
@@ -303,22 +303,23 @@ export function InboxConfigForm() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* IMAP-only: Ordner */}
|
||||
{form.protocol === 'imap' && (
|
||||
<div>
|
||||
<label htmlFor="dkv-folder" className={labelCls}>
|
||||
{t('form.folder')} *
|
||||
</label>
|
||||
<input
|
||||
id="dkv-folder"
|
||||
type="text"
|
||||
required
|
||||
className={inputCls}
|
||||
value={form.folder}
|
||||
onChange={(e) => update('folder', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Ordner / Postfach */}
|
||||
<div>
|
||||
<label htmlFor="dkv-folder" className={labelCls}>
|
||||
{t('form.folder')}
|
||||
</label>
|
||||
<input
|
||||
id="dkv-folder"
|
||||
type="text"
|
||||
placeholder={form.protocol === 'exchange' ? 'Inbox' : 'INBOX'}
|
||||
className={inputCls}
|
||||
value={form.folder}
|
||||
onChange={(e) => update('folder', e.target.value)}
|
||||
/>
|
||||
{form.protocol === 'exchange' && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">{t('form.folderExchangeHelp')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Exchange-only: Domain */}
|
||||
{form.protocol === 'exchange' && (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user