paint-brush
Sur le caractère pratique de Regex pour le traitement des adresses e-mailpar@azw
1,815 lectures
1,815 lectures

Sur le caractère pratique de Regex pour le traitement des adresses e-mail

par Adam Zachary Wasserman12m2023/04/02
Read on Terminal Reader

Trop long; Pour lire

Un collègue m'a récemment pointé vers un article de blog : [On the Futility of Email Regex Validation] Cet article développera ces deux affirmations, discutera de quelques cas d'utilisation possibles pour les regex d'email et se terminera par des exemples annotés de "livre de recettes" de regex de messagerie pratique.
featured image - Sur le caractère pratique de Regex pour le traitement des adresses e-mail
Adam Zachary Wasserman HackerNoon profile picture

Un collègue m'a récemment pointé vers un article de blog : On the Futility of Email Regex Validation . Par souci de brièveté, je l'appellerai Futilité dans cet article.


J'admets que si le défi d'écrire une expression régulière qui peut identifier avec succès si une chaîne est conforme à la définition RFC 5322 d'un en-tête de message Internet est un défi divertissant, Futility n'est pas un guide utile pour le programmeur pratique.


En effet, il confond les en-têtes de message RFC 5322 avec les littéraux d'adresse RFC 5321 ; ce qui, en langage simple, signifie que ce qui constitue une adresse e-mail SMTP valide est différent de ce qui constitue un en-tête de message valide en général.


C'est aussi parce qu'il incite le lecteur à se préoccuper de cas limites théoriquement possibles d'un point de vue normatif, mais dont je vais démontrer qu'ils ont une probabilité infinitésimale de se produire « dans la nature ».


Cet article développera ces deux affirmations, discutera de quelques cas d'utilisation possibles pour l'expression régulière d'e-mail et se terminera par des exemples annotés de "livre de recettes" d'expressions régulières d'e-mail pratiques.

RFC 5321 remplace 5322

L'universalité de SMTP pour la transmission des e-mails signifie qu'en pratique, aucun examen du formatage des adresses e-mail n'est complet sans une lecture attentive de la RFC IETF pertinente, qui est 5321.


5322 considère les adresses e-mail comme un simple en-tête de message générique sans règles de cas particulières qui s'y appliquent. Cela signifie que les commentaires entre parenthèses sont valides, même dans un nom de domaine.


La suite de tests référencée dans Futility comprend 10 tests qui contiennent des commentaires, ou des caractères diacritiques ou Unicode, et indique que 8 d'entre eux représentent des adresses e-mail valides.


Ceci est incorrect car la RFC 5321 est explicite en indiquant que les parties de nom de domaine des adresses e-mail « sont limitées à des fins SMTP à une séquence de lettres, de chiffres et de tirets tirés du jeu de caractères ASCII ».


Dans le contexte de la construction d'une expression régulière, il est difficile d'exagérer à quel point cette contrainte simplifie les choses, notamment en ce qui concerne la détermination d'une longueur de chaîne excessive. L'annotation des exemples le soulignera ci-dessous.


Cela implique également d'autres considérations pratiques dans le cadre de la validation que nous explorerons plus loin.

Noms de boîtes aux lettres dans la nature

Selon les deux RFC, le nom technique de la partie de l'adresse e-mail à gauche du symbole "@" est "boîte aux lettres". Les deux RFC permettent une latitude considérable quant aux caractères autorisés dans la partie boîte aux lettres.


La seule contrainte pratique importante est que les guillemets ou les parenthèses doivent être équilibrés, ce qui est un véritable défi à vérifier dans la regex vanille.


Cependant, les implémentations de boîtes aux lettres dans le monde réel sont à nouveau la mesure que le programmeur pratique devrait utiliser.


En règle générale, les personnes qui nous paient désapprouvent que 90 % de nos heures facturables soient dirigées vers la résolution des 10 % de cas limites théoriques qui pourraient même ne pas exister du tout dans la vie réelle.


Examinons les principaux fournisseurs de boîtes aux lettres de messagerie, les consommateurs et les entreprises, et examinons les types d'adresses e-mail qu'ils autorisent.


Pour les e-mails grand public, j'ai effectué des recherches primaires, en utilisant une liste de 5 280 739 adresses e-mail qui ont été divulguées à partir de comptes Twitter.


