diff --git a/entrypoints/background.ts b/entrypoints/background.ts index 6f927bb..2e85dbf 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -56,6 +56,7 @@ async function refreshToolbarIconForTab( treatDnsResolutionErrorsAsFailure: settings.treatDnsResolutionErrorsAsFailure, dnsProvider: settings.dnsProvider, + customDkimSelectors: settings.customDkimSelectors, }); if (toolbarRefreshGenByTabId.get(tabId) !== token) { return; diff --git a/entrypoints/popup/main.ts b/entrypoints/popup/main.ts index 47ed131..5ddafcb 100644 --- a/entrypoints/popup/main.ts +++ b/entrypoints/popup/main.ts @@ -38,6 +38,9 @@ let settings: ExtensionSettings = { ...DEFAULT_SETTINGS }; let currentView: 'welcome' | 'main' | 'settings' = 'main'; let lastMode: CheckMode = 'apex'; let lastResult: CheckResult | null = null; +let compareResult: CheckResult | null = null; +let lastResultUpdatesToolbar = true; +let compareRequestId = 0; const COG_SVG = ``; @@ -100,6 +103,11 @@ function statusLabel(status: HealthStatus): string { } } +function gradeStatusLabel(status: GradeLine['status']): string { + if (status === 'info') return 'Info'; + return statusLabel(status); +} + function truncate(s: string, max: number): string { const t = s.trim(); if (t.length <= max) return t; @@ -117,7 +125,7 @@ function mxtoolboxEmailHealthUrl(domain: string): string { } const DNS_TECHNIQUE_DISCLOSURE = - 'DNS queries use DNS-over-HTTPS (Cloudflare / Google). Entra probe uses HTTPS only; no MTA-STS policy files or cert inspection. DKIM probes _domainkey for null DKIM, then provider/common selectors, then *._domainkey.'; + 'DNS queries use DNS-over-HTTPS (Cloudflare / Google). Entra probe uses HTTPS only; no MTA-STS policy files or cert inspection. DKIM probes _domainkey for null DKIM, then configured/provider/common selectors, then *._domainkey.'; /** Opens a URL from a user gesture (e.g. modal submit) without extra extension permissions. */ function openUrlInNewTab(url: string): void { @@ -135,6 +143,11 @@ function hasReportableDmarcIssue(result: CheckResult): boolean { } function renderResultFooterActions(result: CheckResult): string { + const rootTargets = resolveCheckTargets(result.tabHostname, 'apex'); + const showCompare = rootTargets.queryHost !== rootTargets.tab; + const compareBtn = showCompare + ? `` + : ''; const showCastShame = hasReportableDmarcIssue(result); const castShameBtn = showCastShame ? `` @@ -142,6 +155,8 @@ function renderResultFooterActions(result: CheckResult): string { return ` `; } +function renderFixGuidance(title: 'SPF' | 'DMARC' | 'DKIM', result: CheckResult): string { + const score = title === 'SPF' + ? result.full.spf + : title === 'DMARC' + ? result.full.dmarc + : result.full.dkim; + if (score.status === 'pass') return ''; + + const host = title === 'DMARC' + ? `_dmarc.${result.dmarcLookupHost}` + : result.queryHostname; + let guidance = ''; + if (title === 'SPF') { + const providerInclude = result.spfMailProviderHint?.expectedInclude; + const example = providerInclude + ? `v=spf1 include:${providerInclude} -all` + : 'v=spf1 -all'; + guidance = `Publish one TXT record at ${host}. Example for a domain that sends no mail, or after adding approved senders: ${example}`; + } else if (title === 'DMARC') { + guidance = `Publish one TXT record at ${host}. Start with reporting, then move toward enforcement: v=DMARC1; p=none; rua=mailto:dmarc@example.com`; + } else { + guidance = 'Confirm the selector your mail platform signs with, then publish that selector under selector._domainkey. Custom selectors can be added in settings.'; + } + + return ` +
+ Fix guidance +

${escapeHtml(guidance)}

+
+ `; +} + function renderProtocolCard( title: string, score: FullScore['spf'], @@ -375,6 +524,18 @@ function renderHeaderBrand(hostname: string): string { `; } +function renderManualLookupForm(hostname: string): string { + return ` +
+ +
+ + +
+
+ `; +} + function renderWelcome(): void { const targets = resolveCheckTargets(tabHostname, 'apex'); const rootHost = targets.queryHost; @@ -389,6 +550,7 @@ function renderWelcome(): void { root.innerHTML = shellWithFabFooterOnly(`
${renderHeaderBrand(rootHost)} + ${renderManualLookupForm(rootHost)}

First run

@@ -403,26 +565,29 @@ function renderWelcome(): void {
`); bindWelcomeActions(); + bindManualLookupForm(); bindSettingsFab(); } -function renderLoading(mode: CheckMode): void { - const targets = tabHostname ? resolveCheckTargets(tabHostname, mode) : null; - const rootTargets = tabHostname ? resolveCheckTargets(tabHostname, 'apex') : null; +function renderLoading(mode: CheckMode, hostname: string): void { + const targets = hostname ? resolveCheckTargets(hostname, mode) : null; + const rootTargets = hostname ? resolveCheckTargets(hostname, 'apex') : null; const headerHost = targets?.queryHost ?? ''; const showExact = rootTargets ? rootTargets.queryHost !== rootTargets.tab : true; root.innerHTML = shellWithFabFooterOnly(`
${headerHost ? renderHeaderBrand(headerHost) : '

JayQuery

'} + ${renderManualLookupForm(headerHost || hostname)} ${modeChips(mode, showExact)} -

${escapeHtml(loadingLabel(mode, tabHostname))}

+

${escapeHtml(loadingLabel(mode, hostname))}

Querying public DNS (DoH)…

`); - bindModeButtons(mode, true); + bindModeButtons(mode, true, hostname); + bindManualLookupForm(); bindSettingsFab(); } @@ -431,11 +596,13 @@ function renderError(message: string): void { root.innerHTML = shellWithFabFooterOnly(`
${tabHostname ? renderHeaderBrand(tabHostname) : '

JayQuery

'} + ${renderManualLookupForm(tabHostname)}

${escapeHtml(message)}

`); + bindManualLookupForm(); bindSettingsFab(); } @@ -480,6 +647,39 @@ function renderMailInfraCard( `; } +function renderComparisonValue(label: string, current: FullScore['spf'], other: FullScore['spf']): string { + const changed = current.status !== other.status || current.points !== other.points; + return ` +
+ ${label} + ${statusLabel(current.status)} ${formatScoreTenth(current.points)}/${current.max} + ${statusLabel(other.status)} ${formatScoreTenth(other.points)}/${other.max} +
+ `; +} + +function renderScopeComparison(current: CheckResult, other: CheckResult | null): string { + if (!other) return ''; + return ` +
+
+

Root vs tab hostname

+ ${current.mode === 'apex' ? 'Current: root' : 'Current: tab'} +
+
+ Check + ${escapeHtml(current.queryHostname)} + ${escapeHtml(other.queryHostname)} +
+
+ ${renderComparisonValue('SPF', current.full.spf, other.full.spf)} + ${renderComparisonValue('DMARC', current.full.dmarc, other.full.dmarc)} + ${renderComparisonValue('DKIM', current.full.dkim, other.full.dkim)} +
+
+ `; +} + function dmarcHint(result: CheckResult): string { return `DMARC is always read from _dmarc.${result.dmarcLookupHost} (organisational domain of the tab). SPF and DKIM use ${result.queryHostname}.`; } @@ -498,11 +698,13 @@ function renderResult(result: CheckResult): void { (detailedBreakdown || result.spfMailProviderHint.status !== 'pass') ? renderSpfMailProviderHint(result.spfMailProviderHint) : ''; + const spfFooter = `${spfSupplement}${renderFixGuidance('SPF', result)}`; root.innerHTML = `
${renderHeaderBrand(result.queryHostname)} + ${renderManualLookupForm(result.queryHostname)} ${modeChips(result.mode, showExact)}
@@ -511,6 +713,10 @@ function renderResult(result: CheckResult): void {

SPF + DMARC + DKIM (max 10)

+ ${renderScoreExplanation(full)} + + ${renderScopeComparison(result, compareResult)} +
${renderProtocolCard( 'SPF', @@ -520,7 +726,7 @@ function renderResult(result: CheckResult): void { result.spfBreakdown, detailedBreakdown, undefined, - spfSupplement || undefined, + spfFooter || undefined, )} ${renderProtocolCard( 'DMARC', @@ -530,6 +736,7 @@ function renderResult(result: CheckResult): void { result.dmarcBreakdown, detailedBreakdown, dmarcHint(result), + renderFixGuidance('DMARC', result) || undefined, )} ${renderProtocolCard( 'DKIM', @@ -538,6 +745,8 @@ function renderResult(result: CheckResult): void { dkimRaw, result.dkimBreakdown, detailedBreakdown, + undefined, + renderFixGuidance('DKIM', result) || undefined, )} ${result.mailInfra .map((c) => @@ -560,12 +769,27 @@ function renderResult(result: CheckResult): void { ${castShameModal}
`; - bindModeButtons(result.mode, false); + bindModeButtons(result.mode, false, result.tabHostname, lastResultUpdatesToolbar); + bindManualLookupForm(); bindSettingsFab(); bindCastShameModal(result); + bindCopyReport(result); + bindScopeCompare(result); bindMailInfraCopyButtons(); } +function bindCopyReport(result: CheckResult): void { + const btn = document.getElementById('btn-copy-report'); + if (!(btn instanceof HTMLButtonElement)) return; + btn.addEventListener('click', () => void copyReport(result, btn)); +} + +function bindScopeCompare(result: CheckResult): void { + const btn = document.getElementById('btn-compare-scope'); + if (!(btn instanceof HTMLButtonElement)) return; + btn.addEventListener('click', () => void loadScopeComparison(result, btn)); +} + function renderSettings(): void { root.innerHTML = `
@@ -611,6 +835,13 @@ function renderSettings(): void {
Advanced
+