Files
worldmonitor/tests/resend-sender-normalize.test.mjs
Elie Habib 29306008e4 fix(email): route Intelligence Brief off the alerts@ mailbox (#3321)
* fix(email): route Intelligence Brief off the alerts@ mailbox

The daily "WorldMonitor Intelligence Brief" email was shipping from
`alerts@worldmonitor.app` with a display name that — if the Railway env
override dropped the `Name <…>` wrapper — Gmail/Outlook fell back to
rendering the local-part ("alerts" / "alert") as the sender name.
Recipients saw a scary-looking "alert" in their inbox for what is
actually a curated editorial read.

Split the sender so editorial mail can't share the `alerts@` mailbox
with incident pushes:

- New env var `RESEND_FROM_BRIEF` (default `WorldMonitor Brief
  <brief@worldmonitor.app>`) consumed by seed-digest-notifications.mjs.
- Falls back to `RESEND_FROM_EMAIL`, then to the built-in default, so
  existing deploys keep working and the rollout is a single Railway
  env flip on the digest service.
- notification-relay.cjs (realtime push alerts) intentionally keeps
  `RESEND_FROM_EMAIL` / `alerts@` — accurate for that path.
- .env.example documents the display-name rule so the bare-address
  trap can't re-introduce the bug.

Rollout: set `RESEND_FROM_BRIEF=WorldMonitor Brief <brief@worldmonitor.app>`
on the `seed-digest-notifications` Railway service. Domain-level Resend
verification already covers the new local-part; no DNS change needed.

* fix(email): runtime normalize sender to prevent bare-address regression

PR review feedback from codex:

  > P2 — RESEND_FROM_BRIEF is consumed verbatim, so an operator can
  > still set brief@worldmonitor.app without a display name and
  > recreate the same Gmail/Outlook rendering bug for the daily brief.
  > Today that protection is only documentation in .env.example, not
  > runtime enforcement.

Add a small shared helper `scripts/lib/resend-from.cjs` that coerces a
bare email address into a "Name <addr>" wrapper with a loud warning
log, and wire it into the digest path.

- Bare-address input (e.g. `brief@worldmonitor.app`) is rewritten to
  `WorldMonitor Brief <brief@worldmonitor.app>` so Gmail/Outlook stop
  falling back to the local-part as the display name.
- Coercion emits a single `console.warn` line per boot so operators
  see the signal in Railway logs and can fix the underlying env.
- Fail-safe (not fail-closed) — a misconfigured env does NOT take the
  cron down.

Also resolves the P3 doc-vs-runtime divergence by reverting
.env.example's RESEND_FROM_EMAIL default from "WorldMonitor Alerts
<...>" back to "WorldMonitor <...>" to match the existing
notification-relay.cjs runtime default. The realtime-alert path will
get the same normalizer treatment in a follow-up PR that cohesively
touches notification-relay.cjs + Dockerfile.relay.

tests: 7 new cases in tests/resend-sender-normalize.test.mjs covering
empty/null/whitespace input, wrapped passthrough, trim, bare-address
coercion, warning emission, no-warning on wrapped, console.warn default
sink. Runs under `npm run test:data`.
2026-04-23 08:51:27 +04:00

76 lines
2.7 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const { normalizeResendSender } = require('../scripts/lib/resend-from.cjs');
const silent = () => {};
test('returns null for empty, null, undefined, or whitespace-only input', () => {
assert.equal(normalizeResendSender(null, 'WorldMonitor', silent), null);
assert.equal(normalizeResendSender(undefined, 'WorldMonitor', silent), null);
assert.equal(normalizeResendSender('', 'WorldMonitor', silent), null);
assert.equal(normalizeResendSender(' ', 'WorldMonitor', silent), null);
});
test('passes a properly wrapped sender through unchanged', () => {
assert.equal(
normalizeResendSender('WorldMonitor <alerts@worldmonitor.app>', 'Default', silent),
'WorldMonitor <alerts@worldmonitor.app>',
);
assert.equal(
normalizeResendSender('WorldMonitor Brief <brief@worldmonitor.app>', 'Default', silent),
'WorldMonitor Brief <brief@worldmonitor.app>',
);
});
test('trims surrounding whitespace before returning a wrapped sender', () => {
assert.equal(
normalizeResendSender(' WorldMonitor Brief <brief@worldmonitor.app> ', 'Default', silent),
'WorldMonitor Brief <brief@worldmonitor.app>',
);
});
test('wraps a bare email address with the supplied default display name', () => {
assert.equal(
normalizeResendSender('brief@worldmonitor.app', 'WorldMonitor Brief', silent),
'WorldMonitor Brief <brief@worldmonitor.app>',
);
assert.equal(
normalizeResendSender('alerts@worldmonitor.app', 'WorldMonitor Alerts', silent),
'WorldMonitor Alerts <alerts@worldmonitor.app>',
);
});
test('emits exactly one warning when coercing a bare address', () => {
const warnings = [];
normalizeResendSender('brief@worldmonitor.app', 'WorldMonitor Brief', (m) => warnings.push(m));
assert.equal(warnings.length, 1);
assert.match(warnings[0], /lacks display name/);
assert.match(warnings[0], /WorldMonitor Brief <brief@worldmonitor\.app>/);
});
test('does not warn when the value already has a display-name wrapper', () => {
const warnings = [];
normalizeResendSender(
'WorldMonitor Brief <brief@worldmonitor.app>',
'Default',
(m) => warnings.push(m),
);
assert.equal(warnings.length, 0);
});
test('defaults to console.warn when no warning sink is supplied', () => {
const original = console.warn;
const captured = [];
console.warn = (m) => captured.push(m);
try {
normalizeResendSender('bare@example.com', 'Name');
assert.equal(captured.length, 1);
assert.match(captured[0], /lacks display name/);
} finally {
console.warn = original;
}
});