Sur la base de 115 millions de comptes Twitter, cela nous donne un niveau de confiance de 99% avec une marge d'erreur de 0,055% pour l'ensemble de la population de Twitter, ce qui serait très représentatif de la population générale de toutes les adresses e-mail Internet. Voici ce que j'ai appris :


  • 82% des adresses ne contenaient que des caractères alphanumériques ASCII,


  • 15% ne contenaient que des caractères ASCII alphanumériques et des points (points ASCII), pour 97% de toutes les adresses,


  • 3 % ne contenaient que des caractères alphanumériques ASCII, des points et des tirets, pour une valeur nominale de 100 % des adresses e-mail.


Cependant, il s'agit d'un arrondi à 100 %. Pour les amateurs de quiz, j'ai aussi trouvé :


  • 38 adresses avec des traits de soulignement pour 0,00072 % du total


  • 27 avec des signes plus pour 0,00051 %, et


  • 1 adresse avec des caractères Unicode représentant 0,00002% du total.


L'effet net est qu'en supposant que les boîtes aux lettres d'adresses e-mail ne contiennent que des caractères alphanumériques ASCII, des points et des tirets, vous obtiendrez une précision supérieure à 5 9 pour les e-mails des consommateurs.


Pour les e-mails professionnels, Datanyze rapporte que 6 771 269 entreprises utilisent 91 solutions d'hébergement de messagerie différentes. Cependant, la distribution de Pareto tient, et 95,19% de ces boîtes aux lettres sont hébergées par seulement 10 fournisseurs de services.

Gmail pour les entreprises (34,35 % de part de marché)

Google n'autorise que les lettres, chiffres et points ASCII lors de la création d'une boîte aux lettres. Il acceptera cependant le signe plus lors de la réception d'e-mails .

Microsoft Exchange en ligne (33,60 %)

Autorise uniquement les lettres, chiffres et points ASCII.

Hébergement de messagerie GoDaddy (14,71 %)

Utilise Microsoft 365 et n'autorise que les lettres, chiffres et points ASCII.

7 fournisseurs supplémentaires (12,53 %)

Non documenté.


Malheureusement, nous ne pouvons être certains que de 82 % des entreprises et nous ne savons pas combien de boîtes aux lettres cela représente. Cependant, nous savons que parmi les adresses e-mail Twitter, seuls 400 des 173 467 domaines avaient plus de 100 boîtes aux lettres individuelles représentées.


Je crois que la plupart des 99 % des domaines restants étaient des adresses e-mail professionnelles.


En termes de politiques de dénomination des boîtes aux lettres au niveau du serveur ou du domaine, je propose qu'il soit raisonnable de considérer ces 237 592 adresses e-mail comme représentant une population d'un milliard d'adresses e-mail professionnelles avec un niveau de confiance de 99 % et une marge d'erreur de 0,25 %, ce qui nous donne proche de 3 9 en supposant qu'une boîte aux lettres d'adresse e-mail ne contient que des caractères alphanumériques ASCII, des points et des tirets.

Cas d'utilisation

Encore une fois, avec l'aspect pratique avant tout dans nos esprits, examinons dans quelles circonstances nous pourrions avoir besoin d'identifier par programme une adresse e-mail valide.

Création de nouveau compte/Inscriptions d'utilisateurs

Dans ce cas d'utilisation, un nouveau client potentiel essaie de créer un compte. Il existe deux stratégies de haut niveau que nous pourrions envisager. Dans le premier cas, nous essayons de vérifier que l'adresse e-mail fournie par le nouvel utilisateur est valide et procédons à la création du compte de manière synchrone.


Il y a deux raisons pour lesquelles vous ne voudrez peut-être pas adopter cette approche. La première est que même si vous pouvez valider que l'adresse e-mail a une forme valide, elle peut néanmoins ne pas exister.


L'autre raison est qu'à n'importe quel type d'échelle, synchrone est un mot drapeau rouge, ce qui devrait amener le programmeur pragmatique à envisager à la place un modèle de feu et d'oubli où un frontal Web sans état transmet les informations de formulaire à un microservice ou à une API qui valider de manière asynchrone l'e-mail en envoyant un lien unique qui déclenchera l'achèvement du processus de création de compte.

Formulaires de contact

Dans le cas d'un simple formulaire de contact, du type souvent utilisé pour télécharger des livres blancs, l'inconvénient potentiel d'accepter des chaînes qui ressemblent à un e-mail valide mais qui ne le sont pas est que vous réduisez la qualité de votre base de données marketing en ne validant pas si l'adresse e-mail existe vraiment.


Donc, encore une fois, le modèle fire-and-forget est une meilleure option que la validation par programme de la chaîne saisie dans un formulaire.

