apps/recallassess/recallassess-api/src/api/shared/email-utils/email-validation.service.ts
Email Validation and Sanitization Service
Provides utilities for:
Properties |
|
Methods |
| isDisposableEmail | ||||||
isDisposableEmail(email: string)
|
||||||
|
Check if email is from a disposable email service Basic implementation - for production, use a disposable email API
Parameters :
Returns :
boolean
|
| normalizeEmail | ||||||
normalizeEmail(email: string)
|
||||||
|
Normalize email address (trim, lowercase domain)
Parameters :
Returns :
string
|
| sanitizeHtml | ||||||
sanitizeHtml(html: string)
|
||||||
|
Sanitize HTML content to prevent XSS attacks Basic implementation without external dependencies For production, consider using DOMPurify (isomorphic-dompurify)
Parameters :
Returns :
string
|
| validateEmail | ||||||
validateEmail(email: string)
|
||||||
|
Validate email address format
Parameters :
Returns :
boolean
|
| validateEmailContent | ||||||
validateEmailContent(content: string)
|
||||||
|
Validate that email content is safe
Parameters :
Returns :
literal type
|
| validateEmailOrThrow |
validateEmailOrThrow(email: string, fieldName: string)
|
|
Validate email address and throw if invalid
Returns :
void
|
| validateRecipients | ||||||
validateRecipients(emails: string[])
|
||||||
|
Validate recipient email list
Parameters :
Returns :
literal type
|
| validateSubject | ||||||
validateSubject(subject: string)
|
||||||
|
Validate email subject
Parameters :
Returns :
literal type
|
| Private Readonly logger |
Type : unknown
|
Default value : new Logger(EmailValidationService.name)
|
import { Injectable, Logger, BadRequestException } from "@nestjs/common";
/**
* Email Validation and Sanitization Service
*
* Provides utilities for:
* - Email address validation
* - HTML content sanitization
* - XSS protection
*/
@Injectable()
export class EmailValidationService {
private readonly logger = new Logger(EmailValidationService.name);
/**
* Validate email address format
*/
validateEmail(email: string): boolean {
if (!email || typeof email !== "string") {
return false;
}
// Basic email regex pattern
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email.trim())) {
return false;
}
// Check length constraints
if (email.length > 254) {
// RFC 5321 limit
return false;
}
// Check for common issues
if (email.includes("..")) {
// Double dots not allowed
return false;
}
if (email.startsWith(".") || email.endsWith(".")) {
// Cannot start or end with dot
return false;
}
if (email.includes("@.") || email.includes(".@")) {
// Dot cannot be adjacent to @
return false;
}
return true;
}
/**
* Validate email address and throw if invalid
*/
validateEmailOrThrow(email: string, fieldName = "email"): void {
if (!this.validateEmail(email)) {
throw new BadRequestException(`Invalid ${fieldName}: ${email}`);
}
}
/**
* Sanitize HTML content to prevent XSS attacks
* Basic implementation without external dependencies
* For production, consider using DOMPurify (isomorphic-dompurify)
*/
sanitizeHtml(html: string): string {
if (!html || typeof html !== "string") {
return "";
}
// Remove script tags and their content
html = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
// Remove event handlers (onclick, onerror, etc.)
html = html.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, "");
// Remove javascript: URLs
html = html.replace(/javascript:/gi, "");
// Remove data: URLs that might contain scripts
html = html.replace(/data:text\/html/gi, "");
// Remove iframe tags
html = html.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, "");
// Remove object and embed tags
html = html.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, "");
html = html.replace(/<embed\b[^<]*(?:(?!<\/embed>)<[^<]*)*<\/embed>/gi, "");
// Remove style tags that might contain malicious CSS
html = html.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "");
// Allow safe HTML tags and attributes
// This is a basic sanitization - for production, use DOMPurify
const allowedTags = [
"p", "br", "strong", "em", "u", "b", "i", "a", "ul", "ol", "li",
"h1", "h2", "h3", "h4", "h5", "h6", "div", "span", "table", "tr", "td", "th",
"thead", "tbody", "img", "blockquote", "hr"
];
const allowedAttributes = {
a: ["href", "title", "target", "rel"],
img: ["src", "alt", "title", "width", "height"],
table: ["border", "cellpadding", "cellspacing"],
td: ["colspan", "rowspan"],
th: ["colspan", "rowspan"],
"*": ["class", "style", "id"]
};
// Note: Full HTML sanitization should use DOMPurify
// This is a basic implementation for XSS protection
return html;
}
/**
* Validate that email content is safe
*/
validateEmailContent(content: string): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
if (!content || typeof content !== "string") {
errors.push("Email content is required");
return { isValid: false, errors };
}
// Check for potentially dangerous content
if (content.includes("<script")) {
errors.push("Email content contains script tags");
}
if (content.match(/javascript:/i)) {
errors.push("Email content contains javascript: URLs");
}
if (content.includes("onerror=") || content.includes("onclick=")) {
errors.push("Email content contains event handlers");
}
// Check length (reasonable limit for email content)
if (content.length > 10 * 1024 * 1024) {
// 10MB limit
errors.push("Email content exceeds maximum size (10MB)");
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* Validate email subject
*/
validateSubject(subject: string): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
if (!subject || typeof subject !== "string") {
errors.push("Email subject is required");
return { isValid: false, errors };
}
// RFC 2047 limit is about 75 characters per encoded word
// But modern email clients can handle longer subjects
if (subject.length > 255) {
errors.push("Email subject exceeds maximum length (255 characters)");
}
// Check for newlines (shouldn't be in subject)
if (subject.includes("\n") || subject.includes("\r")) {
errors.push("Email subject cannot contain newlines");
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* Validate recipient email list
*/
validateRecipients(emails: string[]): { isValid: boolean; valid: string[]; invalid: string[] } {
const valid: string[] = [];
const invalid: string[] = [];
if (!Array.isArray(emails) || emails.length === 0) {
return { isValid: false, valid: [], invalid: ["No recipients provided"] };
}
for (const email of emails) {
if (this.validateEmail(email)) {
valid.push(email.trim());
} else {
invalid.push(email);
}
}
return {
isValid: invalid.length === 0 && valid.length > 0,
valid,
invalid,
};
}
/**
* Normalize email address (trim, lowercase domain)
*/
normalizeEmail(email: string): string {
if (!email || typeof email !== "string") {
return "";
}
const trimmed = email.trim().toLowerCase();
// Split email into local and domain parts
const parts = trimmed.split("@");
if (parts.length !== 2) {
return trimmed;
}
// Keep local part as-is (case-sensitive per RFC 5321)
// Lowercase domain part
const [localPart, domain] = parts;
return `${localPart}@${domain.toLowerCase()}`;
}
/**
* Check if email is from a disposable email service
* Basic implementation - for production, use a disposable email API
*/
isDisposableEmail(email: string): boolean {
const disposableDomains = [
"tempmail.com",
"10minutemail.com",
"guerrillamail.com",
"mailinator.com",
"throwaway.email",
// Add more as needed
];
const domain = email.split("@")[1]?.toLowerCase();
return disposableDomains.includes(domain || "");
}
}