Initial commit

This commit is contained in:
2026-02-10 19:39:08 -05:00
commit 2a251f9b3f
32 changed files with 6135 additions and 0 deletions

160
src/utile/emailSanitizer.ts Normal file
View File

@@ -0,0 +1,160 @@
import DOMPurify from 'dompurify'
export enum SecurityLevel {
STRICT = 'strict',
MODERATE = 'moderate',
RELAXED = 'relaxed'
}
export interface SanitizationOptions {
securityLevel?: SecurityLevel
allowImages?: boolean
allowExternalLinks?: boolean
allowStyles?: boolean
}
type SanitizerConfig = {
ALLOWED_TAGS?: string[]
ALLOWED_ATTR?: string[]
ALLOWED_URI_REGEXP?: RegExp
ALLOW_DATA_ATTR?: boolean
FORBID_TAGS?: string[]
FORBID_ATTR?: string[]
KEEP_CONTENT?: boolean
RETURN_TRUSTED_TYPE?: boolean
}
export class EmailSanitizer {
private static readonly STRICT_CONFIG: SanitizerConfig = {
ALLOWED_TAGS: [
'p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li', 'blockquote',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span', 'b', 'i'
],
ALLOWED_ATTR: ['title'],
ALLOW_DATA_ATTR: false,
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'img', 'style', 'link', 'base', 'meta', 'svg', 'math'],
FORBID_ATTR: ['style', 'onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onmousemove', 'onmouseenter', 'onmouseleave'],
KEEP_CONTENT: true,
RETURN_TRUSTED_TYPE: false
}
private static readonly MODERATE_CONFIG: SanitizerConfig = {
ALLOWED_TAGS: [
'p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li', 'blockquote',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span', 'table', 'thead',
'tbody', 'tr', 'td', 'th', 'b', 'i', 'hr', 'pre', 'code', 'center',
'font', 'small', 'big', 'sub', 'sup'
],
ALLOWED_ATTR: [
'href', 'title', 'class', 'colspan', 'rowspan', 'align', 'valign',
'style', 'color', 'bgcolor', 'width', 'height', 'border', 'cellpadding',
'cellspacing', 'size', 'face'
],
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
ALLOW_DATA_ATTR: false,
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'img', 'style', 'link', 'base', 'meta'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onmousemove', 'onmouseenter', 'onmouseleave'],
KEEP_CONTENT: true,
RETURN_TRUSTED_TYPE: false
}
private static readonly RELAXED_CONFIG: SanitizerConfig = {
ALLOWED_TAGS: [
'p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li', 'blockquote',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span', 'table', 'thead',
'tbody', 'tr', 'td', 'th', 'b', 'i', 'hr', 'pre', 'code', 'center',
'font', 'small', 'big', 'sub', 'sup', 'img', 'picture', 'source'
],
ALLOWED_ATTR: [
'href', 'title', 'class', 'colspan', 'rowspan', 'align', 'valign',
'style', 'color', 'bgcolor', 'width', 'height', 'border', 'cellpadding',
'cellspacing', 'size', 'face', 'src', 'alt', 'srcset', 'loading'
],
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|data|cid):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
ALLOW_DATA_ATTR: false,
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'style', 'link', 'base', 'meta'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onmousemove', 'onmouseenter', 'onmouseleave'],
KEEP_CONTENT: true,
RETURN_TRUSTED_TYPE: false
}
/**
* Sanitize HTML email content based on security level and options
*/
static sanitize(html: string, options: SanitizationOptions = {}): string {
const {
securityLevel = SecurityLevel.MODERATE,
allowImages = false,
allowExternalLinks = true,
allowStyles = true
} = options
// Get base configuration
let config: SanitizerConfig
switch (securityLevel) {
case SecurityLevel.STRICT:
config = { ...this.STRICT_CONFIG }
break
case SecurityLevel.RELAXED:
config = { ...this.RELAXED_CONFIG }
break
case SecurityLevel.MODERATE:
default:
config = { ...this.MODERATE_CONFIG }
}
// Adjust configuration based on options
if (allowImages && config.ALLOWED_TAGS && config.ALLOWED_ATTR) {
// Add image tags
if (!config.ALLOWED_TAGS.includes('img')) {
config.ALLOWED_TAGS.push('img', 'picture', 'source')
}
// Add image attributes
const imageAttrs = ['src', 'alt', 'srcset', 'loading']
imageAttrs.forEach(attr => {
if (!config.ALLOWED_ATTR!.includes(attr)) {
config.ALLOWED_ATTR!.push(attr)
}
})
// Allow data URIs and CID references
config.ALLOWED_URI_REGEXP = /^(?:(?:(?:f|ht)tps?|mailto|tel|data|cid):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
// Remove img from forbidden tags
if (config.FORBID_TAGS) {
config.FORBID_TAGS = config.FORBID_TAGS.filter((tag: string) => tag !== 'img')
}
}
if (!allowExternalLinks && config.ALLOWED_TAGS) {
// Remove anchor tags
config.ALLOWED_TAGS = config.ALLOWED_TAGS.filter((tag: string) => tag !== 'a')
}
if (!allowStyles && config.ALLOWED_ATTR) {
// Remove style-related attributes
config.ALLOWED_ATTR = config.ALLOWED_ATTR.filter(
(attr: string) => !['style', 'color', 'bgcolor'].includes(attr)
)
}
return DOMPurify.sanitize(html, config) as string
}
/**
* Get security level description
*/
static getSecurityLevelDescription(level: SecurityLevel): string {
switch (level) {
case SecurityLevel.STRICT:
return 'Maximum security - Only basic text formatting allowed'
case SecurityLevel.MODERATE:
return 'Balanced security - Allows formatting, links, and tables (recommended)'
case SecurityLevel.RELAXED:
return 'Minimal filtering - Allows most content including images'
default:
return ''
}
}
}