fix: held mail analysis dialog layout + color scheme + policy context
- Dialog constrained to max-w-xl, max-h-85vh, overflow-y-auto (no more overflow) - Severity uses high/medium/low with red/amber/blue (not red/yellow/green) - Left-border accent stripe on explanation block - 'What was already evaluated' section shows Mimecast pipeline steps per hold type - analyzeMessage uses reasonCode for precision, detects auth codes / marketing / spam / DMARC / impersonation / malware - Warning flag on dangerous release actions (impersonation, malware) - Grid layout for message details instead of flex rows
This commit is contained in:
parent
9952365df1
commit
d0112ec12e
1 changed files with 174 additions and 88 deletions
|
|
@ -460,88 +460,130 @@ 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();
|
||||
type AnalysisAction = { label: string; description: string; type: 'release' | 'info'; warning?: boolean };
|
||||
type Analysis = {
|
||||
headline: string;
|
||||
explanation: string;
|
||||
severity: 'high' | 'medium' | 'low';
|
||||
priorSteps: string[];
|
||||
actions: AnalysisAction[];
|
||||
};
|
||||
|
||||
function analyzeMessage(m: any): Analysis {
|
||||
const code: string = (m.reasonCode ?? '').toLowerCase();
|
||||
const policy: string = (m.policyInfo ?? '').toLowerCase();
|
||||
const reason: string = (m.reason ?? '').toLowerCase();
|
||||
const from: string = m.from ?? '';
|
||||
const fromDisplay: string = m.fromDisplay ?? '';
|
||||
const fromDomain = from.includes('@') ? from.split('@')[1] : from;
|
||||
const subject: string = (m.subject ?? '').toLowerCase();
|
||||
|
||||
// DMARC failures
|
||||
if (policy.includes('dmarc') || reason.includes('dmarc')) {
|
||||
// What Mimecast has already evaluated (always true — it went through the full pipeline)
|
||||
const priorSteps = [
|
||||
'Passed through Mimecast inbound gateway',
|
||||
'Evaluated against permitted sender policies — no matching bypass found',
|
||||
'Evaluated against recipient-based allow rules — no match',
|
||||
];
|
||||
|
||||
// DMARC
|
||||
if (code.includes('dmarc') || 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.`,
|
||||
severity: 'high',
|
||||
explanation: `The sending domain failed DMARC validation. The "From" address (${fromDisplay || from}) does not align with the domain that actually sent the message (SPF/DKIM mismatch). This can indicate spoofing — but also fires for legitimate senders using shared email infrastructure (e.g. Mailchimp, Zendesk, HubSpot) who haven't set up DKIM alignment.`,
|
||||
priorSteps: [...priorSteps, 'SPF and DKIM alignment checks failed'],
|
||||
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' },
|
||||
{ label: 'Release this message', description: 'Deliver it now if you recognise the sender. The recipient will receive it normally.', type: 'release' },
|
||||
{ label: 'Add a permitted sender policy', description: `In Mimecast: Administration > Gateway > Policies > Permitted Senders. Add sender domain "${fromDomain}" to bypass DMARC holds for this domain going forward.`, type: 'info' },
|
||||
{ label: 'Ask the sender to fix their authentication', description: 'The sender should configure DKIM signing on their email platform and ensure the d= domain in DKIM matches their From domain.', type: 'info' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Impersonation
|
||||
if (policy.includes('impersonation') || reason.includes('impersonation')) {
|
||||
if (code.includes('impersonation') || 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.`,
|
||||
headline: 'Impersonation Protection Hold',
|
||||
severity: 'high',
|
||||
explanation: `Mimecast's impersonation protection flagged "${fromDisplay || from}" as potentially impersonating an internal user or trusted contact. The display name may match an executive or employee while the sending address is external. This is the primary vector for BEC (Business Email Compromise) fraud.`,
|
||||
priorSteps: [...priorSteps, 'Display name matched internal user list — external sender flagged'],
|
||||
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' },
|
||||
{ label: 'Release this message', description: 'Only release after verifying identity through another channel (phone/Teams). Do not confirm via reply to the held email.', type: 'release', warning: true },
|
||||
{ label: 'Add to permitted senders', description: `If this is a legitimate contact, add "${from}" as a permitted sender in Mimecast to bypass impersonation checks for this specific address.`, 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) {
|
||||
// Spam
|
||||
if (code.includes('spam') || policy.includes('spam') || reason.includes('spam')) {
|
||||
const isAuthCode = subject.includes('authentication code') || subject.includes('verification code') ||
|
||||
subject.includes('your code') || subject.includes('otp') || subject.includes('one-time') ||
|
||||
subject.includes('access code') || subject.includes('login code');
|
||||
const isMarketing = subject.includes('unsubscribe') || subject.includes('offer') ||
|
||||
subject.includes('deal') || subject.includes('sale') || subject.includes('newsletter') ||
|
||||
(m.hasAttachments === false && m.size > 30000);
|
||||
|
||||
if (isAuthCode) {
|
||||
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.`,
|
||||
headline: 'Authentication Code — Held by Spam Filter',
|
||||
severity: 'low',
|
||||
explanation: `This is almost certainly a legitimate authentication or verification code email from "${fromDisplay || from}". It was caught by spam detection due to the sending infrastructure's reputation score — not because the content is malicious. The recipient is likely waiting for this code.`,
|
||||
priorSteps: [...priorSteps, `Spam score exceeded threshold for policy "${m.policyInfo}"`, 'No permitted sender rule found for this address'],
|
||||
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' },
|
||||
{ label: 'Release this message', description: 'Deliver it now. The verification code is time-sensitive.', type: 'release' },
|
||||
{ label: 'Add permitted sender rule', description: `Add "${from}" to Mimecast > Administration > Gateway > Policies > Permitted Senders so future codes from this address are delivered without holds.`, type: 'info' },
|
||||
],
|
||||
};
|
||||
}
|
||||
if (isMarketing) {
|
||||
return {
|
||||
headline: 'Marketing / Promotional Email',
|
||||
severity: 'low',
|
||||
explanation: `This appears to be a marketing or promotional email from "${fromDisplay || from}" that triggered the spam policy "${m.policyInfo}". These are frequently held when sent from bulk mail platforms (Mailchimp, Constant Contact, etc.) with mixed sender reputation.`,
|
||||
priorSteps: [...priorSteps, `Spam score exceeded threshold for policy "${m.policyInfo}"`],
|
||||
actions: [
|
||||
{ label: 'Release this message', description: 'Deliver if the recipient has opted in or is expecting communications from this sender.', type: 'release' },
|
||||
{ label: 'Add permitted sender rule', description: `Add sender domain "${fromDomain}" to permitted senders if this is a trusted marketing partner.`, type: 'info' },
|
||||
{ label: 'Block this sender', description: `Add "${fromDomain}" to blocked senders in Mimecast > Gateway > Policies > Blocked Senders if this is unwanted mail.`, 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.`,
|
||||
headline: 'Spam Signature Match',
|
||||
severity: 'medium',
|
||||
explanation: `The email from "${fromDisplay || from}" matched a spam signature under policy "${m.policyInfo}". This can be a false positive for legitimate transactional or notification emails sent through shared infrastructure with a low sender reputation.`,
|
||||
priorSteps: [...priorSteps, `Spam score exceeded threshold for policy "${m.policyInfo}"`],
|
||||
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' },
|
||||
{ label: 'Release this message', description: 'Deliver if you recognise the sender and the recipient is expecting this email.', type: 'release' },
|
||||
{ label: 'Add permitted sender rule', description: `Add "${from}" to Mimecast > Administration > Gateway > Policies > Permitted Senders to prevent future holds.`, type: 'info' },
|
||||
{ label: 'Block this sender', description: `Add "${fromDomain}" to blocked senders if this is definitively spam.`, type: 'info' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Malware / attachment
|
||||
if (policy.includes('malware') || policy.includes('attachment') || reason.includes('malware')) {
|
||||
// Malware / threat
|
||||
if (code.includes('malware') || code.includes('virus') || code.includes('threat') ||
|
||||
policy.includes('malware') || 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.`,
|
||||
headline: 'Malware or Threat Detected',
|
||||
severity: 'high',
|
||||
explanation: `Mimecast detected a potentially malicious attachment or URL in this email. Do not release without thorough review by a security administrator.`,
|
||||
priorSteps: [...priorSteps, 'Attachment/URL scanned — threat signature matched'],
|
||||
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' },
|
||||
{ label: 'Do not release without security review', description: 'Contact the sender through a separate channel to verify this email is legitimate before considering release.', type: 'info', warning: true },
|
||||
{ label: 'View full threat details', description: 'Open Mimecast Administration Console > Gateway > Held Queue to view the full attachment analysis and URL scan results.', 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.`,
|
||||
severity: 'medium',
|
||||
explanation: `This email was held under policy "${m.policyInfo || m.reason}". Review the sender and content before releasing.`,
|
||||
priorSteps,
|
||||
actions: [
|
||||
{ label: 'Release this email', description: 'Deliver it to the recipient if you determine it is safe.', type: 'release' },
|
||||
{ label: 'Release this message', description: 'Deliver it to the recipient if you determine it is safe.', type: 'release' },
|
||||
{ label: 'Add permitted sender rule', description: `Add "${from}" to Mimecast > Administration > Gateway > Policies > Permitted Senders to bypass this hold for future messages.`, type: 'info' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -554,68 +596,112 @@ function MessageAnalysisDialog({ message, onClose, onRelease, releasing }: {
|
|||
}) {
|
||||
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 severityBar = {
|
||||
high: 'border-l-red-500',
|
||||
medium: 'border-l-amber-500',
|
||||
low: 'border-l-blue-500',
|
||||
};
|
||||
const severityBadge = {
|
||||
high: 'bg-red-100 text-red-700 dark:bg-red-950/40 dark:text-red-400',
|
||||
medium: 'bg-amber-100 text-amber-700 dark:bg-amber-950/40 dark:text-amber-400',
|
||||
low: 'bg-blue-100 text-blue-700 dark:bg-blue-950/40 dark:text-blue-400',
|
||||
};
|
||||
const severityLabel = { high: 'High risk', medium: 'Review needed', low: 'Likely safe' };
|
||||
const severityIcon = {
|
||||
red: <XCircle className="w-5 h-5 text-red-500 flex-shrink-0" />,
|
||||
yellow: <AlertTriangle className="w-5 h-5 text-yellow-500 flex-shrink-0" />,
|
||||
green: <CheckCircle2 className="w-5 h-5 text-green-500 flex-shrink-0" />,
|
||||
high: <XCircle className="w-4 h-4" />,
|
||||
medium: <AlertTriangle className="w-4 h-4" />,
|
||||
low: <CheckCircle2 className="w-4 h-4" />,
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={!!message} onOpenChange={open => !open && onClose()}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogContent className="max-w-xl w-full max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{severityIcon[analysis.severity]}
|
||||
{analysis.headline}
|
||||
</DialogTitle>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<DialogTitle className="text-base">{analysis.headline}</DialogTitle>
|
||||
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${severityBadge[analysis.severity]}`}>
|
||||
{severityIcon[analysis.severity]}
|
||||
{severityLabel[analysis.severity]}
|
||||
</span>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Message details */}
|
||||
<div className="rounded-lg border bg-muted/30 p-3 space-y-1 text-sm">
|
||||
<div className="flex gap-2"><span className="text-muted-foreground w-16 flex-shrink-0">Subject</span><span className="font-medium">{message.subject || '(no subject)'}</span></div>
|
||||
<div className="flex gap-2"><span className="text-muted-foreground w-16 flex-shrink-0">From</span><span>{message.fromDisplay || message.from}{message.fromDisplay ? <span className="text-muted-foreground ml-1"><{message.from}></span> : ''}</span></div>
|
||||
<div className="flex gap-2"><span className="text-muted-foreground w-16 flex-shrink-0">To</span><span>{message.toDisplay || message.to}</span></div>
|
||||
<div className="flex gap-2"><span className="text-muted-foreground w-16 flex-shrink-0">Date</span><span>{new Date(message.dateReceived).toLocaleString()}</span></div>
|
||||
<div className="flex gap-2"><span className="text-muted-foreground w-16 flex-shrink-0">Policy</span>
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
analysis.severity === 'red' ? 'bg-red-500/10 text-red-600' : 'bg-yellow-500/10 text-yellow-700'
|
||||
}`}>{message.policyInfo || message.reason}</span>
|
||||
<div className="rounded-md border bg-muted/20 divide-y text-sm">
|
||||
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs pt-0.5">Subject</span>
|
||||
<span className="font-medium break-words">{message.subject || '(no subject)'}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs pt-0.5">From</span>
|
||||
<span className="break-all">{message.fromDisplay ? `${message.fromDisplay} ` : ''}<span className="text-muted-foreground">{message.fromDisplay ? `<${message.from}>` : message.from}</span></span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs pt-0.5">To</span>
|
||||
<span>{message.toDisplay || message.to}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs pt-0.5">Received</span>
|
||||
<span>{new Date(message.dateReceived).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs pt-0.5">Policy</span>
|
||||
<span className="font-medium">{message.policyInfo || '—'}</span>
|
||||
</div>
|
||||
{message.reason && (
|
||||
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs pt-0.5">Reason</span>
|
||||
<span className="text-muted-foreground">{message.reason}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Explanation */}
|
||||
<div className={`rounded-lg border p-3 text-sm ${severityColors[analysis.severity]}`}>
|
||||
<div className={`rounded-md border-l-4 border border-border pl-3 pr-3 py-2.5 text-sm text-foreground/90 ${severityBar[analysis.severity]}`}>
|
||||
{analysis.explanation}
|
||||
</div>
|
||||
|
||||
{/* Resolution options */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Resolution Options</p>
|
||||
{analysis.actions.map((action, i) => (
|
||||
<div key={i} className="rounded-lg border p-3 flex items-start justify-between gap-3">
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<p className="text-sm font-medium">{action.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{action.description}</p>
|
||||
{/* What Mimecast already checked */}
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">What was already evaluated</p>
|
||||
<div className="rounded-md border bg-muted/10 divide-y">
|
||||
{analysis.priorSteps.map((step, i) => (
|
||||
<div key={i} className="flex items-start gap-2 px-3 py-2 text-xs text-muted-foreground">
|
||||
<CheckCircle2 className="w-3.5 h-3.5 mt-0.5 flex-shrink-0 text-muted-foreground/50" />
|
||||
{step}
|
||||
</div>
|
||||
{action.type === 'release' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex-shrink-0 gap-1 text-green-700 border-green-300 hover:bg-green-50"
|
||||
disabled={releasing}
|
||||
onClick={() => { onRelease(message); onClose(); }}
|
||||
>
|
||||
{releasing ? <Loader2 className="w-3 h-3 animate-spin" /> : <Check className="w-3 h-3" />}
|
||||
Release
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resolution options */}
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Resolution options</p>
|
||||
<div className="space-y-2">
|
||||
{analysis.actions.map((action, i) => (
|
||||
<div key={i} className={`rounded-md border p-3 flex items-start justify-between gap-3 ${
|
||||
action.warning ? 'border-amber-300 bg-amber-50/50 dark:bg-amber-950/10' : 'bg-background'
|
||||
}`}>
|
||||
<div className="space-y-0.5 flex-1 min-w-0">
|
||||
<p className="text-sm font-medium">{action.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{action.description}</p>
|
||||
</div>
|
||||
{action.type === 'release' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex-shrink-0 gap-1 text-green-700 border-green-300 hover:bg-green-50 dark:hover:bg-green-950/20"
|
||||
disabled={releasing}
|
||||
onClick={() => { onRelease(message); onClose(); }}
|
||||
>
|
||||
{releasing ? <Loader2 className="w-3 h-3 animate-spin" /> : <Check className="w-3 h-3" />}
|
||||
Release
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue