* fix(email): make team org invites more transactional Use org-branded sender and subject instead of the default "Best from ClassroomIO" marketing identity. Include the inviter name in the body, remove promotional emojis, and thread optional subject overrides through the email job pipeline. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(email): escape invite fields and sanitize email headers Address Greptile review: escape inviter/org/role values in the team invite template, sanitize display names in buildEmailFromName, and strip control characters from dynamic subjects. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(email): use RFC 5322 backslash escaping in display names Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * test(email): add vitest coverage for email header helpers Cover escapeHtml, sanitizeEmailDisplayName, sanitizeEmailSubject, and buildEmailFromName. Collapse repeated whitespace when stripping control characters from header values. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(email): strip all ASCII control chars from email headers Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
64 lines
2.3 KiB
TypeScript
64 lines
2.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
buildEmailFromName,
|
|
escapeHtml,
|
|
sanitizeEmailDisplayName,
|
|
sanitizeEmailSubject
|
|
} from '../src/utils/functions/email-helpers';
|
|
|
|
const DEFAULT_FROM = '"Best from ClassroomIO" <notify@mail.classroomio.com>';
|
|
|
|
describe('escapeHtml', () => {
|
|
it('escapes HTML metacharacters', () => {
|
|
expect(escapeHtml(`Tom & Jerry's "Lab"`)).toBe('Tom & Jerry's "Lab"');
|
|
expect(escapeHtml('<img src=x onerror=alert(1)>')).toBe('<img src=x onerror=alert(1)>');
|
|
});
|
|
});
|
|
|
|
describe('sanitizeEmailDisplayName', () => {
|
|
it('strips control characters and trims whitespace', () => {
|
|
expect(sanitizeEmailDisplayName(' Acme\r\nAcademy\u0000 ')).toBe('Acme Academy');
|
|
expect(sanitizeEmailDisplayName('Acme\x01Academy\x7F')).toBe('Acme Academy');
|
|
});
|
|
|
|
it('backslash-escapes embedded quotes per RFC 5322 quoted-strings', () => {
|
|
expect(sanitizeEmailDisplayName('Alice "Admin" Academy')).toBe('Alice \\"Admin\\" Academy');
|
|
});
|
|
|
|
it('escapes backslashes before quotes', () => {
|
|
expect(sanitizeEmailDisplayName('Acme\\Corp "East"')).toBe('Acme\\\\Corp \\"East\\"');
|
|
});
|
|
});
|
|
|
|
describe('sanitizeEmailSubject', () => {
|
|
it('strips control characters and trims whitespace', () => {
|
|
expect(sanitizeEmailSubject(' Invite to Acme\r\n\rAcademy\u0000 ')).toBe('Invite to Acme Academy');
|
|
expect(sanitizeEmailSubject('Invite\x01to\x7FAcme')).toBe('Invite to Acme');
|
|
});
|
|
|
|
it('preserves quotes in subject text', () => {
|
|
expect(sanitizeEmailSubject('Join "Acme" today')).toBe('Join "Acme" today');
|
|
});
|
|
});
|
|
|
|
describe('buildEmailFromName', () => {
|
|
it('returns the default sender when name is missing or empty after sanitization', () => {
|
|
expect(buildEmailFromName()).toBe(DEFAULT_FROM);
|
|
expect(buildEmailFromName('')).toBe(DEFAULT_FROM);
|
|
expect(buildEmailFromName('\r\n\u0000')).toBe(DEFAULT_FROM);
|
|
});
|
|
|
|
it('wraps a plain display name in a quoted From header', () => {
|
|
expect(buildEmailFromName('Acme Academy (via ClassroomIO.com)')).toBe(
|
|
'"Acme Academy (via ClassroomIO.com)" <notify@mail.classroomio.com>'
|
|
);
|
|
});
|
|
|
|
it('sanitizes embedded quotes in the display name', () => {
|
|
expect(buildEmailFromName('Alice "Admin" Academy (via ClassroomIO.com)')).toBe(
|
|
'"Alice \\"Admin\\" Academy (via ClassroomIO.com)" <notify@mail.classroomio.com>'
|
|
);
|
|
});
|
|
});
|