'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ArrowLeft, Activity, History, Calendar, Mail, RefreshCw, Loader2, CheckCircle2, XCircle, AlertTriangle, Shield, Inbox, Send, Clock, ChevronDown, ChevronRight, Users, Search, LockKeyhole, UnlockKeyhole, PauseCircle, Building2, } from 'lucide-react'; import SyncScheduler from '@/components/admin/SyncScheduler'; function fmtDate(d: string | null | undefined) { if (!d) return 'Never'; return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } function fmtNum(n: number | null | undefined) { if (n == null) return '—'; return n.toLocaleString(); } function StatCard({ label, value, sub, icon: Icon, cls }: { label: string; value: string | number; sub?: string; icon?: React.ElementType; cls?: string; }) { return (
{Icon && }{label}
{value}
{sub &&
{sub}
}
); } function StatusBadge({ status }: { status: string }) { const cls = status === 'delivered' ? 'bg-green-500/15 text-green-700' : status === 'rejected' ? 'bg-red-500/15 text-red-600' : status === 'held' ? 'bg-yellow-500/15 text-yellow-700' : status === 'bounced' ? 'bg-orange-500/15 text-orange-700' : status === 'spam' ? 'bg-purple-500/15 text-purple-700' : 'bg-muted text-muted-foreground'; return ( {status || '—'} ); } function ThreatBadge({ level }: { level: string }) { const cls = level === 'high' ? 'bg-red-500/15 text-red-600' : level === 'medium' ? 'bg-orange-500/15 text-orange-700' : level === 'low' ? 'bg-yellow-500/15 text-yellow-700' : 'bg-muted text-muted-foreground'; return ( {level || 'info'} ); } // ── Status Tab ──────────────────────────────────────────────────────────────── function StatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) => void; syncing: boolean }) { if (!data) return (
); const stats = data.stats ?? {}; return (
{/* Connection banner */}
{data.connected ? : }

{data.connected ? `Connected — ${data.accountName ?? 'Mimecast'}` : 'Not connected'}

{data.packageName && ( ({data.packageName}) )}

Last sync: {fmtDate(stats.lastSync)} · Oldest message: {fmtDate(stats.oldestMessage)}

{data.error && (
{data.error}
)} {/* Stats grid */}
0 ? 'border-red-500/20 bg-red-500/5' : ''} />

Data Retention

