'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)}
onSync('incremental')} disabled={syncing || !data.connected}>
{syncing ? : }
Incremental
onSync('full')} disabled={syncing || !data.connected}>
{syncing ? : }
Full Sync (120d)
{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"
/>
setDirection(e.target.value)}
className="border rounded-md px-3 py-1.5 text-sm bg-background">
All directions
Inbound
Outbound
setStatus(e.target.value)}
className="border rounded-md px-3 py-1.5 text-sm bg-background">
All statuses
Delivered
Rejected
Held
Bounced
Spam
setDays(e.target.value)}
className="border rounded-md px-3 py-1.5 text-sm bg-background">
Last 24h
Last 7 days
Last 30 days
Last 90 days
Last 120 days
Search
{loading ? (
) : !rows.length ? (
No messages found — run a sync first or adjust filters
) : (
From
To
Subject
Direction
Status
Sent
{rows.map((r: any) => (
{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 (
Type
Level
Actor
Verdict
URL / File
When
{rows.map((r: any) => (
{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 (
{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} }
setShowRaw(v => !v)}
>
{showRaw ? 'Hide' : 'Show'} raw response
{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 (
Type
Status
Messages
Threats
Started
Duration
{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 (
{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 */}
Recipient email
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 && (
Tenant
setTenantFilter(e.target.value)}
className="border rounded-md px-3 py-1.5 text-sm bg-background">
All tenants
{tenants.map(t => (
{t.accountName}
))}
)}
{loaded && policies.length > 0 && (
Policy
setPolicyFilter(e.target.value)}
className="border rounded-md px-3 py-1.5 text-sm bg-background min-w-48">
All policies
{policies.map(p => {p} )}
)}
load()} disabled={loading} className="gap-2">
{loading ? : }
{loaded ? 'Refresh' : 'Load Held Mail'}
{/* 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)` : ''}
Date
To
From
Subject
Policy
{tenants.length > 1 && Tenant }
{filtered.map((m: any) => (
{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 || '—'}
{tenants.length > 1 && (
{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 */}
Integrations
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
);
}