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)
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||||
async testConnection(@Req() req: any, @Body() dto: DkvConfigDto) {
|
async testConnection(@Req() req: any, @Body() dto: DkvConfigDto) {
|
||||||
const tenantId = this._requireTenant(req);
|
const tenantId = this._requireTenant(req);
|
||||||
const success = await this.dkvService.testConnection(tenantId, dto);
|
return this.dkvService.testConnection(tenantId, dto);
|
||||||
return { success };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── History ───────────────────────────────────────────────────────────────
|
// ─── History ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export class DkvService {
|
|||||||
*
|
*
|
||||||
* T-05-13: Decrypted password used only within this method scope — never logged.
|
* 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;
|
let password: string | undefined = dto.password;
|
||||||
|
|
||||||
// If no password in DTO, fall back to the stored one
|
// 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
|
* @param config Decrypted inbox connection parameters
|
||||||
* @returns true on success, false on any auth/network failure
|
* @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 {
|
try {
|
||||||
// Dynamic import — ews-javascript-api is JS-only, no .d.ts (follows ExchangeProvider pattern)
|
// Dynamic import — ews-javascript-api is JS-only, no .d.ts (follows ExchangeProvider pattern)
|
||||||
const ews: any = await import('ews-javascript-api');
|
const ews: any = await import('ews-javascript-api');
|
||||||
const service = this.buildService(ews, config);
|
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);
|
const itemView = new ews.ItemView(1);
|
||||||
await service.FindItems(ews.WellKnownFolderName.Inbox, itemView);
|
const folder = this.resolveFolder(ews, config.folder);
|
||||||
return true;
|
await service.FindItems(folder, itemView);
|
||||||
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// T-07-03: generic error message — no credential details
|
const message = (err as Error).message;
|
||||||
this.logger.error(
|
// T-07-03: log without credentials; return message to admin for diagnosis
|
||||||
`EWS connection test failed: ${(err as Error).message}`,
|
this.logger.error(`EWS connection test failed: ${message}`);
|
||||||
);
|
return { success: false, message };
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,9 +83,10 @@ export class ExchangeInboxProvider implements InboxProvider {
|
|||||||
const ews: any = await import('ews-javascript-api');
|
const ews: any = await import('ews-javascript-api');
|
||||||
const service = this.buildService(ews, config);
|
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
|
// FindAppointments is Calendar-only — inbox emails use FindItems
|
||||||
const itemView = new ews.ItemView(50);
|
const itemView = new ews.ItemView(50);
|
||||||
|
const folder = this.resolveFolder(ews, config.folder);
|
||||||
|
|
||||||
let findResults: any;
|
let findResults: any;
|
||||||
if (config.senderFilter) {
|
if (config.senderFilter) {
|
||||||
@@ -94,16 +95,9 @@ export class ExchangeInboxProvider implements InboxProvider {
|
|||||||
ews.EmailMessageSchema.From,
|
ews.EmailMessageSchema.From,
|
||||||
config.senderFilter,
|
config.senderFilter,
|
||||||
);
|
);
|
||||||
findResults = await service.FindItems(
|
findResults = await service.FindItems(folder, senderFilter, itemView);
|
||||||
ews.WellKnownFolderName.Inbox,
|
|
||||||
senderFilter,
|
|
||||||
itemView,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
findResults = await service.FindItems(
|
findResults = await service.FindItems(folder, itemView);
|
||||||
ews.WellKnownFolderName.Inbox,
|
|
||||||
itemView,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: InboxEmail[] = [];
|
const results: InboxEmail[] = [];
|
||||||
@@ -226,4 +220,28 @@ export class ExchangeInboxProvider implements InboxProvider {
|
|||||||
);
|
);
|
||||||
return service;
|
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
|
* @param config Decrypted inbox connection parameters
|
||||||
* @returns true on success, false on any network/auth failure
|
* @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);
|
const client = this.buildClient(config);
|
||||||
try {
|
try {
|
||||||
await client.connect();
|
await client.connect();
|
||||||
await client.logout();
|
await client.logout();
|
||||||
return true;
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// T-07-03: generic error message — no credential details
|
const message = (err as Error).message;
|
||||||
this.logger.error(
|
// T-07-03: log without credentials; return message to admin for diagnosis
|
||||||
`IMAP connection test failed: ${(err as Error).message}`,
|
this.logger.error(`IMAP connection test failed: ${message}`);
|
||||||
);
|
return { success: false, message };
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export interface InboxProvider {
|
|||||||
* Tests whether the inbox connection can be established.
|
* Tests whether the inbox connection can be established.
|
||||||
*
|
*
|
||||||
* @param config Decrypted inbox connection parameters
|
* @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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* IMAP-only: Ordner */}
|
{/* Ordner / Postfach */}
|
||||||
{form.protocol === 'imap' && (
|
<div>
|
||||||
<div>
|
<label htmlFor="dkv-folder" className={labelCls}>
|
||||||
<label htmlFor="dkv-folder" className={labelCls}>
|
{t('form.folder')}
|
||||||
{t('form.folder')} *
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
id="dkv-folder"
|
||||||
id="dkv-folder"
|
type="text"
|
||||||
type="text"
|
placeholder={form.protocol === 'exchange' ? 'Inbox' : 'INBOX'}
|
||||||
required
|
className={inputCls}
|
||||||
className={inputCls}
|
value={form.folder}
|
||||||
value={form.folder}
|
onChange={(e) => update('folder', e.target.value)}
|
||||||
onChange={(e) => update('folder', e.target.value)}
|
/>
|
||||||
/>
|
{form.protocol === 'exchange' && (
|
||||||
</div>
|
<p className="mt-1 text-xs text-muted-foreground">{t('form.folderExchangeHelp')}</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Exchange-only: Domain */}
|
{/* Exchange-only: Domain */}
|
||||||
{form.protocol === 'exchange' && (
|
{form.protocol === 'exchange' && (
|
||||||
|
|||||||
@@ -318,7 +318,8 @@
|
|||||||
"port": "Port",
|
"port": "Port",
|
||||||
"encryption": "Verschluesselung",
|
"encryption": "Verschluesselung",
|
||||||
"encryptionNone": "Keine",
|
"encryptionNone": "Keine",
|
||||||
"folder": "Ordner",
|
"folder": "Ordner / Postfach",
|
||||||
|
"folderExchangeHelp": "Standard-Postfaecher: Inbox, SentItems, DeletedItems, Drafts, JunkEmail",
|
||||||
"senderFilter": "Absenderfilter",
|
"senderFilter": "Absenderfilter",
|
||||||
"pollInterval": "Abrufintervall",
|
"pollInterval": "Abrufintervall",
|
||||||
"pollIntervalSuffix": "Minuten",
|
"pollIntervalSuffix": "Minuten",
|
||||||
|
|||||||
@@ -318,7 +318,8 @@
|
|||||||
"port": "Port",
|
"port": "Port",
|
||||||
"encryption": "Encryption",
|
"encryption": "Encryption",
|
||||||
"encryptionNone": "None",
|
"encryptionNone": "None",
|
||||||
"folder": "Folder",
|
"folder": "Folder / Mailbox",
|
||||||
|
"folderExchangeHelp": "Common mailboxes: Inbox, SentItems, DeletedItems, Drafts, JunkEmail",
|
||||||
"senderFilter": "Sender Filter",
|
"senderFilter": "Sender Filter",
|
||||||
"pollInterval": "Poll Interval",
|
"pollInterval": "Poll Interval",
|
||||||
"pollIntervalSuffix": "Minutes",
|
"pollIntervalSuffix": "Minutes",
|
||||||
|
|||||||
Reference in New Issue
Block a user