File

apps/recallassess/recallassess-api/src/api/shared/email-utils/email-validation.service.ts

Description

Email Validation and Sanitization Service

Provides utilities for:

  • Email address validation
  • HTML content sanitization
  • XSS protection

Index

Properties
Methods

Methods

isDisposableEmail
isDisposableEmail(email: string)

Check if email is from a disposable email service Basic implementation - for production, use a disposable email API

Parameters :
Name Type Optional
email string No
Returns : boolean
normalizeEmail
normalizeEmail(email: string)

Normalize email address (trim, lowercase domain)

Parameters :
Name Type Optional
email string No
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 :
Name Type Optional
html string No
Returns : string
validateEmail
validateEmail(email: string)

Validate email address format

Parameters :
Name Type Optional
email string No
Returns : boolean
validateEmailContent
validateEmailContent(content: string)

Validate that email content is safe

Parameters :
Name Type Optional
content string No
Returns : literal type
validateEmailOrThrow
validateEmailOrThrow(email: string, fieldName: string)

Validate email address and throw if invalid

Parameters :
Name Type Optional Default value
email string No
fieldName string No "email"
Returns : void
validateRecipients
validateRecipients(emails: string[])

Validate recipient email list

Parameters :
Name Type Optional
emails string[] No
Returns : literal type
validateSubject
validateSubject(subject: string)

Validate email subject

Parameters :
Name Type Optional
subject string No
Returns : literal type

Properties

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

results matching ""

    No results matching ""