Analyse des journaux de référence et d'autres gros volumes de données.

Cela nous amène au cas d'utilisation réel de l'identification programmatique des adresses e-mail en général, et des regex en particulier : anonymiser ou extraire de gros morceaux de texte non structuré.


J'ai découvert ce cas d'utilisation pour la première fois en aidant un chercheur en sécurité qui avait besoin de télécharger des journaux de référence vers une base de données de détection de fraude. Les journaux de référence contenaient des adresses e-mail qui devaient être anonymisées avant de quitter le jardin clos de l'entreprise.


C'étaient des fichiers avec des centaines de millions de lignes, et il y avait des centaines de fichiers par jour. Les « lignes » peuvent compter près d'un millier de caractères.


Itérer à travers les caractères d'une ligne, appliquer des tests complexes (par exemple, est-ce la première occurrence de @ dans la ligne et fait-il partie d'un nom de fichier tel que [email protected] ?) en utilisant des boucles et des fonctions de chaîne standard aurait créé une complexité temporelle incroyablement grande.


En fait, l'équipe de développement interne de cette (très grande) entreprise avait déclaré que c'était une tâche impossible.


J'ai écrit la regex compilée suivante:

search_pattern = re.compile("[a-zA-Z0-9\!\#\$\%\'\*\+\-\^\_\`\{\|\}\~\.]+@|\%40(?!(\w+\.)**(jpg|png))(([\w\-]+\.)+([\w\-]+)))")


Et l'a déposé dans la compréhension de liste Python suivante :

results = [(re.sub(search_pattern, "[email protected]", line)) for line in file]


Je ne me souviens pas à quelle vitesse c'était, mais c'était rapide. Mon ami pourrait l'exécuter sur un ordinateur portable et le faire en quelques minutes. C'était exact. Nous l'avons chronométré à 5 9 en regardant à la fois les faux négatifs et les faux positifs.


Mon travail a été quelque peu facilité par le fait que les journaux de référence; ils ne pouvaient contenir que des caractères "légaux" d'URL, j'ai donc pu cartographier toutes les collisions que j'ai documentées dans le repo readme .


De plus, j'aurais pu le rendre encore plus simple (et plus rapide) si j'avais effectué l'analyse de l'adresse e-mail et appris avec l'assurance que tout ce qui était nécessaire pour atteindre la cible du 5 9 était l'ASCII alphanumérique, les points et les tirets.


Néanmoins, c'est un bon exemple d'aspect pratique et de portée de la solution pour s'adapter au problème réel à résoudre.


L'une des plus grandes citations de toute la tradition et de l'histoire de la programmation est l'avertissement du grand Ward Cunningham de prendre une seconde pour se souvenir exactement de ce que vous essayez d'accomplir, puis de vous demander « Quelle est la chose la plus simple qui pourrait éventuellement fonctionner ? »


Dans le cas d'utilisation de l'analyse (et éventuellement de la transformation) d'une adresse e-mail à partir d'une grande quantité de texte non structuré, cette solution était certainement la chose la plus simple à laquelle je pouvais penser.

Livre de cuisine annoté

Comme je l'ai dit au début, j'ai trouvé l'idée de construire une regex conforme à la RFC 5322 amusante, donc je vais vous montrer des morceaux composables de regex pour traiter divers aspects de la norme et expliquer comment les politiques de regex cela. Au final, je vais vous montrer à quoi ça ressemble tout assemblé.


