Files
SRCmail/lib/__tests__/email-sanitization.test.ts
T
Matthieu MALVACHEandMatthieu MALVACHE 5d273e2109 feat: expand internationalization and add identity management
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
2026-01-08 22:15:32 +01:00

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);
});
});
});