PSA/shared/lib/email/__tests__/listRewriteSender.test.ts
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

153 lines
6.0 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest';
import {
computeListRewriteSender,
resolveListRewriteSender,
type HeaderBag,
} from '../listRewriteSender';
// All addresses below use RFC 2606 reserved example domains (no real PII).
const AUTHOR_AUTH =
'mx.example-mta.com; dkim=pass header.i=@vendor.example header.s=sel1 header.b=AbCdEf; ' +
'spf=pass (example-mta.com: domain of jane.doe@vendor.example designates 203.0.113.10 as permitted sender) ' +
'smtp.mailfrom=jane.doe@vendor.example; dmarc=pass (p=QUARANTINE sp=NONE dis=NONE) header.from=vendor.example';
function groupHeaders(overrides: Partial<HeaderBag> = {}): HeaderBag {
return {
'x-beenthere': 'support@lists.example.com',
'list-id': '<support.lists.example.com>',
'x-original-sender': 'jane.doe@vendor.example',
'reply-to': 'Jane Doe <jane.doe@vendor.example>',
'authentication-results': AUTHOR_AUTH,
...overrides,
};
}
describe('computeListRewriteSender', () => {
it('recovers the original sender from a mailing-list DMARC rewrite', () => {
const result = computeListRewriteSender(groupHeaders(), {
email: 'support@lists.example.com',
name: "'Jane Doe' via support",
});
expect(result).toEqual({
sender: { email: 'jane.doe@vendor.example', name: undefined },
listAddress: 'support@lists.example.com',
via: 'x-original-sender',
});
});
it('prefers X-Original-From over X-Original-Sender and Reply-To', () => {
const result = computeListRewriteSender(
groupHeaders({ 'x-original-from': 'jane.doe@vendor.example' }),
{ email: 'support@lists.example.com' }
);
expect(result?.via).toBe('x-original-from');
expect(result?.sender.email).toBe('jane.doe@vendor.example');
});
it('falls back to Reply-To when no X-Original-* header is present', () => {
const headers = groupHeaders();
delete headers['x-original-sender'];
const result = computeListRewriteSender(headers, { email: 'support@lists.example.com' });
expect(result?.via).toBe('reply-to');
expect(result?.sender.email).toBe('jane.doe@vendor.example');
});
it('returns null for ordinary direct mail (no list markers)', () => {
const result = computeListRewriteSender(
{ 'authentication-results': AUTHOR_AUTH },
{ email: 'jane.doe@vendor.example' }
);
expect(result).toBeNull();
});
it('returns null when From was NOT rewritten to the list address', () => {
// Normal list mail that preserves the author in From.
const result = computeListRewriteSender(groupHeaders(), {
email: 'jane.doe@vendor.example',
});
expect(result).toBeNull();
});
it('returns null when the recovered sender is not DKIM/DMARC/SPF aligned (anti-spoof)', () => {
const headers = groupHeaders({
'x-original-sender': 'attacker@evil.example',
'reply-to': 'attacker@evil.example',
});
// Auth results still only vouch for vendor.example, not evil.example.
const result = computeListRewriteSender(headers, { email: 'support@lists.example.com' });
expect(result).toBeNull();
});
it('returns null when no Authentication-Results header is available', () => {
const headers = groupHeaders();
delete headers['authentication-results'];
const result = computeListRewriteSender(headers, { email: 'support@lists.example.com' });
expect(result).toBeNull();
});
it('excludes spam relays whose recovered sender fails auth alignment', () => {
// Mirrors the observed spam pattern: From rewritten to a junk list domain,
// X-Original-Sender claims a brand, but auth does not vouch for it.
const headers: HeaderBag = {
'x-beenthere': 'list@spam.example',
'list-id': '<bulk.spam.example>',
'x-original-sender': 'support@bank.example',
'authentication-results': 'mx.example-mta.com; dkim=none; spf=softfail; dmarc=fail header.from=spam.example',
};
const result = computeListRewriteSender(headers, { email: 'list@spam.example' });
expect(result).toBeNull();
});
it('accepts subdomain-aligned auth domains', () => {
const headers = groupHeaders({
'x-original-sender': 'user@mail.vendor.example',
'reply-to': 'user@mail.vendor.example',
});
const result = computeListRewriteSender(headers, { email: 'support@lists.example.com' });
expect(result?.sender.email).toBe('user@mail.vendor.example');
});
});
describe('resolveListRewriteSender (mailparser shape + feature flag)', () => {
const FLAG = 'INBOUND_RESOLVE_LIST_ORIGINAL_SENDER';
afterEach(() => {
delete process.env[FLAG];
});
function parsedFixture() {
return {
from: { value: [{ address: 'support@lists.example.com', name: "'Jane Doe' via support" }] },
headerLines: [
{ key: 'authentication-results', line: `Authentication-Results: ${AUTHOR_AUTH}` },
{ key: 'x-beenthere', line: 'X-BeenThere: support@lists.example.com' },
{ key: 'x-original-sender', line: 'X-Original-Sender: jane.doe@vendor.example' },
{ key: 'reply-to', line: 'Reply-To: Jane Doe <jane.doe@vendor.example>' },
{ key: 'from', line: "From: 'Jane Doe' via support <support@lists.example.com>" },
],
};
}
it('reads headerLines and recovers the original sender', () => {
const result = resolveListRewriteSender(parsedFixture());
expect(result?.sender.email).toBe('jane.doe@vendor.example');
expect(result?.listAddress).toBe('support@lists.example.com');
});
it('returns null when the feature flag is disabled', () => {
process.env[FLAG] = 'false';
expect(resolveListRewriteSender(parsedFixture())).toBeNull();
});
it('keeps the topmost Authentication-Results when duplicated', () => {
const fixture = parsedFixture();
// A later (lower) auth header for a different domain must not win.
fixture.headerLines.push({
key: 'authentication-results',
line: 'Authentication-Results: relay.example; dmarc=pass header.from=evil.example',
});
const result = resolveListRewriteSender(fixture);
expect(result?.sender.email).toBe('jane.doe@vendor.example');
});
});