From 9952365df1b51e95860d51a4129ac66777a8a099 Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 1 Apr 2026 07:18:52 -0400 Subject: [PATCH] feat: held mail analysis dialog + fix release check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add analyzeMessage() — context-aware explanations for DMARC/impersonation/spam/auth-code/malware holds - Add MessageAnalysisDialog with severity icon, message details, explanation, and resolution options - Analyze button per row opens the dialog; Release button inside dialog triggers release + closes - Fix releaseHeldMessage(): treat HTTP 200 + empty fail[] as success (not release===true check) - Remove action:'release' from payload (API doesn't need it) --- app/admin/sync/mimecast/page.tsx | 209 +++++++++++++++++++++++++++++-- lib/services/mimecast-client.ts | 10 +- 2 files changed, 203 insertions(+), 16 deletions(-) diff --git a/app/admin/sync/mimecast/page.tsx b/app/admin/sync/mimecast/page.tsx index 0ba4a58..5c05f64 100644 --- a/app/admin/sync/mimecast/page.tsx +++ b/app/admin/sync/mimecast/page.tsx @@ -4,11 +4,12 @@ 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 { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { ArrowLeft, Activity, History, Calendar, Mail, RefreshCw, Loader2, CheckCircle2, XCircle, AlertTriangle, Shield, Inbox, Send, Clock, ChevronDown, ChevronRight, Users, Search, LockKeyhole, UnlockKeyhole, - PauseCircle, Building2, Check, + PauseCircle, Building2, Check, Info, } from 'lucide-react'; import SyncScheduler from '@/components/admin/SyncScheduler'; @@ -458,6 +459,169 @@ function HistoryTab() { ); } +// ── Message Analysis ───────────────────────────────────────────────────────── +function analyzeMessage(m: any): { headline: string; explanation: string; severity: 'red' | 'yellow' | 'green'; actions: { label: string; description: string; type: 'release' | 'info' }[] } { + const policy: string = (m.policyInfo ?? m.reason ?? '').toLowerCase(); + const reason: string = (m.reason ?? '').toLowerCase(); + const from: string = m.from ?? ''; + const fromDisplay: string = m.fromDisplay ?? ''; + + // DMARC failures + if (policy.includes('dmarc') || reason.includes('dmarc')) { + return { + headline: 'DMARC Authentication Failure', + severity: 'red', + explanation: `This email failed DMARC authentication. The sender domain's DMARC policy rejected it because the email's "From" header (${fromDisplay || from}) doesn't align with the authenticated sending domain. This is a common pattern in spoofing attempts — but can also occur with legitimate senders who haven't configured SPF/DKIM on their sending infrastructure.`, + actions: [ + { label: 'Release this email', description: 'Deliver it now if you recognize the sender and trust the content.', type: 'release' }, + { label: 'How to fix permanently', description: 'Ask the sender to configure SPF and DKIM records on their domain, or add an inbound DMARC bypass policy in Mimecast for this sender domain.', type: 'info' }, + ], + }; + } + + // Impersonation + if (policy.includes('impersonation') || reason.includes('impersonation')) { + return { + headline: 'Impersonation Attempt Detected', + severity: 'red', + explanation: `Mimecast flagged this email as a potential impersonation attack. The display name "${fromDisplay || from}" may resemble an internal user, executive, or trusted partner. These are frequently used in BEC (Business Email Compromise) attacks.`, + actions: [ + { label: 'Release this email', description: 'Only release if you have verified this is a legitimate sender out-of-band (phone/Teams). Do not rely on the email itself to confirm identity.', type: 'release' }, + { label: 'Add to permitted senders', description: 'In Mimecast, add a permitted sender policy for this specific address to prevent future holds.', type: 'info' }, + ], + }; + } + + // Spam / spam signature + if (policy.includes('spam') || reason.includes('spam')) { + const isAuth = (m.subject ?? '').toLowerCase().includes('authentication code') || + (m.subject ?? '').toLowerCase().includes('verification code') || + (m.subject ?? '').toLowerCase().includes('your code') || + (m.subject ?? '').toLowerCase().includes('otp') || + (m.subject ?? '').toLowerCase().includes('one-time'); + if (isAuth) { + return { + headline: 'Authentication / Verification Code Held', + severity: 'yellow', + explanation: `This looks like a legitimate authentication or verification code email from "${fromDisplay || from}". It was held by spam detection, likely due to a spam-like sending pattern (bulk infrastructure, shared IP reputation), not because it's genuinely malicious.`, + actions: [ + { label: 'Release this email', description: 'Release to deliver the verification code to the recipient.', type: 'release' }, + { label: 'Allow this sender permanently', description: `Add "${from}" to the permitted senders list in Mimecast > Gateway > Policies > Permitted Senders to prevent future holds from this address.`, type: 'info' }, + ], + }; + } + return { + headline: 'Spam Signature Detected', + severity: 'yellow', + explanation: `Mimecast's spam engine matched this email against known spam signatures. The sender "${fromDisplay || from}" triggered a spam policy rule. This may be a marketing email, newsletter, or a legitimate transactional email from a sender with low reputation.`, + actions: [ + { label: 'Release this email', description: 'Deliver it if you recognize the sender and the recipient is expecting it.', type: 'release' }, + { label: 'Allow this sender permanently', description: `Add "${from}" to permitted senders in Mimecast > Gateway > Policies > Permitted Senders.`, type: 'info' }, + { label: 'Block this sender', description: `Add "${from}" to blocked senders in Mimecast > Gateway > Policies > Blocked Senders if this is unwanted mail.`, type: 'info' }, + ], + }; + } + + // Malware / attachment + if (policy.includes('malware') || policy.includes('attachment') || reason.includes('malware')) { + return { + headline: 'Malware or Dangerous Attachment', + severity: 'red', + explanation: `This email was held because Mimecast detected a potentially malicious attachment or URL. Do not release without careful review.`, + actions: [ + { label: 'Do not release', description: 'This email should be reviewed by security before delivery. Contact the sender via another channel to verify legitimacy.', type: 'info' }, + { label: 'View in Mimecast console', description: 'Open the Mimecast Administration Console to view full threat details and attachment analysis.', type: 'info' }, + ], + }; + } + + // Generic hold + return { + headline: 'Message Hold Applied', + severity: 'yellow', + explanation: `This email was held by a Mimecast policy: "${m.policyInfo || m.reason}". Review the sender and subject before releasing.`, + actions: [ + { label: 'Release this email', description: 'Deliver it to the recipient if you determine it is safe.', type: 'release' }, + ], + }; +} + +function MessageAnalysisDialog({ message, onClose, onRelease, releasing }: { + message: any; + onClose: () => void; + onRelease: (m: any) => void; + releasing: boolean; +}) { + if (!message) return null; + const analysis = analyzeMessage(message); + const severityColors = { + red: 'border-red-300 bg-red-50 dark:bg-red-950/20 text-red-700', + yellow: 'border-yellow-300 bg-yellow-50 dark:bg-yellow-950/20 text-yellow-700', + green: 'border-green-300 bg-green-50 dark:bg-green-950/20 text-green-700', + }; + const severityIcon = { + red: , + yellow: , + green: , + }; + + return ( + !open && onClose()}> + + + + {severityIcon[analysis.severity]} + {analysis.headline} + + + + {/* Message details */} +
+
Subject{message.subject || '(no subject)'}
+
From{message.fromDisplay || message.from}{message.fromDisplay ? <{message.from}> : ''}
+
To{message.toDisplay || message.to}
+
Date{new Date(message.dateReceived).toLocaleString()}
+
Policy + {message.policyInfo || message.reason} +
+
+ + {/* Explanation */} +
+ {analysis.explanation} +
+ + {/* Resolution options */} +
+

Resolution Options

+ {analysis.actions.map((action, i) => ( +
+
+

{action.label}

+

{action.description}

+
+ {action.type === 'release' && ( + + )} +
+ ))} +
+
+
+ ); +} + // ── Held Mail Tab ───────────────────────────────────────────────────────────── const TENANT_OPTIONS = [ { id: '1', name: 'Wulf Consulting' }, @@ -475,6 +639,7 @@ function HeldMailTab() { const [loaded, setLoaded] = useState(false); const [releasing, setReleasing] = useState>({}); const [releaseErrors, setReleaseErrors] = useState>({}); + const [analysisMessage, setAnalysisMessage] = useState(null); const load = async (recipientVal?: string) => { setLoading(true); @@ -661,18 +826,29 @@ function HeldMailTab() {
- +
+ + +
{releaseErrors[m.id] && ( {releaseErrors[m.id]} )} @@ -685,6 +861,13 @@ function HeldMailTab() {
)} + + setAnalysisMessage(null)} + onRelease={release} + releasing={analysisMessage ? !!releasing[analysisMessage.id] : false} + /> ); } diff --git a/lib/services/mimecast-client.ts b/lib/services/mimecast-client.ts index aaad11e..f38cb79 100644 --- a/lib/services/mimecast-client.ts +++ b/lib/services/mimecast-client.ts @@ -533,10 +533,14 @@ export class MimecastClient { async releaseHeldMessage(id: string): Promise<{ released: boolean; error?: string }> { try { const result = await this.request('POST', '/api/gateway/hold-release', { - data: [{ id, action: 'release' }], + data: [{ id }], }); - const row = result?.data?.[0]; - return { released: row?.release === true }; + // API returns 200 with fail[] empty on success; release field may be false for already-released msgs + const hasFail = result?.fail?.length > 0; + if (hasFail) { + return { released: false, error: result.fail[0]?.message ?? 'Release rejected by Mimecast' }; + } + return { released: true }; } catch (err: any) { return { released: false, error: err.message }; }