import { isAllowedRecipient } from '../../src/mailer'; // This function is the whole safety property of QA mail. If it says yes when it // should say no, a QA run emails a real customer; if it says no when it should // say yes, the regression test it was written for silently proves nothing. Both // directions are tested, and the dangerous direction is tested hardest. describe('isAllowedRecipient', () => { describe('with no allowlist configured', () => { // Production. It must send to whoever it is told to, or every customer // email in the product stops working. it('allows anyone when the variable is absent', () => { expect(isAllowedRecipient('anyone@example.com', undefined)).toBe(true); }); }); describe('with an allowlist configured', () => { const ALLOW = 'someone@gmail.com, @internal.example'; it('allows an address listed exactly', () => { expect(isAllowedRecipient('someone@gmail.com', ALLOW)).toBe(true); }); // The reason this feature exists: a tester invents a new plus-suffix per // run and must not have to edit the allowlist each time. it('allows any plus-suffixed variant of a listed address', () => { expect(isAllowedRecipient('someone+favtest1@gmail.com', ALLOW)).toBe(true); expect(isAllowedRecipient('someone+anything-at-all@gmail.com', ALLOW)).toBe(true); }); it('allows any address at a listed domain', () => { expect(isAllowedRecipient('whoever@internal.example', ALLOW)).toBe(true); }); it('refuses a different mailbox at a listed address domain', () => { expect(isAllowedRecipient('someone.else@gmail.com', ALLOW)).toBe(false); }); it('refuses an address that is not listed at all', () => { expect(isAllowedRecipient('realcustomer@example.com', ALLOW)).toBe(false); }); // The dangerous case. A suffix match rather than an equality check would // let an attacker-controlled domain ending in a listed one through. it('refuses a lookalike domain that merely ends with a listed one', () => { expect(isAllowedRecipient('someone@gmail.com.evil.example', ALLOW)).toBe(false); expect(isAllowedRecipient('whoever@not-internal.example', ALLOW)).toBe(false); }); // And the reverse of the same mistake, on the local part. it('refuses a local part that merely ends with a listed one', () => { expect(isAllowedRecipient('notsomeone@gmail.com', ALLOW)).toBe(false); }); it('ignores case on both sides', () => { expect(isAllowedRecipient('SomeOne+Test@GMAIL.com', 'someone@gmail.com')).toBe(true); expect(isAllowedRecipient('someone@gmail.com', 'SOMEONE@GMAIL.COM')).toBe(true); }); it('tolerates padding and empty entries in the list', () => { expect(isAllowedRecipient('someone@gmail.com', ' someone@gmail.com ,, ')).toBe(true); }); it('refuses a recipient that is not a usable address', () => { expect(isAllowedRecipient('not-an-address', ALLOW)).toBe(false); expect(isAllowedRecipient('', ALLOW)).toBe(false); expect(isAllowedRecipient('@nolocalpart.example', ALLOW)).toBe(false); }); }); // Fails closed rather than open. Someone who writes MAIL_ALLOWLIST= into a // compose file is expressing an intent to restrict, and reading that as // "unrestricted" would turn a typo into an outbound mail incident. describe('with the variable present but empty', () => { it('refuses everyone', () => { expect(isAllowedRecipient('someone@gmail.com', '')).toBe(false); expect(isAllowedRecipient('someone@gmail.com', ' ')).toBe(false); expect(isAllowedRecipient('someone@gmail.com', ' , , ')).toBe(false); }); }); });