From 2f88be9ab3c9ae5e72ac807dc51a18e4590b0d15 Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 1 Apr 2026 06:52:58 -0400 Subject: [PATCH] fix: held mail route force-dynamic, 403 fallback, favicon 404s, error display - export const dynamic = 'force-dynamic' on /api/mimecast/held to prevent Next.js caching - Add AbortController timeout (20s) per request in MimecastClient.request() - getHeldMessages: 403 fallback without admin:true flag for tenants lacking permission - Reduce maxMessages default to 100 (10 pages) to stay within route timeout - Show 'permission denied' tooltip in tenant badge for 403 errors - Surface HTTP errors in HeldMailTab instead of silently failing - Add missing favicons: sentinelone.ico, itglue.ico, mimecast.ico --- app/admin/sync/mimecast/page.tsx | 34 ++++++++++++++++++++++++-------- app/api/mimecast/held/route.ts | 4 +++- lib/services/mimecast-client.ts | 27 ++++++++++++++++++++++--- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/app/admin/sync/mimecast/page.tsx b/app/admin/sync/mimecast/page.tsx index ea24756..2ab5198 100644 --- a/app/admin/sync/mimecast/page.tsx +++ b/app/admin/sync/mimecast/page.tsx @@ -462,22 +462,34 @@ function HistoryTab() { function HeldMailTab() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); + const [loadError, setLoadError] = useState(null); const [recipient, setRecipient] = useState(''); const [tenantFilter, setTenantFilter] = useState(''); const [policyFilter, setPolicyFilter] = useState(''); const [loaded, setLoaded] = useState(false); - const load = (recipientVal?: string) => { + const load = async (recipientVal?: string) => { setLoading(true); + setLoadError(null); const params = new URLSearchParams(); const r = recipientVal ?? recipient; if (r) params.set('recipient', r); if (tenantFilter) params.set('tenantId', tenantFilter); - fetch(`/api/mimecast/held?${params}`) - .then(res => res.json()) - .then(d => { setData(d); setLoaded(true); }) - .catch(() => setData(null)) - .finally(() => setLoading(false)); + try { + const res = await fetch(`/api/mimecast/held?${params}`); + if (!res.ok) { + const text = await res.text(); + throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`); + } + const d = await res.json(); + setData(d); + setLoaded(true); + } catch (e: any) { + setLoadError(e.message ?? 'Unknown error'); + console.error('[HeldMail] fetch error:', e); + } finally { + setLoading(false); + } }; const messages: any[] = data?.messages ?? []; @@ -543,7 +555,7 @@ function HeldMailTab() { {t.accountName} {t.error - ? error + ? — permission denied : — {t.count.toLocaleString()}{t.totalCount > t.count ? ` of ${t.totalCount.toLocaleString()}` : ''} held } @@ -551,7 +563,13 @@ function HeldMailTab() { )} - {!loaded && !loading && ( + {loadError && ( +
+ {loadError} +
+ )} + + {!loaded && !loading && !loadError && (
Click “Load Held Mail” to fetch held messages across all configured Mimecast tenants.
diff --git a/app/api/mimecast/held/route.ts b/app/api/mimecast/held/route.ts index 8ef2663..907c521 100644 --- a/app/api/mimecast/held/route.ts +++ b/app/api/mimecast/held/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { postgresClient } from '@/lib/services/postgres-client'; import { getMimecastClientForTenant } from '@/lib/services/mimecast-client'; +export const dynamic = 'force-dynamic'; + export async function GET(req: NextRequest) { try { const { searchParams } = new URL(req.url); @@ -36,7 +38,7 @@ export async function GET(req: NextRequest) { const results = await Promise.allSettled( tenants.map(async (tenant) => { const client = getMimecastClientForTenant(tenant); - const { messages, totalCount } = await client.getHeldMessages({ recipient, maxMessages: 500 }); + const { messages, totalCount } = await client.getHeldMessages({ recipient, maxMessages: 100 }); return { tenantId: tenant.id, accountCode: tenant.account_code, diff --git a/lib/services/mimecast-client.ts b/lib/services/mimecast-client.ts index 6822ef3..d3c5ecf 100644 --- a/lib/services/mimecast-client.ts +++ b/lib/services/mimecast-client.ts @@ -165,7 +165,8 @@ export class MimecastClient { method: 'GET' | 'POST', path: string, body?: Record, - params?: Record + params?: Record, + timeoutMs = 20_000 ): Promise { const token = await this.getToken(); @@ -183,12 +184,18 @@ export class MimecastClient { headers['x-mc-account'] = this.accountCode; } + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + const res = await fetch(url, { method, + signal: controller.signal, headers, body: body ? JSON.stringify(body) : undefined, }); + clearTimeout(timer); + if (!res.ok) { const text = await res.text(); throw new Error(`Mimecast ${method} ${path} failed (${res.status}): ${text}`); @@ -459,7 +466,7 @@ export class MimecastClient { recipient?: string; maxMessages?: number; } = {}): Promise<{ messages: MimecastHeldMessage[]; totalCount: number }> { - const maxMessages = options.maxMessages ?? 500; + const maxMessages = options.maxMessages ?? 100; const all: MimecastHeldMessage[] = []; let cursor: string | null = null; let totalCount = 0; @@ -475,7 +482,21 @@ export class MimecastClient { body.meta = { pagination: { pageToken: cursor } }; } - const result = await this.request('POST', '/api/gateway/get-hold-message-list', body); + let result: any; + try { + result = await this.request('POST', '/api/gateway/get-hold-message-list', body); + } catch (err: any) { + if (err.message?.includes('403') && !cursor) { + // Retry without admin: true for tenants that lack the permission + const fallbackBody: any = { data: [{ ...reqBody }] }; + delete fallbackBody.data[0].admin; + if (cursor) fallbackBody.meta = { pagination: { pageToken: cursor } }; + result = await this.request('POST', '/api/gateway/get-hold-message-list', fallbackBody); + } else { + throw err; + } + } + const pagination = result?.meta?.pagination ?? {}; const msgs: any[] = result?.data ?? [];