Skip to content

[Feature]: Add hideEmail method for email masking (privacy/LGPD) #25

@viniciusvts

Description

@viniciusvts

Feature Description

Implement a hideEmail method that intelligently masks parts of an email address for privacy and data protection compliance (LGPD, GDPR). The method should obscure the local part (before @) and domain name while keeping the structure recognizable, e.g., [email protected] becomes jo***.*****@ex*****.com.

Use Case

This feature would solve privacy and compliance challenges when:

  • Displaying user email addresses in admin panels, dashboards, or logs (LGPD/GDPR compliance)
  • Showing partial email information for account verification ("We sent a code to jo***@em*****.com")
  • Logging authentication attempts or security events without exposing full email addresses
  • Displaying user lists in customer support tools while protecting PII (Personally Identifiable Information)
  • Showing email preview in "forgot password" flows ("Reset link sent to j***@gm***.com")
  • Exporting user data for analytics while maintaining privacy
  • Audit trails and compliance reports that reference users without exposing full contact details
  • Public-facing user profiles where email visibility is restricted

Current workaround limitations:
Developers currently need to manually implement email masking logic with complex string manipulation:

// Manual implementation (error-prone, inconsistent)
$email = 'joao.silva@example. com';
[$local, $domain] = explode('@', $email);
$maskedLocal = substr($local, 0, 2) . '***';
$maskedDomain = substr($domain, 0, 2) . '***.' . substr($domain, -3);
$masked = $maskedLocal . '@' . $maskedDomain;
// Result inconsistent, doesn't handle edge cases

This method would provide a standardized, tested, and reliable API for email masking that follows best practices.

Real-world scenarios:

// Admin dashboard - user list
$users = User::all();
foreach ($users as $user) {
    echo SM::hideEmail($user->email); 
    // 'jo***@ex*****.com' instead of '[email protected]'
}

// Account verification flow
$message = "We sent a verification code to " . SM::hideEmail($user->email);
// "We sent a verification code to us****@gm***.com"

// Security logs (LGPD compliant)
Log::info('Failed login attempt', [
    'email' => SM::hideEmail($request->email),
    'ip' => $request->ip()
]);
// Logs: 'email' => 'ad***@co*****.com' (not full PII)

// Forgot password confirmation
flash()->success('If this email exists, reset link sent to ' . SM::hideEmail($email));
// "... sent to ma***@ya***.com"

// Customer support ticket view
$ticket->user_email = SM::hideEmail($ticket->user_email);
// Support sees 'su****@ho*****.com' instead of full email

// Analytics export (anonymized)
$export = User::select(['id', DB::raw('email')])->get()->map(function($user) {
    return ['id' => $user->id, 'email_masked' => SM::hideEmail($user->email)];
});
// CSV contains 'us***@do*****.com' patterns

// Public profile display
$profile->public_email = $user->show_email 
    ? $user->email 
    : SM::hideEmail($user->email);
// Shows 'co****@pr*****.org' if privacy enabled

Static Usage

use SSolWEB\StringMorpher\StringMorpher as SM;

// Basic email masking
$safe = SM::hideEmail('[email protected]');
echo $safe; // 'us****@em***.com'

// Long local part
$safe = SM::hideEmail('joao.silva. [email protected]');
echo $safe; // 'jo***.*****.*****@ex*****.com'

// Short email
$safe = SM::hideEmail('[email protected]');
echo $safe; // 'a*@c*. com'

// Complex domain
$safe = SM::hideEmail('[email protected]');
echo $safe; // 'us**@ma**. ex*****.co. uk'

// Corporate email
$safe = SM:: hideEmail('[email protected]');
echo $safe; // 'fi********.*******@co*****.com'

Fluent Usage

use SSolWEB\StringMorpher\StringMorpher as SM;

// Chain with trimming (user input)
$safeEmail = SM::make('  [email protected]  ')
    ->trim()
    ->toLower()
    ->hideEmail();
    
echo $safeEmail; // 'us**@ex*****.com'

// Integrate in notification messages
$message = SM::make('Verification sent to:  ')
    ->append($userEmail)
    ->hideEmail();
    
echo $message; // 'Verification sent to:  jo***@gm***.com'

// Log formatting
$logEntry = SM::make($request->email)
    ->trim()
    ->toLower()
    ->hideEmail()
    ->prepend('[AUTH] Email:  ')
    ->getString();
    
Log::info($logEntry); // '[AUTH] Email: ad***@do*****.com'

// Privacy-compliant export
$users->map(function($user) {
    return [
        'id' => $user->id,
        'name' => SM::make($user->name)->capitalize(),
        'email' => SM:: make($user->email)->hideEmail()->getString(),
        'created' => $user->created_at
    ];
});

// Dynamic masking based on permission
$displayEmail = SM::make($email);
if (! auth()->user()->can('view_full_emails')) {
    $displayEmail->hideEmail();
}
echo $displayEmail; // Masked or full based on permission

Technical Considerations

The implementation should:

  • Validate email format before masking (basic structure check)
  • Preserve email structure: keep @ and . visible for context
  • Mask local part: show first 2 chars, mask rest with *** (or proportional to length)
  • Mask domain: show first 2 chars of domain name, mask middle, preserve TLD
  • Handle edge cases:
  • Consistent masking pattern: same email always produces same mask (deterministic)
  • No reversibility: masked output should not allow reconstruction of original

Algorithm suggestion:

  1. Split email by @ into local and domain parts
  2. For local part:
    • If length ≤ 3: show 1 char + **
    • If length > 3: show first 2 chars + *** + (handle dots in between)
  3. For domain part:
    • Extract TLD (last segment after final .)
    • Mask domain name (first segment): show 2 chars + ***
    • Preserve structure: [email protected]
  4. Combine: local@domain

Privacy note:
This method supports LGPD (Lei Geral de Proteção de Dados - Brazil) and GDPR compliance by minimizing exposure of personal data in logs, displays, and exports while maintaining enough context for user recognition.

Checklist

  • I have read the documentation
  • I have read the existing Methods documentation
  • I have searched for similar feature requests

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions