この記事を書いたきっかけ
AIにテストケースを作ってもらって、そのままテストを実行してもらいました。
あとから結果を眺めていたら、自分の知らないメールアドレスで会員登録されていました。
user+testcase1-1@gmail.com
あれ?
自分はテストには
user@gmail.com
でお願いします、と言ったはずなのに、なんで??
調べてみたら、これは Gmailの「プラスアドレス」 という仕組みでした。AIが勝手に発明したわけでも、誤植でもありません。
ただ、調べていくうちに「思っていたより深い話だった」ので、そこも含めてまとめます。
表記について: 実際に使ったのは自分のGmailアドレスですが、この記事では user@gmail.com に置き換えて書きます。メールアドレスを公開の記事に生で書くと収集の対象になるためで、これ自体が後半(6-6)で触れる話でもあります。
先に結論を書きます。
+ 以降は無視されて、本体の user@gmail.com に届きます。設定は不要です。
- これはGmailの独自機能ではありません。「サブアドレス(subaddressing)」という名前でRFCにも登場する、広く使われている慣行です。ただし「全メールサービスがこう動く」と決めた標準規格があるわけではなく、実際の挙動は各サービスが決めています。
- 万能ではありません。
+ を弾くサービスがあり、フォーム形式のURLでは + が半角スペースに化けるという地雷もあります。
- テスト用途としては便利ですが、本番のメールが自分に実際に届くので、そこは意識して使う必要があります。
1. プラスアドレスとは何か
元のアドレスが user@gmail.com の場合、たとえば次のアドレスをそのまま利用できます。
user+1@gmail.com
user+test@gmail.com
user+shop@gmail.com
user+20260812@gmail.com
これらに送信されたメールは、すべて通常の user@gmail.com の受信トレイに届きます。事前登録や設定は必要ありません。
user+testcase1-1@gmail.com
↓ 「+」以降を切り落として配送先を解決
user@gmail.com の受信トレイへ
主な用途は次のとおりです。
- システムの会員登録テスト
- サービスごとのメール振り分け
- どこに登録したアドレスからメールが来たかの判別
- Gmailフィルタによる自動ラベル付け
2. これはGmailの独自機能ではない
ここが調べていて一番「へえ」と思ったところです。
この仕組みには正式名称があります。サブアドレス(subaddressing)、または詳細アドレス(detailed addressing)、タグ付きアドレス(tagged addressing)と呼ばれます。
メールフィルタ言語Sieveの拡張仕様である RFC 5233 に、その定義が書かれています。1
Subaddressing is the practice of augmenting the local-part of an address with some "detail" information in order to give some extra meaning to that address.
(サブアドレスとは、アドレスに追加の意味を持たせるために、ローカル部に「詳細」情報を付け足す慣行のことである)
そしてアドレスをこう分解して扱います。
:user "+" :detail "@" :domain
user + testcase1-1 @ gmail.com
つまり「Gmailの裏技」ではなく、仕様書に名前が載っている概念です。
ただし「標準規格」と言い切ると言い過ぎ
ここは正確に書いておきます。
RFC 5233 は 「サブアドレスという慣行が既にある」ことを前提に、Sieveというメールフィルタ言語からそれを扱うための拡張です。「全メールサーバーは + 以降を切り落として配送せよ」と決めた規格ではありません。
RFC 5233 自身にもこう書かれています。[^1]
the logic used to split the address is implementation-defined and is usually dependent on the format used by the encompassing mail system.
(アドレスを分割するロジックは実装依存であり、通常はそれを含むメールシステムが使う形式に依存する)
つまり、
- 区切り文字が
+ とは限らない
- 切り落として配送するかどうかも各サービスの判断
「業界の共通慣行としてよく使われていて、RFCにも名前が載っている」くらいの理解が正確です。だからこそ、後述するようにサービスごとに挙動がバラつきます。
実際、+ 以外の実装もあるし、既定オフの実装もある
自前のメールサーバーを触ったことがある人には、ここが一番実感しやすいと思います。
代表的なMTAである Postfix には recipient_delimiter という設定項目があり、これが区切り文字を決めています。そして デフォルト値は空です。2
recipient_delimiter = ← 既定は空。つまりサブアドレス機能はオフ
recipient_delimiter = + ← これでようやく + が区切り文字になる
recipient_delimiter = +- ← 複数指定もできる
qmail系では歴史的に - が使われてきました。
ここから分かることは、
自社ドメインで + が効かないとき、それは「壊れている」のではなく「有効にしていないだけ」
ということです。Gmailで動いたからといって、自分の会社のメールサーバーで動くとは限りません。テスト計画を立てるときは、まずそこを確認してください。
+ はそもそもアドレスに使ってよい文字
メールアドレスの書式を定めた RFC 5322 では、@ の左側(ローカル部)に使える文字として + が明示的に許可されています。3
ALPHA / DIGIT / ! # $ % & ' * + - / = ? ^ _ ` { | } ~
+ は「特殊な抜け道の文字」ではなく、最初から普通に使える文字です。
ローカル部の意味を決めてよいのは受信側だけ
さらに、SMTPの仕様である RFC 5321 にはこう書かれています。4
the local-part MUST be interpreted and assigned semantics only by the host specified in the domain part of the address.
(ローカル部の解釈と意味づけは、アドレスのドメイン部で指定されたホストだけが行わなければならない)
これは地味に重要です。
user+test@gmail.com の +test に意味があるかどうかを決めてよいのは gmail.com のサーバーだけです。会員登録フォームを作っている側が「+test は余計なものだから削っていい」と勝手に判断するのは、この立て付けからすると危うい、ということになります。
ただし、ここから 「だから + を弾くサービスは仕様違反だ」とまでは言えません。 RFCが決めているのは「アドレスの意味を解釈してよいのは誰か」であって、「サービスがどんなアドレスを会員登録に受け付けるべきか」は各サービスのポリシーの話です。
整理すると、
+ はアドレスに使える正規の文字である(RFC 5322)
+ 以降の意味を決めるのは受信側のメールサーバーである(RFC 5321)
- しかし、登録フォームがそれを受け付けるかどうかは、規格ではなくサービスの判断
現実には弾くサービスがそこそこあります。理由は後述します。
3. 対応しているメールサービス
Gmail以外でも使えます。ただし対応状況はサービスによって違います。
受信できるかとそのアドレスから送信できるかは別の話なので、分けて書きます。
| サービス |
受信(プラスアドレス) |
送信元にできるか |
備考 |
| Gmail(@gmail.com) |
対応・設定不要5 |
既定では不可。「他のメールアドレスを追加」で確認手続きを踏めば可6 |
|
| Google Workspace(独自ドメイン) |
対応[^14] |
同上 |
ドットの扱いは後述の通り異なる |
| Exchange Online(Microsoft 365) |
対応(既定で有効) |
不可(受信専用と明記)7 |
管理者が組織単位で無効化可能[^4] |
| Outlook.com(@outlook.com 等) |
対応(公式表記は "+sitename") |
不可 |
サインインにも使えないと明記8 |
| Proton Mail |
対応・無制限9 |
要確認 |
プラン制限があるのは「追加アドレス」「hide-my-email」で、プラスアドレスではない[^9] |
| Fastmail |
対応 |
送信元として設定すれば可 |
サブドメインアドレッシングも可(後述) |
| iCloud Mail |
要確認(Apple公式に記載なし) |
要確認 |
公式に確認できるのは手動エイリアス最大3つ10。別途 Hide My Email あり |
| 日本のキャリアメール(docomo / au / SoftBank) |
対応表明なし=使えない前提で |
不可 |
そもそもアドレスに + を使えない。後述 |
iCloudについて: ネット上には「iCloudはプラスアドレス非対応」という記述が多いのですが、Appleの公式ドキュメントにはプラスアドレスについての記載自体がありません。公式に確認できるのは「手動で作るエイリアスは最大3つ」という点だけです。使う前に実際に送って確かめてください。
Fastmailの「サブドメインアドレッシング」が地味に賢い
Fastmailには + とは別に、サブドメインアドレッシングという方式があります。
プラスアドレス : username+shop@fastmail.com
サブドメインアドレッシング: shop@username.fastmail.com
+ が消えて、普通のメールアドレスの見た目になります。独自ドメインを持っていなくても、@fastmail.com のユーザーがそのまま使えます。
Fastmailは公式ドキュメントで、この方式を勧める理由をはっきり書いています。
The main downside to plus addressing is that addresses with a + are incorrectly considered invalid by some websites, and may not allowed on some registration forms. Subdomain addressing, described below, should overcome this problem.
(プラスアドレスの主な欠点は、+ を含むアドレスを一部のWebサイトが誤って無効と判断し、登録フォームで受け付けないことがある点です。サブドメインアドレッシングはこの問題を回避できます)
メールサービス側が「+ は弾かれることがある」と認めて回避策を用意しているわけです。「+ が通らないのは自分の使い方が悪いのか?」と悩む必要はない、という証拠でもあります。
Microsoftの公式ドキュメントには、Exchange Onlineの挙動がはっきり書かれていて分かりやすいです。[^4]
Exchangeは + を含むアドレス宛のメールを受け取ると、まず完全なアドレス(例: sean+newsletter@contoso.com)で既知のメールボックスへの解決を試みます。それが失敗した場合、+ とタグを除いたアドレス(例: sean@contoso.com)で2回目の解決を試みます。
つまり「まずそのまま探して、なければ + を削って探す」という二段構えです。
なぜExchangeは二度手間なのか
ここが分かると、サービスごとの違いが腑に落ちます。
Gmailのユーザー名には、そもそも + を使えません。 使えるのは英数字とピリオドだけです。だから user+test@gmail.com の +test が「本物のアドレスの一部」である可能性は 原理的にゼロで、無条件に区切り文字として切り落とせます。
一方、Exchangeは歴史的に + を普通の文字として許してきました。 つまり sean+newsletter@contoso.com が実在するメールボックスである可能性が残ります。だから「まず完全一致を試す」という一段目が必要になるわけです。
Gmail : + を見つけたら即座に切り落とす(1回で解決)
Exchange : まず完全一致 → 見つからなければ + を削って再検索(2回)
同じ + でも挙動が違うのは、各サービスがユーザー名にどんな文字を許してきたかの歴史の差、ということになります。
また、Exchange Onlineには重要な制約が明記されています。
- プラスアドレスから送信することはできません(受信専用)
- メールボックスに設定されたエイリアスではないので、Outlookの宛先欄で名前に解決されません
ただし、これをそのまま他社にも当てはめるのは間違いです。
Gmailの場合、何もしなければ確かに返信時の送信元は元の user@gmail.com になります。 ですが、設定 →「アカウントとインポート」→「他のメールアドレスを追加」からプラスアドレスを登録し、確認メールで所有確認を済ませれば、送信元として選べるようになります。[^8]
このとき、確認メール自体がプラスアドレス経由で自分の受信トレイに届くので、所有確認はすんなり通ります。
「受信専用」はExchange Onlineの仕様であって、プラスアドレス一般の性質ではない、というのがここでの結論です。
日本のキャリアメールはどうなのか
日本でテストをするなら、ここは避けて通れません。docomo / au / SoftBank のキャリアメールです。
結論から言うと、プラスアドレス対応をうたっているキャリアは1社もありません。使えないものとして設計してください。
理由は単純で、そもそもアドレスに + という文字を使えないからです。3社とも公式に、使える記号を3つだけに限定しています。
| キャリア |
ローカル部に使える文字 |
+ |
| docomo(@docomo.ne.jp) |
半角英数字と _ . - の3記号のみ(3〜30文字)11 |
使用不可 |
| au(@au.com / @ezweb.ne.jp) |
半角英数小文字と - . _ のみ(最大30文字)12 |
使用不可 |
| SoftBank(@softbank.ne.jp 等) |
半角英小文字・数字と - . _ 以外の記号は使用できません13 |
使用不可 |
ここで注意したいのは、この表が答えているのは 「自分のアドレスに + を入れられるか」 であって、「+tag 付きで送ったメールが届くか」ではないということです。この2つは別の話でした(3章の冒頭で分けたのと同じ理屈です)。
そして皮肉なことに、キャリアはGmailと同じ条件を満たしています。
Gmail : ユーザー名に + を使えない → + は必ず区切り文字 → 安心して切り落とせる
キャリア : ユーザー名に + を使えない → 同じ条件のはず
理屈のうえでは、キャリアがプラスアドレスに対応するのを妨げるものは何もありません。にもかかわらず、対応するという記載がどこにもない。 この状況で「たぶん動くだろう」と踏むのは、テストの前提としては危険すぎます。
実務的な結論はこうなります。
- キャリアメールでプラスアドレスは使えないものとして扱う
- キャリアメール宛の登録テストで複数ユーザーが必要なら、実際に複数の回線・アドレスを用意するしかない
- どうしても確認したいなら、実機に送って確かめる(届かなければ即座に分かります)
注意: 各サービスの仕様は変わります。重要な用途で使う前に、別のメールアドレスから自分宛てにテスト送信して、実際に届くことを確認してください。これはキャリアメールに限らず、この章の表全体に言えることです。
4. Gmailにはもう一つの「別名」がある ― ドットは無視される
プラスアドレスを調べていて見つけた、もう一つの落とし穴です。
Gmail(@gmail.com)では、ローカル部のドット(.)が無視されます。14
user@gmail.com
us.er@gmail.com
u.s.e.r@gmail.com
これらは すべて同じアカウントです。Googleの公式ヘルプにも明記されています。
さらに、他人が u.s.e.r@gmail.com で新規アカウントを作ろうとしても「既に使われています」となります。ドット違いは全部自分のものです。
ここが罠
この挙動は @gmail.com だけです。
Google Workspaceで独自ドメイン(yourdomain.com や yourschool.edu)を使っている場合、ドットは区別されます。[^5]
つまり、
| |
+tag |
ドット |
| @gmail.com |
無視される |
無視される |
| Google Workspace(独自ドメイン) |
無視される |
区別される(別アドレス) |
テストで「ドット違いで別アカウントを作ろう」とすると、@gmail.comでは失敗しますが、Workspaceドメインでは成功します。同じ「Gmail」と呼んでいても挙動が違う、というのは覚えておいて損はないです。
5. 実際の使い方
5-1. Gmailのフィルタで自動ラベルを付ける
たとえば user+test@gmail.com 宛てのメールに「テスト」というラベルを自動で付けられます。Gmailには条件に応じてメールを処理するフィルタ機能があります。
条件の書き方には2通りあります。
to:user+test@gmail.com ← To/Cc/Bcc に書かれていたか
deliveredto:user+test@gmail.com ← 実際に配送されたアドレスを見る
to: は誤解されやすいのですが、Toヘッダだけを見ているわけではありません。 Gmail APIのリファレンスには、to は "Includes recipients in the 'to', 'cc', and 'bcc' header fields."(To / Cc / Bcc を含む)と書かれています。
一方 deliveredto: は「実際にどのアドレス宛てに配送されたか」を見ます。15
メーリングリストや転送を経由すると、To / Cc / Bcc のどこにも自分のアドレスが載らないことがあります。そういうケースでは deliveredto: のほうが素直に引っかかります。
手順で詰まりやすいところ
- Gmailの検索ボックス右端の「検索オプション」を開く
- 「含む」欄に
deliveredto:user+test@gmail.com と、演算子ごと文字列で入力する
- 「フィルタを作成」→「ラベルを付ける」
ポイントは2です。検索オプションのパネルには From / To / 件名 / 含む / 含まない / サイズ / 期間 しかなく、deliveredto という専用の欄はありません。 「含む」欄に演算子ごと書きます。ここを知らないとパネルを開いた時点で詰まります。
また、すべての検索演算子がフィルタで機能するわけではありません(日付系の演算子は受信時フィルタでは働きません)。フィルタを作る前に、同じ文字列を検索ボックスにそのまま入れて結果が出ることを確かめてください。検索でヒットしないものは、フィルタでも一致しません。
配送先を自分の目で確かめる
deliveredto: が何を見ているのか気になったら、実物を見るのが早いです。
Gmailでメールを開き、右上の「⋮」→「メッセージのソースを表示」を選ぶと、先頭付近にこのヘッダがあります。
Delivered-To: user+test@gmail.com
+test が残っていることが確認できます。AIが勝手に登録したアドレスを後から追跡したいときも、ここを見るのが確実です。
5-2. 名簿流出の手がかりにする
user+shopA@gmail.com でA社に登録しておけば、そのアドレス宛てに知らない業者から広告が来たとき、A社まわりの経路から流出・共有された可能性がある、という手がかりになります。
ただし 「A社が漏らした」と断定はできません。 A社の委託先、決済代行、メール配信業者など経路は複数あり得ますし、アドレスを機械的に推測・収集された可能性もゼロではありません。
そして 過信も禁物です。+ 以降を機械的に削るのは一行のコードでできるので、悪意のある業者は簡単に本体アドレスを取り出せます。
「漏れたら気づける可能性がある」仕組みであって、「漏れなくする」仕組みでも「犯人を特定する」仕組みでもありません。
本気で本体アドレスを隠したいなら
+ の弱点は「本体アドレスが丸見え」という一点に尽きます。本気で隠したいなら、別の道具を使うことになります。
| 手段 |
本体アドレスを逆算できるか |
備考 |
| プラスアドレス |
できる(+ 以降を削るだけ) |
分類用。匿名化には使えない |
| サブドメインアドレッシング |
できる |
Fastmailなど。+ を弾くフォーム対策には有効 |
| 独自ドメインのcatch-all |
できない |
shopA@mail.example.com のように自由に作れる |
| Hide My Email(iCloud+) |
できない |
ランダム文字列の転送アドレス |
| SimpleLogin / Firefox Relay |
できない |
同上。個別に無効化もできる |
独自ドメインを持っているなら、Cloudflare Email Routing も選択肢になります(2025年7月にサブアドレッシングに対応しました)。
要するに、+ は「分類のための道具」であって「匿名化のための道具」ではない、というのがここの結論です。
6. 開発・テストで使うときの注意(ここが本題)
自分がAIに指示したのは「テスト」でした。なので、テスト目線で気をつける点を整理します。
6-1. サービス側が「同じ人」とみなす場合がある
不正利用対策として、登録時にメールアドレスを正規化(normalize)するサービスがあります。
これは無料トライアルの繰り返し利用や、1メールアドレス1アカウント制限の回避を防ぐための、よくある対策です。
その結果、
user+1@gmail.com で登録 → 「そのメールアドレスは既に登録されています」
- あるいは、そもそも
+ を含むアドレスを入力段階で弾かれる
ということが起きます。
テストケースを「+1, +2, +3 で3ユーザー作る」という前提で組むと、この仕様に当たった瞬間に全部崩れます。
逆に、自分が作る側になったときの注意
ここまでは「テストする側」の話ですが、自分がその正規化を実装する側になったときのほうが危険です。
前述のとおり、+ の意味もドットの意味も ドメインごとに違います。 それを全アドレスに一律で適用すると、こうなります。
全ドメインに「ドットを削る」を適用すると…
taro.yamada@example.co.jp ← 別人
tarohyamada@example.co.jp ← ではなく taroyamada@… と衝突しうる
+ やドットに特別な意味を持たせていないドメインは普通にあります。そこで正規化をかけると、別人のアカウントを同一人物として統合してしまうという、かなり厄介な事故になります。
重複登録を防ぎたいなら、アドレスをいじるのではなく、
- メール確認済みかどうか
- 決済情報
- デバイス・利用状況のシグナル
といった別の軸を併用するほうが安全です。アドレスの文字列加工は、それ単体では不正対策として弱く、副作用が大きいという認識でいたほうがいいと思います。
さらに怖いのは「正規化する場所としない場所がある」こと
これがテスト観点として一番おいしいところだと思います。
正規化を「登録時だけ」実装してしまうと、こうなります。
登録時 : user+1@gmail.com → 正規化して user@gmail.com で保存
ログイン : user+1@gmail.com → 正規化せずそのまま照合 → 該当なし
→ 「登録できたのにログインできない」
もっと危ないのがこちらです。
パスワードリセット要求 : user+1@gmail.com
→ 正規化して user@gmail.com のアカウントを特定
→ リセットメールの送信先は入力された user+1@gmail.com
= 正規化が認証の境界を跨いでしまっている
Gmailなら同じ受信箱なので実害は出ませんが、+ を区切り文字として扱っていないドメインで同じことをやると、別人にリセットメールを送ることになります。
なので、正解は「正規化するかどうかを決める」ではなく、
登録・ログイン・パスワードリセット・重複判定のすべてで、同じ正規化関数を通す
です。どこか一箇所だけ違う、が事故のもとです。
6-2. フォーム形式のURLでは + が半角スペースに化ける
これが一番刺さる地雷だと思います。
まず正確に書くと、URL一般の規則ではありません。 URLのクエリ部分そのものには + を含められます。
問題になるのは application/x-www-form-urlencoded(HTMLフォームの標準的な送信形式、および多くのフレームワークがクエリ文字列を解釈するときの形式)として パースされたときです。この形式では、+ は半角スペースを意味します。16
?email=user+test@gmail.com
↓ サーバー側でデコードすると
user test@gmail.com ← 半角スペースになる(壊れる)
正しくは %2B にエンコードする必要があります。
?email=user%2Btest%40gmail.com
↓ デコード
user+test@gmail.com ← 正しい
始末が悪いのは「環境によって壊れたり壊れなかったりする」こと
同じURLでも、受け取った側が何でデコードするかで結果が変わります。
| デコード方法 |
+ はどうなるか |
PHP の $_GET |
半角スペース |
Java の URLDecoder.decode |
半角スペース |
Python の urllib.parse.parse_qs |
半角スペース |
Rails の params |
半角スペース |
JavaScript の decodeURIComponent |
そのまま + |
Python の urllib.parse.unquote |
そのまま + |
さらに、同じアプリでもパス成分なら壊れません。
/verify?email=user+test@... ← form-urlencoded として読まれると壊れる
/verify/user+test@... ← パス成分なので + はそのまま
「再現する環境としない環境がある」バグになるので、原因にたどり着くまでが長いです。
会員登録のメール確認リンクやパスワードリセットリンクにメールアドレスをクエリで載せている実装だと、プラスアドレスのユーザーだけ確認リンクが動かないという形で表面化します。
根本的な対策は、URLを文字列連結で組み立てないことです。URLSearchParams(JavaScript)や各言語標準のエンコーダに任せれば、+ も @ も適切にエスケープされます。手で組み立てるのをやめるのが一番効きます。
プラスアドレスでテストすると、この手のバグが炙り出せます。これはむしろプラスアドレスでテストする価値がある部分です。
6-3. そもそも確認リンクにメールアドレスを載せない
%2B にエンコードすれば動きますが、一段引いて考えると 「確認リンクにメールアドレスを載せる」設計自体を見直したほうがいいです。
URLに載せたメールアドレスは、
- Webサーバーやプロキシのアクセスログ
- ブラウザの履歴
- 外部リソースを踏んだときのRefererヘッダ
に残り得ます。エンコードの問題以前に、個人情報が想定外の場所にコピーされていく設計です。
確認・再設定リンクに載せるのは、
- 十分に推測困難
- 短命(数十分〜数時間で失効)
- 一回使ったら無効
なトークンだけにして、そのトークンとユーザーの対応付けはサーバー側で持つ。こうすれば + の問題も同時に消えます。
6-4. テスト観点としてまとめると
| 観点 |
確認すること |
| 入力バリデーション |
+ を含むアドレスを弾いていないか(正規表現の作りすぎ) |
| 重複判定 |
a+1@ と a@ を同一人物とみなす仕様か、別人とみなす仕様か(どちらが正解かは要件次第。決めて明文化する) |
| 正規化の適用範囲 |
ドメインを問わず一律にドット除去・+ 除去をしていないか |
| 正規化の一貫性 |
登録・ログイン・リセット・重複判定で同じ関数を通しているか |
| URLエンコード |
確認リンク・リセットリンクで + が %2B になっているか |
| 長さの上限 |
ローカル部は 64オクテット、アドレス全体は経路長として 256オクテットが上限17。DBのカラム長と入力チェックはこれに耐えるか |
| 国際化アドレス |
ローカル部に非ASCIIを含むアドレス(SMTPUTF8)を受け付ける想定があるか。あるなら正規表現・DB照合順序・送信ライブラリすべてが対象 |
| 画面表示 |
長いアドレスでレイアウトが崩れないか |
| メール送信 |
テスト環境から実在のアドレスに本物のメールが飛んでいないか |
長さについて補足すると、SMTPの仕様(RFC 5321)ではローカル部の上限が64オクテットです。[^12]
user+testcase1-1@gmail.com
^^^^^^^^^^^ タグを足すぶんローカル部は伸びる
タグを長くすると普通に上限に当たります。AIに機械的にタグを生成させると、ここを平気で超えてきます。テストで極端に長いタグを使うときは、弾かれたのがアプリのバグなのか仕様どおりなのかを切り分けられるようにしておくと混乱しません。
おまけ:メールアドレスの正規表現を自作しない
「+ を弾いていないか」の話に戻りますが、そもそも メールアドレスの検証正規表現を自分で書くべきではありません。
HTMLの仕様(WHATWG HTML Standard)には <input type="email"> が受け付けるアドレスの正規表現が載っていて、当然 + も含まれています。そして仕様自身がこう注記しています。
This requirement is a willful violation of RFC 5322, which defines a syntax for email addresses that is simultaneously too strict (before the "@" character), too vague (after the "@" character), and too lax ... to be of practical use here.
(この要件はRFC 5322に対する意図的な違反である。RFC 5322のメールアドレス構文は、@ の前については厳しすぎ、@ の後については曖昧すぎ、かつ全体としては緩すぎて、ここで実用に供さない)
HTML仕様ですら「RFC 5322 に故意に違反する」と宣言している世界です。そこに手書きの正規表現で挑むと、ほぼ確実に + を含む正当なアドレスを弾きます。
<input type="email"> を使う
- サーバー側は言語標準・定番ライブラリのバリデータを使う
- 最終的な到達性は確認メールを送って確かめる(結局これが唯一の確実な検証です)
これで十分です。
日本だと逆方向の圧力もかかる ― RFC違反アドレス
ここまでは「厳しすぎる正規表現が + を弾く」という話でしたが、日本のサービスでは真逆の問題が同時に起きます。
2009年3月頃まで、docomo と au では RFCに違反したアドレスを作れました。
abc..def@docomo.ne.jp ← ピリオドが連続している
abcde.@docomo.ne.jp ← @ の直前がピリオド
RFC 5322 の dot-atom は「ピリオドは atom の区切り」なので、連続したピリオドも末尾のピリオドも構文違反です。
新規発行はとっくに止まっていますが、当時取ったアドレスを今も使い続けている人がいます。 そして厄介なことに、
- キャリア同士では送受信できる
- しかしRFC準拠側のシステムからは送信できないことがある
という状態になっています。
つまり日本でメールアドレスの入力欄を作ると、こういう板挟みになります。
| 正規表現の傾向 |
起きること |
| 厳しすぎる |
+ 付きアドレスを弾く(この記事の主題) |
| RFCに忠実 |
RFC違反のキャリアアドレスを持つ実在ユーザーを弾く |
| 緩すぎる |
明らかな打ち間違いを通してしまう |
RFCに忠実に実装すれば正しい、という話ではないのが日本の事情です。
だからこそ、結論は先ほどと同じところに落ち着きます。
正規表現で到達性は判定できない。確認メールを送って、届いたかどうかで判断する。
+ 付きアドレスも、RFC違反のキャリアアドレスも、この方針なら等しく正しく扱えます。「バリデーションは打ち間違いを減らすためのもの、到達確認は別」と割り切るのが、結局いちばん事故が少ないと思います。
6-5. そもそも実在のアドレスを使うべきか
プラスアドレスは「実在の自分のメールボックスに本当にメールが届く」仕組みです。
つまり、テストを回した分だけ自分の受信トレイが汚れます。CIで毎晩回したら悲惨なことになります。
開発・テスト環境では、メールを実際には外に出さずに受け止める仕組みを使うほうが安全です。
- Mailpit … ローカルでSMTPを受けてWeb UIで中身を確認できる。MailHogの後継で、ポート(SMTP 1025 / UI 8025)もAPIも互換。MailHog自体は2020年頃から更新が止まっており、Laravel Sail や DDEV も Mailpit に移行済みなので、新規採用ならこちら
- Mailtrap … 同様のSaaS
- SMTPの送信先をステージング用に差し替えて外に出さない
プラスアドレスが向いているのは、「本番相当の経路で本当に届くかを人間が確認する」少数のケースです。網羅的な自動テストには向きません。
6-6. ドキュメントやリポジトリに残す場合
テストケースやサンプルコードに実在のメールアドレスを書くと、リポジトリ・スクリーンショット・課題管理ツールに個人のアドレスが残り続けます。
RFC 2606 では、ドキュメントやテスト用に予約された名前が定義されています。18
example.com / example.net / example.org
- TLD:
.test / .example / .invalid
- (
.localhost も予約されていますが、これはローカルホスト解決用です。user@localhost は実際にローカル配送され得るので、メールアドレスの例には使わないでください)
これらは第三者が登録して自分のものにすることができません。「勝手に他人のドメインを例に使ってしまった」という事故を防げます。公開するドキュメントに書くアドレスは user@example.com にしておくのが定石です。
細かい注意: 「予約されている」=「DNS上に存在しない」ではありません。example.com は実際にIANAが運用していて名前解決できますし、.localhost はループバックアドレスに向くよう静的に定義されることがある、とRFC 2606自身が述べています。「誰にも取られない安全な例示用の名前」という理解が正確です。なお .test / .invalid / .localhost は、その後 RFC 6761 で特別用途ドメインとして整理し直されています。
7. AIにテストを任せるときの教訓
今回の件、AIは間違ったことをしていません。「1つのメールアドレスで複数ユーザーのテストをする」ための業界の定石を、当たり前のように使っただけです。
ただ、指示した側からすると 「言っていないことをやられた」 のも事実です。
そして、それが今回はたまたま無害だっただけで、
- テスト環境のつもりが本番にメールを送ってしまう
- 外部サービスに本当にアカウントを作ってしまう
- 個人のメールアドレスが成果物に残る
といった副作用は、同じ構造から普通に起こり得ます。
一番厄介なのは「消せない残骸」
このうち「アカウントを作ってしまう」が、あとから効いてきます。
AIは作ったアカウントの消し方までは面倒を見てくれません。 そして今回のケースだと、
- どのアドレスで登録したかは、受信トレイを漁らないと分からない
- もしサービス側が
+ を正規化していたら、本体アドレスで登録されている(= + では追跡できない)
つまり、後から棚卸しするのが地味に難しいのです。今回は自分の受信トレイに user+testcase1-1@gmail.com 宛てのメールが届いていたから気づけましたが、これが正規化されていたら気づかなかったかもしれません。
だからこそ、「使ったアドレスを最後に一覧で報告させる」ことに意味があります。 あとから追跡できないものは、その場で記録させるしかありません。
なので、指示の出し方をこう変えることにしました。
【悪い例】
テストは user@gmail.com でお願いします
【良い例】
テストで使うメールアドレスは以下のルールに従ってください。
- 複数ユーザーが必要な場合は user+テスト内容@gmail.com 形式を使ってよい
- 実際にメールを送信する操作を行う前に、必ず確認を取ること
- ドキュメントやコードに残すサンプルは example.com を使うこと
- 使用したアドレスは最後に一覧で報告すること
ポイントは 「使ってよい/だめ」を決めることより、使ったものを報告させること だと思います。
今回モヤっとした原因は、+ の存在そのものではなく、知らないうちに増えていたことでしたので。
まとめ
| |
|
| 正体 |
「サブアドレス」という広く使われている慣行。RFC 5233 に定義がある。ただし配送動作を決めた標準規格ではなく、挙動は各サービス次第 |
| 仕組み |
+ 以降は無視され、本体のメールボックスに届く。設定不要 |
| 受信 |
Gmail / Workspace / Outlook / Proton / Fastmail は対応。iCloudは公式に記載がなく要確認 |
| 日本のキャリアメール |
対応表明なし。 docomo / au / SoftBank ともアドレスに + を使えず、使えない前提で設計する |
| 日本特有の事情 |
2009年頃までのdocomo/auにはRFC違反アドレスが存在し、今も使われている。RFCに忠実な正規表現は実在ユーザーを弾く |
| 送信 |
サービスによる。 Exchange Onlineは受信専用。Gmailは所有確認をすれば送信元にできる |
| Gmail特有 |
@gmail.com はドットも無視される(Workspace独自ドメインは区別される) |
| 最大の地雷 |
application/x-www-form-urlencoded として解釈されると + が半角スペースになる。%2B にエンコードが必要 |
| 実装側の地雷 |
ドット除去・+ 除去の正規化を全ドメインに一律適用すると、別人を統合する事故になる |
| 使いどころ |
サービスごとの振り分け、少数の手動テスト |
| 向かないところ |
網羅的な自動テスト(Mailpit等を使う)、公開ドキュメント(example.comを使う) |
気になる人は、別のメールアドレスから自分の +test 宛てに1通送ってみてください。届いた瞬間に全部わかります。
脚注