Hint or partially hide email address with stars (*) in PHP

At first, I thought that strpos() on @ would get me the length of the “local” part of the address and I could use substr_replace() to simply inject the asterisks BUT a valid email address can have multiple @ in the local part AND multibyte support is a necessary inclusion for any real-world project. This means that mb_strpos() would be an adequate replacement for strpos(), but there isn’t yet a native mb_substr_replace() function in php, so the convolution of devising a non-regex snippet became increasingly unattractive.

If you want to see my original answer, you can check the edit history, but I no longer endorse its use. My original answer also detailed how other answers on this page fail to obfuscate email addresses which have 1 or 2 characters in the local substring. If you are considering using any other answers, but sure to test against [email protected] and [email protected] as preliminary unit tests.

My snippet to follow DOES NOT validate an email address; it is assumed that your project will use appropriate methods to validate the address before bothering to allow it into your system. The power/utility of this snippet is that it is multibyte-safe and it will add asterisks in all scenarios and when there is only a single character in the local part, the leading character is repeated before the @ so that the mutated address is harder to guess. Oh, and the number of asterisks to be added is declared as a variable for simpler maintenance.

Code: (Demo) (PHP7.4+ Demo) (Regex Demo)

$minFill = 4;
echo preg_replace_callback(
         '/^(.)(.*?)([^@]?)(?=@[^@]+$)/u',
         function ($m) use ($minFill) {
              return $m[1]
                     . str_repeat("*", max($minFill, mb_strlen($m[2], 'UTF-8')))
                     . ($m[3] ?: $m[1]);
         },
         $email
     );

Input/Output:

'[email protected]'              => 'a****[email protected]',
'[email protected]'             => 'a****[email protected]',
'[email protected]'            => 'a****[email protected]',
'[email protected]'           => 'a****[email protected]',
'[email protected]'          => 'a****[email protected]',
'[email protected]'         => 'a****[email protected]',
'[email protected]'        => 'a*****[email protected]',
'Ф@example.com'              => 'Ф****Ф@example.com',
'ФѰ@example.com'             => 'Ф****Ѱ@example.com',
'ФѰД@example.com'            => 'Ф****Д@example.com',
'ФѰДӐӘӔӺ@example.com'        => 'Ф*****Ӻ@example.com',
'"a@tricky@one"@example.com' => '"************"@example.com',

Regex-planation:

/            #pattern delimiter
^            #start of string
(.)          #capture group #1 containing the first character
(.*?)        #capture group #2 containing zero or more characters (lazy, aka non-greedy)
([^@]?)      #capture group #3 containing an optional single non-@ character
(?=@[^@]+$)  #require that the next character is @ then one or more @ until the end of the string
/            #pattern delimiter
u            #unicode/multibyte pattern modifier

Callback explanation:

  • $m[1]
    the first character (capture group #1)
  • str_repeat("*", max($minFill, mb_strlen($m[2], 'UTF-8')))
    measure the multibyte length of capture group #2 using UTF-8 encoding, then use the higher value between that calculated length and the declared $minFill, then repeat the character * the number of times returned from the max() call.
  • ($m[3] ?: $m[1])
    the last character before the @ (capture group #3); if the element is empty in the $m array, then use the first element’s value — it will always be populated.

Leave a Comment