120-day rolling window. Messages older than 120 days are automatically purged on each sync. Message bodies are fetched for delivered inbound messages (up to 500 per sync run).

); } // ── Messages Tab ────────────────────────────────────────────────────────────── function MessagesTab() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [direction, setDirection] = useState(''); const [status, setStatus] = useState(''); const [days, setDays] = useState('7'); const load = () => { setLoading(true); const params = new URLSearchParams({ days, limit: '200' }); if (search) params.set('search', search); if (direction) params.set('direction', direction); if (status) params.set('status', status); fetch(`/api/mimecast/messages?${params}`) .then(r => r.json()) .then(d => setRows(d.messages ?? [])) .catch(() => setRows([])) .finally(() => setLoading(false)); }; useEffect(() => { load(); }, [days, direction, status]); return (
{/* Filters */}
setSearch(e.target.value)} onKeyDown={e => e.key === 'Enter' && load()} className="flex-1 min-w-48 border rounded-md px-3 py-1.5 text-sm bg-background" />
{loading ? (
) : !rows.length ? (
No messages found — run a sync first or adjust filters
) : (
{rows.map((r: any) => ( ))}
From To Subject Direction Status Sent
{r.sender_address ?? '—'} {r.recipient_address ?? '—'} {r.subject ?? '—'} {r.direction ?? '—'} {fmtDate(r.sent_datetime)}
)}
); } // ── Threats Tab ─────────────────────────────────────────────────────────────── function ThreatsTab() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch('/api/mimecast/threats?limit=200') .then(r => r.json()) .then(d => setRows(d.threats ?? [])) .catch(() => setRows([])) .finally(() => setLoading(false)); }, []); if (loading) return
; if (!rows.length) return
No threat events — run a sync first
; return (
{rows.map((r: any) => ( ))}
Type Level Actor Verdict URL / File When
{r.event_type ?? '—'} {r.actor_email ?? '—'} {r.verdict ?? '—'} {r.url ?? r.file_name ?? '—'} {fmtDate(r.event_datetime)}
); } // ── Cloud Users Tab ─────────────────────────────────────────────────────────── function CloudUserTab() { const [email, setEmail] = useState(''); const [domain, setDomain] = useState(''); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const [showRaw, setShowRaw] = useState(false); const handleEmailChange = (v: string) => { setEmail(v); const atIdx = v.indexOf('@'); if (atIdx >= 0) setDomain(v.slice(atIdx + 1)); }; const lookup = async () => { if (!email || !domain) return; setLoading(true); setResult(null); setShowRaw(false); try { const params = new URLSearchParams({ emailAddress: email, domain }); const res = await fetch(`/api/mimecast/cloud-user?${params}`); setResult(await res.json()); } catch (err: any) { setResult({ error: err.message }); } finally { setLoading(false); } }; const user = result?.user; const lockedOut = user?.lockedOut ?? false; return (
handleEmailChange(e.target.value)} onKeyDown={e => e.key === 'Enter' && lookup()} className="border rounded-md px-3 py-1.5 text-sm bg-background w-72" />
setDomain(e.target.value)} onKeyDown={e => e.key === 'Enter' && lookup()} className="border rounded-md px-3 py-1.5 text-sm bg-background w-48" />
{result?.error && (
{result.error}
)} {result && !result.error && !result.found && (
User not found in Mimecast Cloud Gateway.
)} {user && (
{lockedOut ? : }

{lockedOut ? 'Account Locked Out' : 'Account Active'}

{user.name && {user.name} · } {user.emailAddress} {user.status && · Status: {user.status}}

{showRaw && (
              {JSON.stringify(user._raw ?? user, null, 2)}
            
)}
)}
); } // ── History Tab ─────────────────────────────────────────────────────────────── function HistoryTab() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch('/api/sync/history?entityType=mimecast&limit=30') .then(r => r.json()) .then(d => setRows(d.history ?? [])) .catch(() => setRows([])) .finally(() => setLoading(false)); }, []); if (loading) return
; if (!rows.length) return
No sync history yet
; return (
{rows.map((r: any, i: number) => { const dur = r.completed_at && r.started_at ? new Date(r.completed_at).getTime() - new Date(r.started_at).getTime() : null; const durStr = dur == null ? '—' : dur < 60000 ? `${Math.round(dur / 1000)}s` : `${Math.floor(dur / 60000)}m ${Math.round((dur % 60000) / 1000)}s`; const statusCls = r.status === 'completed' ? 'bg-green-500/15 text-green-700' : r.status === 'failed' ? 'bg-red-500/15 text-red-600' : 'bg-muted text-muted-foreground'; const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata ?? {}); return ( ); })}
Type Status Messages Threats Started Duration
{r.sync_type ?? '—'} {r.status} {fmtNum(meta.messagesUpserted ?? r.records_added)} {fmtNum(meta.threatsUpserted)} {fmtDate(r.started_at)} {durStr}
); } // ── Held Mail Tab ───────────────────────────────────────────────────────────── function HeldMailTab() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [recipient, setRecipient] = useState(''); const [tenantFilter, setTenantFilter] = useState(''); const [policyFilter, setPolicyFilter] = useState(''); const [loaded, setLoaded] = useState(false); const load = (recipientVal?: string) => { setLoading(true); 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)); }; const messages: any[] = data?.messages ?? []; const filtered = policyFilter ? messages.filter(m => m.policyInfo?.toLowerCase().includes(policyFilter.toLowerCase())) : messages; const policies = [...new Set(messages.map((m: any) => m.policyInfo).filter(Boolean))].sort(); const tenants: any[] = data?.tenants ?? []; return (
{/* Search bar */}
setRecipient(e.target.value)} onKeyDown={e => e.key === 'Enter' && load()} className="w-full border rounded-md px-3 py-1.5 text-sm bg-background" />
{tenants.length > 1 && (
)} {loaded && policies.length > 0 && (
)}
{/* Tenant summary badges */} {loaded && tenants.length > 0 && (
{tenants.map(t => (
0 ? 'border-yellow-400/40 bg-yellow-500/5 text-yellow-700' : 'border-border bg-muted/30 text-muted-foreground' }`}> {t.accountName} {t.error ? error : — {t.count.toLocaleString()}{t.totalCount > t.count ? ` of ${t.totalCount.toLocaleString()}` : ''} held }
))}
)} {!loaded && !loading && (
Click “Load Held Mail” to fetch held messages across all configured Mimecast tenants.
)} {loading && (
Fetching held messages from all tenants…
)} {loaded && !loading && filtered.length === 0 && (
No held messages found.
)} {loaded && !loading && filtered.length > 0 && (
{filtered.length.toLocaleString()} held message{filtered.length !== 1 ? 's' : ''} {data?.totalCount > filtered.length ? ` (showing ${filtered.length} of ${data.totalCount.toLocaleString()} total)` : ''}
{tenants.length > 1 && } {filtered.map((m: any) => ( {tenants.length > 1 && ( )} ))}
Date To From Subject PolicyTenant
{new Date(m.dateReceived).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
{m.to}
{m.fromDisplay || m.from}
{m.fromDisplay &&
{m.from}
}
{m.subject || '(no subject)'}
{m.policyInfo || m.reason || '—'} {m.accountName}
)}
); } // ── Page ────────────────────────────────────────────────────────────────────── export default function MimecastSyncPage() { const [statusData, setStatusData] = useState(null); const [syncing, setSyncing] = useState(false); const [lastResult, setLastResult] = useState(null); const fetchStatus = () => { fetch('/api/mimecast/status') .then(r => r.json()) .then(d => setStatusData(d)) .catch(() => setStatusData({ configured: false, connected: false })); }; useEffect(() => { fetchStatus(); }, []); const handleSync = async (syncType: string) => { setSyncing(true); setLastResult(null); try { const res = await fetch('/api/sync/mimecast', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ syncType }), }); const data = await res.json(); setLastResult(data); fetchStatus(); } catch (err: any) { setLastResult({ error: err.message }); } finally { setSyncing(false); } }; return (
{/* Header */}

Email Security — Mimecast

Message logs, threat events, 120-day retention

{/* Last sync result banner */} {lastResult && (
{lastResult.error ? `Sync failed: ${lastResult.error}` : `Sync complete — ${fmtNum(lastResult.messagesUpserted)} messages, ${fmtNum(lastResult.threatsUpserted)} threats, ${fmtNum(lastResult.bodiesFetched)} bodies fetched in ${Math.round((lastResult.durationMs ?? 0) / 1000)}s` } {lastResult.errors?.length > 0 && (
{lastResult.errors.slice(0, 3).join(' · ')}
)}
)} Status Held Mail Messages Threats Cloud Users History Schedules
); }