This release significantly expands internationalization support and adds comprehensive identity management features. Internationalization (i18n): - Add 5 new languages: Spanish, Italian, German, Dutch, Portuguese - Expand from 3 to 8 total supported languages - Redesign language switcher for better scalability (dropdown UI) - Complete translations for all features across all languages Identity Management: - Multiple sender identities with per-identity signatures - Sub-addressing support (user+tag@domain.com) - Context-aware tag suggestions for sub-addresses - Identity badges in email viewer and list - Full CRUD operations for managing identities Newsletter Management: - RFC 2369 List-Unsubscribe support (one-click unsubscribe) - HTTP and mailto unsubscribe methods - Security validation prevents XSS attacks - Two-step confirmation with persistent dismissal Security & Accessibility: - Dark mode email readability (intelligent color transformation) - WCAG 2.0 Level AA color contrast compliance - Comprehensive XSS prevention with validation utilities - Unit test coverage for security-critical code (57 validation tests) Testing: - Add unit tests for validation utilities - Add unit tests for email sanitization - Add unit tests for color transformation - Full test coverage for XSS attack vectors
196 lines
6.8 KiB
TypeScript
196 lines
6.8 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
sanitizeEmailHtml,
|
|
sanitizeSignatureHtml,
|
|
parseHtmlSafely,
|
|
hasRichFormatting,
|
|
} from '../email-sanitization';
|
|
|
|
describe('email-sanitization', () => {
|
|
describe('sanitizeEmailHtml', () => {
|
|
it('should remove script tags', () => {
|
|
const malicious = '<p>Hello</p><script>alert("XSS")</script>';
|
|
const clean = sanitizeEmailHtml(malicious);
|
|
expect(clean).not.toContain('<script>');
|
|
expect(clean).toContain('Hello');
|
|
});
|
|
|
|
it('should remove event handlers', () => {
|
|
const malicious = '<img src="x" onerror="alert(\'XSS\')">';
|
|
const clean = sanitizeEmailHtml(malicious);
|
|
expect(clean).not.toContain('onerror');
|
|
});
|
|
|
|
it('should remove iframe, object, embed tags', () => {
|
|
const malicious = '<div>Content</div><iframe src="evil.com"></iframe><object></object>';
|
|
const clean = sanitizeEmailHtml(malicious);
|
|
expect(clean).not.toContain('<iframe');
|
|
expect(clean).not.toContain('<object');
|
|
expect(clean).toContain('Content');
|
|
});
|
|
|
|
it('should remove meta, link, base tags', () => {
|
|
const malicious = '<p>Text</p><meta charset="utf-8"><link rel="stylesheet" href="evil.css">';
|
|
const clean = sanitizeEmailHtml(malicious);
|
|
expect(clean).not.toContain('<meta');
|
|
expect(clean).not.toContain('<link');
|
|
expect(clean).toContain('Text');
|
|
});
|
|
|
|
it('should preserve safe HTML structure', () => {
|
|
const safe = '<p>Paragraph</p><div><span>Nested</span></div><table><tr><td>Cell</td></tr></table>';
|
|
const clean = sanitizeEmailHtml(safe);
|
|
expect(clean).toContain('<p>');
|
|
expect(clean).toContain('<div>');
|
|
expect(clean).toContain('<table>');
|
|
expect(clean).toContain('Cell');
|
|
});
|
|
|
|
it('should preserve safe attributes', () => {
|
|
const withAttrs = '<p style="color: red;" class="text">Styled</p>';
|
|
const clean = sanitizeEmailHtml(withAttrs);
|
|
expect(clean).toContain('style');
|
|
expect(clean).toContain('class');
|
|
});
|
|
|
|
it('should handle empty input', () => {
|
|
expect(sanitizeEmailHtml('')).toBe('');
|
|
expect(sanitizeEmailHtml(' ')).toBeTruthy();
|
|
});
|
|
|
|
it('should handle malformed HTML', () => {
|
|
const malformed = '<p>Unclosed<div>Tags';
|
|
const clean = sanitizeEmailHtml(malformed);
|
|
expect(clean).toContain('Unclosed');
|
|
expect(clean).toContain('Tags');
|
|
});
|
|
});
|
|
|
|
describe('sanitizeSignatureHtml', () => {
|
|
it('should allow basic formatting tags', () => {
|
|
const signature = '<p><strong>John Doe</strong><br><em>Software Engineer</em></p>';
|
|
const clean = sanitizeSignatureHtml(signature);
|
|
expect(clean).toContain('<strong>');
|
|
expect(clean).toContain('<em>');
|
|
expect(clean).toContain('John Doe');
|
|
});
|
|
|
|
it('should remove images from signatures', () => {
|
|
const signature = '<p>John</p><img src="logo.png" alt="Logo">';
|
|
const clean = sanitizeSignatureHtml(signature);
|
|
expect(clean).not.toContain('<img');
|
|
expect(clean).toContain('John');
|
|
});
|
|
|
|
it('should remove video and audio tags', () => {
|
|
const signature = '<p>John</p><video src="vid.mp4"></video><audio src="sound.mp3"></audio>';
|
|
const clean = sanitizeSignatureHtml(signature);
|
|
expect(clean).not.toContain('<video');
|
|
expect(clean).not.toContain('<audio');
|
|
});
|
|
|
|
it('should preserve links with safe attributes', () => {
|
|
const signature = '<p><a href="https://example.com" style="color: blue;">Website</a></p>';
|
|
const clean = sanitizeSignatureHtml(signature);
|
|
expect(clean).toContain('<a');
|
|
expect(clean).toContain('href');
|
|
expect(clean).toContain('example.com');
|
|
});
|
|
|
|
it('should remove script tags', () => {
|
|
const malicious = '<p>Signature</p><script>alert("XSS")</script>';
|
|
const clean = sanitizeSignatureHtml(malicious);
|
|
expect(clean).not.toContain('<script>');
|
|
expect(clean).toContain('Signature');
|
|
});
|
|
|
|
it('should handle empty signatures', () => {
|
|
expect(sanitizeSignatureHtml('')).toBe('');
|
|
expect(sanitizeSignatureHtml(' ')).toBe('');
|
|
});
|
|
|
|
it('should be stricter than email sanitization', () => {
|
|
const html = '<p>Text</p><img src="pic.jpg"><table><tr><td>Data</td></tr></table>';
|
|
const emailClean = sanitizeEmailHtml(html);
|
|
const signatureClean = sanitizeSignatureHtml(html);
|
|
|
|
// Email allows img and table
|
|
expect(emailClean).toContain('<img');
|
|
expect(emailClean).toContain('<table>');
|
|
|
|
// Signature blocks img but may allow some tables (verify in implementation)
|
|
expect(signatureClean).not.toContain('<img');
|
|
});
|
|
});
|
|
|
|
describe('parseHtmlSafely', () => {
|
|
it('should return a valid Document', () => {
|
|
const html = '<p>Test</p>';
|
|
const doc = parseHtmlSafely(html);
|
|
expect(doc).toBeInstanceOf(Document);
|
|
});
|
|
|
|
it('should not execute scripts', () => {
|
|
let executed = false;
|
|
const html = '<script>executed = true;</script>';
|
|
parseHtmlSafely(html);
|
|
expect(executed).toBe(false);
|
|
});
|
|
|
|
it('should handle malformed HTML gracefully', () => {
|
|
const malformed = '<p>Unclosed<div>Tags';
|
|
const doc = parseHtmlSafely(malformed);
|
|
expect(doc).toBeInstanceOf(Document);
|
|
expect(doc.body.textContent).toContain('Unclosed');
|
|
});
|
|
});
|
|
|
|
describe('hasRichFormatting', () => {
|
|
it('should detect tables', () => {
|
|
const html = '<table><tr><td>Data</td></tr></table>';
|
|
expect(hasRichFormatting(html)).toBe(true);
|
|
});
|
|
|
|
it('should detect images', () => {
|
|
const html = '<img src="pic.jpg">';
|
|
expect(hasRichFormatting(html)).toBe(true);
|
|
});
|
|
|
|
it('should detect inline styles', () => {
|
|
const html = '<div style="color: red;">Styled</div>';
|
|
expect(hasRichFormatting(html)).toBe(true);
|
|
});
|
|
|
|
it('should detect formatting tags', () => {
|
|
expect(hasRichFormatting('<b>Bold</b>')).toBe(true);
|
|
expect(hasRichFormatting('<strong>Strong</strong>')).toBe(true);
|
|
expect(hasRichFormatting('<em>Emphasized</em>')).toBe(true);
|
|
});
|
|
|
|
it('should detect headings', () => {
|
|
expect(hasRichFormatting('<h1>Title</h1>')).toBe(true);
|
|
expect(hasRichFormatting('<h3>Subtitle</h3>')).toBe(true);
|
|
});
|
|
|
|
it('should detect lists', () => {
|
|
expect(hasRichFormatting('<ul><li>Item</li></ul>')).toBe(true);
|
|
expect(hasRichFormatting('<ol><li>Item</li></ol>')).toBe(true);
|
|
});
|
|
|
|
it('should return false for plain text', () => {
|
|
const plain = '<p>Just plain text</p>';
|
|
expect(hasRichFormatting(plain)).toBe(false);
|
|
});
|
|
|
|
it('should return false for simple paragraphs', () => {
|
|
const simple = '<p>Line 1</p><p>Line 2</p>';
|
|
expect(hasRichFormatting(simple)).toBe(false);
|
|
});
|
|
|
|
it('should handle empty HTML', () => {
|
|
expect(hasRichFormatting('')).toBe(false);
|
|
expect(hasRichFormatting(' ')).toBe(false);
|
|
});
|
|
});
|
|
});
|