M · UDF
fnEmailAnonimo
Mascara um e-mail trocando por * uma parte dos caracteres da parte local e do nome do domínio, em posições pseudoaleatórias e estáveis entre refreshes.
Sintaxe
fnEmailAnonimo(email as nullable text, optional proporcao as nullable number) as nullable textArgumentos
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| nullable text | Sim | E-mail a mascarar. | |
| proporcao | nullable number | Não | Fração dos caracteres elegíveis que vira *. De 0.1 a 1; padrão 0.5. |
Retorno
nullable text
Código completo
let
fnEmailAnonimo = (email as nullable text, optional proporcao as nullable number) as nullable text =>
let
vEmail = Text.Lower(Text.Trim(email ?? "")),
vPartes = Text.Split(vEmail, "@"),
// Exatamente um "@", parte local preenchida e domínio com ponto (não no início)
// "and" para no primeiro false: vPartes{1} só é lido quando há 2 partes
vValido = List.Count(vPartes) = 2 and vPartes{0} <> "" and Text.PositionOf(vPartes{1}, ".") > 0,
// Proporção por parte: padrão 50%, limitada entre 10% e 100%
vProporcao = List.Max({0.1, List.Min({1, proporcao ?? 0.5})}),
// Mascara um trecho mantendo o 1º caractere e os separadores (. - _ +).
// A semente vem do PRÓPRIO trecho: o mesmo domínio recebe sempre a mesma máscara,
// e comparar linhas não revela as letras escondidas.
fnMascarar = (trecho as text) as text =>
let
vChars = List.Buffer(Text.ToList(trecho)),
vElegiveis = List.Select(
List.Numbers(1, List.Count(vChars) - 1),
each not List.Contains({".", "-", "_", "+"}, vChars{_})
),
vTotal = List.Count(vElegiveis),
vQtd = List.Min({vTotal, List.Max({1, Number.RoundUp(vTotal * vProporcao)})}),
vSemente = Number.Mod(
List.Sum(List.Transform(List.Positions(vChars), each Character.ToNumber(vChars{_}) * (_ + 1))),
2147483647
),
// Embaralha as posições elegíveis e pega as vQtd primeiras
vSorteados = List.Buffer(
List.Transform(
List.FirstN(List.Sort(List.Zip({vElegiveis, List.Random(vTotal, vSemente)}), each _{1}), vQtd),
each _{0}
)
)
in
if vTotal = 0 then trecho // nada a mascarar (ex.: "a")
else Text.Combine(
List.Transform(List.Positions(vChars), each if List.Contains(vSorteados, _) then "*" else vChars{_})
),
vLocal = vPartes{0},
vDominio = vPartes{1},
vPosPonto = Text.PositionOf(vDominio, "."),
vNomeDominio = Text.Start(vDominio, vPosPonto), // "empresa" em "empresa.com.br"
vSufixo = Text.Middle(vDominio, vPosPonto) // ".com.br" fica visível
in
if not vValido then null
else fnMascarar(vLocal) & "@" & fnMascarar(vNomeDominio) & vSufixo,
fnEmailAnonimoTipo = type function (
email as (type nullable text meta [
Documentation.FieldCaption = "E-mail",
Documentation.FieldDescription = "Endereço a mascarar. Espaços nas pontas são removidos; e-mail inválido retorna null."
]),
optional proporcao as (type nullable number meta [
Documentation.FieldCaption = "Proporção",
Documentation.FieldDescription = "Fração dos caracteres elegíveis de cada parte que vira * (0.1 a 1; padrão 0.5)."
])
) as nullable text meta [
Documentation.Name = "fnEmailAnonimo",
Documentation.Description = "Mascara um e-mail substituindo caracteres por * em posições pseudoaleatórias.",
Documentation.LongDescription = "Mascara separadamente a parte local e o nome do domínio, cada um com semente derivada do próprio trecho: o mesmo domínio recebe sempre a mesma máscara, e o resultado é estável entre refreshes. Mantém o 1º caractere de cada parte, o @, os separadores (. - _ +) e o sufixo após o primeiro ponto do domínio. A saída é em minúsculas. E-mail inválido retorna null.",
Documentation.Category = "Máscara de dados",
Documentation.Author = "Paulo André Jesus",
Documentation.Version = "1.2",
Documentation.Examples = {
[Description = "Padrão (50%)", Code = "fnEmailAnonimo(""paulo.jesus@officetuning.com.br"")", Result = "Metade dos caracteres elegíveis de cada parte vira *; o domínio sai igual em todas as linhas"],
[Description = "Máscara mais forte", Code = "fnEmailAnonimo(""paulo.jesus@officetuning.com.br"", 0.8)", Result = "80% dos caracteres elegíveis de cada parte viram *"],
[Description = "Nada elegível", Code = "fnEmailAnonimo(""a@b.com"")", Result = """a@b.com"""],
[Description = "Inválido", Code = "fnEmailAnonimo(""paulo.officetuning.com"")", Result = "null"]
}
]
in
Value.ReplaceType(fnEmailAnonimo, fnEmailAnonimoTipo)Exemplos
fnEmailAnonimo("paulo.jesus@officetuning.com.br")
// metade dos caracteres elegíveis de cada parte vira *; o domínio sai igual em todas as linhas
fnEmailAnonimo("a@b.com")
// "a@b.com" (nada a mascarar)
fnEmailAnonimo("paulo.officetuning.com")
// null (sem @)Dicas e observações
⚠️ Até a versão 1.1, a semente vinha do e-mail inteiro: o mesmo domínio saía com máscaras diferentes em cada linha, e cruzando poucas linhas dava para reconstruí-lo. Num modelo de RH real, um único domínio aparecia com 1.911 máscaras distintas. A 1.2 mascara parte local e domínio com sementes próprias. 💡 Ficam visíveis o 1º caractere de cada parte, o @, os separadores (. - _ +) e tudo após o primeiro ponto do domínio. 💡 A proporção vale para cada parte, com no mínimo um * em cada uma. ⚠️ Só o trecho antes do primeiro ponto do domínio é mascarado: em joao@mail.empresa.com.br o nome da empresa fica à vista. ⚠️ Mascarar não é anonimizar: com a coluna de nome aberta ao lado, a parte local continua identificável. Oculte o nome (OLS) ou suba a proporção. 💡 A saída vem em minúsculas. Trocar de versão muda todas as máscaras já geradas.