最近、同僚が次のブログ投稿を指摘してくれました: On the Futility of Email Regex Validation .簡潔にするために、この記事ではこれを無益と呼びます。
文字列がインターネット メッセージ ヘッダーの RFC 5322 定義に準拠しているかどうかを正常に識別できる正規表現を作成するという課題は楽しい課題ですが、実用的なプログラマーにとって無駄は有益なガイドではありません。
これは、RFC 5322 メッセージ ヘッダーを RFC 5321 アドレス リテラルと混同するためです。簡単に言えば、有効な SMTP 電子メール アドレスを構成するものは、一般的に有効なメッセージ ヘッダーを構成するものとは異なることを意味します。
また、標準の観点からは理論的には可能であるが、「実際に」発生する可能性が非常に小さいエッジ ケースに読者が夢中になるように仕向けているためでもあります。
この記事では、これらの主張の両方を詳しく説明し、メール正規表現のいくつかの可能な使用例について説明し、実際のメール正規表現の注釈付きの「クックブック」の例で締めくくります。
電子メールの送信における SMTP の普遍性は、実際問題として、関連する IETF RFC (5321) をよく読まない限り、電子メール アドレスのフォーマットの検査を完了できないことを意味します。
5322 は、電子メール アドレスを、特別なケース ルールが適用されない単なる一般的なメッセージ ヘッダーと見なします。これは、ドメイン名であっても、括弧で囲まれたコメントが有効であることを意味します。
Futilityで参照されているテスト スイートには、コメント、分音記号または Unicode 文字を含む 10 個のテストが含まれており、そのうちの 8 個が有効な電子メール アドレスを表していることが示されています。
RFC 5321 では、電子メール アドレスのドメイン名部分は「 SMTP の目的で、ASCII 文字セットから引き出された一連の文字、数字、およびハイフンで構成されるように制限されている」と明言されているため、これは正しくありません。
正規表現を構築するという文脈では、特に過剰な文字列の長さを決定することに関して、この制約が問題を単純化する度合いを誇張することは困難です。例の注釈は、以下でこれを強調します。
また、検証のコンテキストでさらに検討するいくつかの他の実際的な考慮事項も意味します。
両方の RFC によると、「@」記号の左側にある電子メール アドレスの部分の技術名は「mailbox」です。どちらの RFC でも、メールボックス部分で使用できる文字についてかなりの自由度が認められています。
唯一の重要な実用上の制約は、引用符または括弧のバランスをとらなければならないことです。これは、バニラの正規表現で検証するのが本当に難しいことです。
ただし、実際のメールボックスの実装は、実際のプログラマーが採用する必要がある手段です。
原則として、私たちにお金を払ってくれる人々は、私たちの請求可能な時間の 90% が、現実にはまったく存在しない可能性がある理論的なエッジ ケースの 10% の解決に向けられていることに眉をひそめています。
主要な電子メール メールボックス プロバイダー、消費者、および企業を見て、それらが許可する電子メール アドレスの種類を考えてみましょう。
消費者の電子メールについては、Twitter アカウントから漏洩した 5,280,739 個の電子メール アドレスのリストを使用して、いくつかの主要な調査を行いました。
1 億 1,500 万の Twitter アカウントに基づくと、これにより、Twitter の全人口に対して 99% の信頼水準と 0.055% の誤差範囲が得られます。これは、すべてのインターネット電子メール アドレスの一般的な母集団を非常に代表するものです。これが私が学んだことです:
ただし、これは四捨五入された 100% です。そこにいるトリビア愛好家のために、私はまた見つけました:
最終的な効果として、電子メール アドレスのメールボックスに ASCII 英数字、ドット、およびダッシュのみが含まれていると仮定すると、消費者向け電子メールの正確性はファイブ ナインよりも高くなります。
ビジネス メールの場合、Datanyze は、6,771,269 社が 91 の異なるメール ホスティング ソリューションを使用していると報告しています。ただし、パレート分布は成り立ち、これらのメールボックスの 95.19% は、わずか 10 のサービス プロバイダーによってホストされています。
Google では、メールボックスの作成時に ASCII 文字、数字、およびドットのみを使用できます。ただし、電子メールを受信するときはプラス記号を受け入れます。
ASCII 文字、数字、およびドットのみを許可します。
Microsoft 365 を使用し、ASCII 文字、数字、およびドットのみを許可します。
文書化されていません。
残念ながら、82% の企業しか確認できず、それが何個のメールボックスを表しているかはわかりません。ただし、Twitter の電子メール アドレスのうち、173,467 のドメインのうち 400 のみが 100 を超える個々の電子メール メールボックスを表していたことがわかっています。
残りのドメインの 99% のほとんどは、ビジネス用のメール アドレスだったと思います。
サーバーまたはドメイン レベルでのメールボックスの命名ポリシーに関して、私は、これらの 237,592 の電子メール アドレスを、99% の信頼水準と 0.25% の誤差範囲で 10 億のビジネス電子メール アドレスの母集団を表すと考えるのが妥当であると提案します。電子メール アドレスのメールボックスに ASCII 英数字、ドット、およびダッシュのみが含まれていると仮定すると、スリー ナインに近くなります。
繰り返しになりますが、実用性を最優先に考えて、どのような状況で有効な電子メール アドレスをプログラムで識別する必要があるかを考えてみましょう。
この使用例では、新規見込み顧客がアカウントを作成しようとしています。検討できる 2 つの大まかな戦略があります。最初のケースでは、新しいユーザーが提供した電子メール アドレスが有効であることを確認し、アカウントの作成を同期的に進めます。
このアプローチを採用したくない理由は 2 つあります。 1 つ目は、電子メール アドレスの形式が有効であることは確認できても、存在しない可能性があることです。
もう 1 つの理由は、どのような規模においても、同期は危険信号であり、実用的なプログラマーは代わりに、ステートレス Web フロント エンドがフォーム情報をマイクロサービスまたは API に渡すファイア アンド フォーゲット モデルを考慮する必要があるためです。アカウント作成プロセスの完了をトリガーする一意のリンクを送信して、電子メールを非同期的に検証します。
ホワイト ペーパーをダウンロードするためによく使用される単純な連絡先フォームの場合、有効な電子メールのように見えてもそうではない文字列を受け入れることの潜在的な欠点は、有効かどうかの検証に失敗することによって、マーケティング データベースの品質を低下させることです。メールアドレスは実在します。
繰り返しになりますが、フォームに入力された文字列をプログラムで検証するよりも、ファイア アンド フォーゲット モデルの方が適しています。
これにより、プログラムによる電子メール アドレスの識別全般、特に正規表現の実際のユース ケース、つまり構造化されていないテキストの大きなチャンクの匿名化またはマイニングにつながります。
私が最初にこのユース ケースに出会ったのは、リファラー ログを不正検出データベースにアップロードする必要があるセキュリティ研究者を支援するときでした。リファラー ログには、会社の壁に囲まれた庭を出る前に匿名化する必要のある電子メール アドレスが含まれていました。
これらは数億行のファイルで、1 日に数百のファイルがありました。 「行」の長さは 1,000 文字近くになることもあります。
ループと標準の文字列関数を使用して、行内の文字を反復し、複雑なテスト (たとえば、これは行内で最初に出現する@
であり、 [email protected]
などのファイル名の一部であるか?) を適用すると、信じられないほど大きな時間の複雑さ。
実際、この (非常に大きな) 会社の社内開発チームは、それは不可能な作業であると宣言していました。
次のコンパイル済み正規表現を作成しました。
search_pattern = re.compile("[a-zA-Z0-9\!\#\$\%\'\*\+\-\^\_\`\{\|\}\~\.]+@|\%40(?!(\w+\.)**(jpg|png))(([\w\-]+\.)+([\w\-]+)))")
そして、それを次の Python リスト内包表記に落とし込みました。
results = [(re.sub(search_pattern, "[email protected]", line)) for line in file]
どれだけ速かったか覚えていませんが、速かったです。私の友人はそれをラップトップで実行でき、数分で完了しました。正確でした。偽陰性と偽陽性の両方を調べて、ファイブ ナインで計測しました。
リファラー ログのおかげで、私の仕事はいくらか楽になりました。それらには URL の「正当な」文字のみを含めることができたので、レポのreadmeに文書化した衝突を特定することができました。
また、電子メール アドレスの分析を実行し、ファイブ ナインのターゲットに到達するために必要なのは ASCII 英数字、ドット、およびダッシュだけであるという確信を持って学習していれば、さらに簡単 (かつ高速) にできたはずです。
とはいえ、これは実用性と、解決すべき実際の問題に合わせたソリューションの範囲設定の良い例です。
プログラミングの伝承と歴史の中で最も偉大な引用の 1 つは、達成しようとしていることを正確に思い出すために少し時間を取ってから、「機能する可能性のある最も簡単なことは何ですか?」
大量の非構造化テキストから電子メール アドレスを解析 (およびオプションで変換) するユース ケースでは、このソリューションは間違いなく私が考えることができる最も単純なものでした。
冒頭で述べたように、RFC 5322 準拠の正規表現を作成するというアイデアは面白いと思いました。そのため、標準のさまざまな側面に対処するための構成可能な正規表現のチャンクを示し、正規表現がそれをどのようにポリシー化するかを説明します。最後に、すべてが組み立てられた様子をお見せします。
メールアドレスの構造は次のとおりです。
次に正規表現です。
^(?<mailbox>(\[a-zA-Z0-9\\+\\!\\#\\$\\%\\&\\'\\\*\\-\\/\\=\\?\\+\\\_\\\{\\}\\|\\\~]|(?<singleDot>(?<!\\.)(?<!^)\\.(?!\\.))|(?<foldedWhiteSpace>\\s?\\&\\#13\\;\\&\\#10\\;.))\{1,64})
まず、文字列の最初の文字を「アンカー」する^
があります。これは、有効な電子メールのみを含むはずの文字列を検証する場合に使用されます。最初の文字が有効であることを確認します。
代わりに、より長い文字列で電子メールを検索する場合は、アンカーを省略します。
次に、 (?<mailbox>
があります。これは、便宜上キャプチャ グループに名前を付けます。キャプチャ グループ内には、代替一致記号|
で区切られた 3 つの正規表現チャンクがあります。これは、文字が 3 つの式のいずれかに一致できることを意味します。
適切な (パフォーマンスが高く予測可能な) 正規表現を作成するには、3 つの式が相互に排他的であることを確認する必要があります。つまり、1 つに一致する部分文字列は、他の 2 つのいずれとも一致しません。これを行うには、恐ろしい . .*
の代わりに特定の文字クラスを使用します。
[a-zA-Z0-9\+\!\#\$\%\&\'\*\-\/\=\?\+\_\{\}\|\~]
最初の代替一致は、角括弧で囲まれた文字クラスであり、ドット、「折り畳まれた空白」、二重引用符、および括弧を除く、電子メール メールボックスで有効なすべての ASCII 文字をキャプチャします。
それらを除外した理由は、それらが条件付きでのみ合法であるということです。つまり、それらの使用方法には検証が必要なルールがあるということです。次の2回の代替試合でそれらを処理します。
(?<singleDot>(?<!\.)(?<!^)\.(?!\.))
そのような最初の規則は、ドット (ピリオド) に関するものです。メールボックスでは、ドットは有効な文字の 2 つの文字列間の区切りとしてのみ許可されているため、2 つの連続するドットは無効です。
2 つの連続したドットがある場合に一致を防ぐために、正規表現の否定後読み(?<!\.)
を使用します。これは、ドットが前にある場合に次の文字 (ドット) が一致しないことを指定します。
正規表現ルックアラウンドは連鎖できます。ドット(?!^)
に到達する前に、別の否定的な後読みがあります。これは、ドットがメールボックスの最初の文字であってはならないという規則を強制します。
ドットの後には、負の look_ahead_ _(?!\.)_
があります。これにより、ドットの直後にドットが続くと、ドットが一致しなくなります。
(?<foldedWhiteSpace>\s?\&\#13\;\&\#10\;.)
これは、メッセージで複数行のヘッダーを許可することに関する RFC 5322 のナンセンスです。電子メール アドレスの歴史の中で、真剣に複数行のメールボックスを使ってアドレスを作成した人はいないと断言できます (彼らは冗談で作ったのかもしれません)。
しかし、私は 5322 ゲームをプレイしているので、代替一致としてFolded White Spaceを作成する Unicode 文字の文字列がここにあります。
どちらの RFC も、通常は不正な文字を囲む (またはエスケープする) 方法として、二重引用符の使用を許可しています。
また、コメントを括弧で囲んで人間が読めるようにすることもできますが、アドレスを解釈するときにメール転送エージェント (MTA) によって考慮されません。
どちらの場合も、文字はバランスがとれている場合にのみ有効です。これは、開く文字と閉じる文字のペアが必要であることを意味します。
私はデモンストレーション奇跡を発見したと書きたくなりますが、これはおそらく死後にしか機能しません.真実は、これはバニラの正規表現では自明ではありません。
「貪欲な」正規表現の再帰的な性質が有利に利用される可能性があるという直感を持っていますが、今後数年間、この問題に取り組むために必要な時間を費やす可能性は低いため、最善の伝統として、私はそれを残します読者の練習として。
{1,64}
実際に重要なのは、メールボックスの最大長である 64 文字です。
そのため、最後の閉じ括弧でメールボックス キャプチャ グループを閉じた後、中かっこの間に量指定子を使用して、少なくとも 1 回、64 回を超えて代替のいずれかと一致する必要があることを指定します。
\s?(?<atSign>(?<!\-)(?<!\.)\@(?!\@))
デリミタ チャンクは特殊なケース\s?
で始まります。 Futility によれば、区切り文字の直前にスペースを入れることは合法であり、私は彼らの言葉を信じているだけです。
キャプチャ グループの残りの部分は、 singleDotと同様のパターンに従います。ドットまたはダッシュが前にある場合、または直後に別の@
が続く場合は一致しません。
ここでは、メールボックスと同様に、3 つの代替一致があります。そして、これらの最後のものには、別の 4 つの代替一致がネストされています。
(?<dns>[[:alnum:]]([[:alnum:]\-]{0,63}\.){1,24}[[:alnum:]\-]{1,63}[[:alnum:]])
これはFutility のいくつかのテストに合格しませんが、前述のように、最終的な言葉がある RFC 5321 に厳密に準拠しています。
(?<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]?)\])
これについては言うまでもありません。これはよく知られており、簡単に利用できる IPv4 アドレスの正規表現です。
(?<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]?)\]))
IPv6 (および IPv6v4) アドレスの適切な正規表現を見つけることができなかったので、RFC 5321 の Backus/Naur 表記規則に注意深く従って、独自の正規表現を作成しました。
IPv6 正規表現のすべてのサブグループに注釈を付けるわけではありませんが、簡単に区別して何が起こっているかを確認できるように、すべてのサブグループに名前を付けました。
IUPv6Comp1 キャプチャ グループの "左側" の貪欲なマッチングと "右側" の非貪欲なマッチングを組み合わせた方法を除いて、あまり興味深いものはありません。
最終的な正規表現を Futility からのテスト データと共に保存し、独自の IPv6 テスト ケースによってRegex101に拡張しました。この記事を楽しんでいただけたでしょうか。多くの皆様にとって、この記事が役に立ち、時間の節約になることを願っています。
AZW