La structure d'une adresse e-mail est la suivante :

  1. La boîte aux lettres
  2. Caractères juridiques
  3. Points simples (les points doubles ne sont pas légaux)
  4. Espace blanc plié (folie RFC 5322)
  5. (Une solution regex complète inclurait également des parenthèses équilibrées et/ou des guillemets, mais je ne l'ai pas encore. Et très probablement jamais.)
  6. Le délimiteur (@)
  7. Le nom de domaine
  8. Domaines analysables DNS standard
  9. Littéraux d'adresse IPv4
  10. Littéraux d'adresse IPv6
  11. IPv6 complet
  12. IPv6-comp (pour compressé)
  13. 1ère forme (2+ groupes de 16 bits de zéro au milieu)
  14. 2ème forme (2+ groupes de 16 bits de zéro au début)
  15. 3ème forme (2 groupes de 16 bits de zéro à la fin)
  16. 4ème forme (8 groupes de 16 bits de zéro)
  17. IPv6v4 complet
  18. IPv6v4-comp (compressé)
  19. 1ère forme
  20. 2ème forme
  21. 3ème forme
  22. 4ème forme

Maintenant pour la regex.

Boites aux lettres

^(?<mailbox>(\[a-zA-Z0-9\\+\\!\\#\\$\\%\\&\\'\\\*\\-\\/\\=\\?\\+\\\_\\\{\\}\\|\\\~]|(?<singleDot>(?<!\\.)(?<!^)\\.(?!\\.))|(?<foldedWhiteSpace>\\s?\\&\\#13\\;\\&\\#10\\;.))\{1,64})


Tout d'abord, nous avons ^ qui « ancre » le premier caractère au début de la chaîne. Ceci doit être utilisé lors de la validation d'une chaîne censée ne contenir qu'un e-mail valide. Il s'assure que le premier caractère est légal.


Si le cas d'utilisation consiste plutôt à rechercher un e-mail dans une chaîne plus longue, omettez l'ancre.


Ensuite, nous avons (?<mailbox> . Cela nomme le groupe de capture pour plus de commodité. À l'intérieur du groupe capturé se trouvent les trois morceaux de regex séparés par le symbole de correspondance alternatif | ce qui signifie qu'un caractère peut correspondre à l'une des trois expressions.


Une partie de l'écriture d'une bonne regex (performante et prévisible) consiste à s'assurer que les trois expressions s'excluent mutuellement. C'est-à-dire qu'une sous-chaîne qui correspond à l'une ne correspondra certainement à aucune des deux autres. Pour ce faire, nous utilisons des classes de caractères spécifiques au lieu du redoutable .* .

Caractères inconditionnellement légaux

[a-zA-Z0-9\+\!\#\$\%\&\'\*\-\/\=\?\+\_\{\}\|\~]

La première correspondance alternative est une classe de caractères entre crochets, qui capture tous les caractères ASCII autorisés dans une boîte aux lettres électronique, à l'exception du point, de l'"espace blanc plié", du guillemet double et de la parenthèse.


La raison pour laquelle nous les avons exclus est qu'ils ne sont que conditionnellement légaux, c'est-à-dire qu'il existe des règles sur la façon dont vous pouvez les utiliser qui doivent être validées. Nous les traitons dans les 2 prochains matchs alternatifs.

point unique

(?<singleDot>(?<!\.)(?<!^)\.(?!\.))

La première de ces règles concerne le point (point). Dans une boîte aux lettres, le point n'est autorisé que comme séparateur entre deux chaînes de caractères légaux, donc deux points consécutifs ne sont pas légaux.


Pour empêcher une correspondance s'il y a deux points consécutifs, nous utilisons le regex négatif lookbehind (?<!\.) qui spécifie que le caractère suivant (un point) ne correspondra pas s'il y a un point qui le précède.


Les contours de Regex peuvent être enchaînés. Il y a un autre lookbehind négatif avant d'arriver au point (?!^) qui applique la règle selon laquelle le point ne peut pas être le premier caractère de la boîte aux lettres.


Après le point, il y a un look_ahead_ _(?!\.)_ négatif , cela empêche un point d'être mis en correspondance s'il est immédiatement suivi d'un point.

pliéBlancEspace

(?<foldedWhiteSpace>\s?\&\#13\;\&\#10\;.)

C'est un non-sens RFC 5322 sur l'autorisation des en-têtes multilignes dans les messages. Je suis prêt à parier que dans l'histoire des adresses e-mail, il n'y a jamais eu quelqu'un qui ait sérieusement créé une adresse avec une boîte aux lettres multiligne (ils l'ont peut-être fait pour plaisanter).


Mais je joue au jeu 5322, alors la voici, la chaîne de caractères Unicode qui crée l' espace blanc plié comme correspondance alternative.

Guillemets doubles équilibrés et parenthèses

Les deux RFC autorisent l'utilisation de guillemets doubles comme moyen d'enfermer (ou d'échapper ) des caractères qui seraient normalement illégaux.


Ils permettent également de mettre des commentaires entre parenthèses afin qu'ils soient lisibles par l'homme, mais qu'ils ne soient pas pris en compte par l'agent de transfert de courrier (MTA) lors de l'interprétation de l'adresse.


Dans les deux cas, les caractères ne sont légaux que s'ils sont équilibrés . Cela signifie qu'il doit y avoir une paire de caractères, un qui ouvre et un qui ferme .


Je suis tenté d'écrire que j'ai découvert un mirabilem de démonstration , cependant, cela ne fonctionne probablement qu'à titre posthume. La vérité est que ce n'est pas trivial dans la regex vanille.


J'ai l'intuition que la nature récursive des regex "gourmandes" pourrait être exploitée à son avantage, cependant, il est peu probable que je consacre le temps nécessaire pour attaquer ce problème au cours des prochaines années, et donc dans la meilleure tradition, je le laisse comme exercice pour le lecteur.

Longueur de la boîte aux lettres

{1,64}

Ce qui compte vraiment, c'est la longueur maximale d'une boîte aux lettres : 64 caractères.


Ainsi, après avoir fermé le groupe de capture de boîte aux lettres avec une parenthèse fermante finale, nous utilisons un quantificateur entre accolades pour spécifier que nous devons faire correspondre l'un de nos suppléants au moins une fois et pas plus de 64 fois.

arobase

\s?(?<atSign>(?<!\-)(?<!\.)\@(?!\@))

Le bloc délimiteur commence par le cas spécial \s? car selon Futility, un espace est légal juste avant le délimiteur, et je ne fais que les croire sur parole.


Le reste du groupe de capture suit un modèle similaire à singleDot ; il ne correspondra pas s'il est précédé d'un point ou d'un tiret ou s'il est suivi immédiatement d'un autre @ .

Nom de domaine

Ici, comme dans la boîte aux lettres, nous avons 3 correspondances alternatives. Et le dernier d'entre eux a niché en lui 4 autres matchs alternatifs.

Analysable DNS standard

(?<dns>[[:alnum:]]([[:alnum:]\-]{0,63}\.){1,24}[[:alnum:]\-]{1,63}[[:alnum:]])

Cela ne passera pas plusieurs des tests dans Futility, mais comme mentionné précédemment, il est strictement conforme à la RFC 5321 qui a le dernier mot.

IPv4

(?<IPv4>\[((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])

Il n'y a pas grand chose à dire à ce sujet. Il s'agit d'une expression régulière bien connue et facilement disponible pour les adresses IPv4.

IPv6

(?<IPv6>(?<IPv6Full>(\[IPv6(\:[0-9a-fA-F]{1,4}){8}\]))|(?<IPv6Comp1>\[IPv6\:((([0-9a-fA-F]{1,4})\:){1,3}(\:([0-9a-fA-F]{1,4})){1,5}?\])|\[IPv6\:((([0-9a-fA-F]{1,4})\:){1,5}(\:([0-9a-fA-F]{1,4})){1,3}?\]))|(?<IPv6Comp2>(\[IPv6\:\:(\:[0-9a-fA-F]{1,4}){1,6}\]))|(?<IPv6Comp3>(\[IPv6\:([0-9a-fA-F]{1,4}\:){1,6}\:\]))|(?<IPv6Comp4>(\[IPv6\:\:\:)\])|(?<IPv6v4Full>(\[IPv6(\:[0-9a-fA-F]{1,4}){6}\:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3})(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])|(?<IPv6v4Comp1>\[IPv6\:((([0-9a-fA-F]{1,4})\:){1,3}(\:([0-9a-fA-F]{1,4})){1,5}?(\:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\])|\[IPv6\:((([0-9a-fA-F]{1,4})\:){1,5}(\:([0-9a-fA-F]{1,4})){1,3}?(\:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\]))|(?<IPv6v4Comp2>(\[IPv6\:\:(\:[0-9a-fA-F]{1,4}){1,5}(\:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\]))|(?<IPv6v4Comp3>(\[IPv6\:([0-9a-fA-F]{1,4}\:){1,5}\:(((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\]))|(?<IPv6v4Comp4>(\[IPv6\:\:\:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3})(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\]))


Je n'ai pas pu trouver une bonne expression régulière pour les adresses IPv6 (et IPv6v4), j'ai donc écrit la mienne, en suivant attentivement les règles notées Backus/Naur de la RFC 5321.


Je n'annoterai pas chaque sous-groupe de la regex IPv6, mais j'ai nommé chaque sous-groupe pour faciliter la séparation et voir ce qui se passe.


Rien de vraiment intéressant, sauf peut-être la façon dont j'ai combiné la correspondance gourmande du côté "gauche" et non gourmande du côté "droit" dans le groupe de capture IUPv6Comp1.

Le plein Monty

J'ai enregistré la regex finale, ainsi que les données de test de Futility, et améliorée par mes propres cas de test IPv6, dans Regex101 . J'espère que vous avez apprécié cet article, et qu'il s'avère utile et un gain de temps pour beaucoup d'entre vous.


AZW