ActionMailer で国際化ドメインに対応する
国際化ドメイン
https://日本語.jp みたいなやつは、ブラウザが日本語部分を 7bit-asciiに置き換えた上でリクエストするというルールになっている。
ネットワーク上の既存資産が影響を受けないよう、ブラウザやメーラがその仕事をするという風にIDNで規定されている。
日本語.jp -> xn--wgv71a119e.jp となる。
markdown で誰がどうすべきかみたいなことは決まりがない。ここ zenn では、裸の https://日本語.jp
を貼ると以下のように punycode 変換された上で展開されるが、海外製のサービスやライブラリで同じ挙動は基本的に期待できない。
NAMEPREP と Punycode
日本語を含むドメインは、2段階の変換を経て最終的に 7-bit ascii 文字列となる。
Punycode は非 ascii 部分を抜き出して別にエンコードする。どこから抜き出したかの情報を持っているので元に戻せる仕組みになっている。
「日本語ドメイン名EXAMPLE。jp」(ユーザが入力した文字列)
↓ NAMEPREPによる正規化
「日本語ドメイン名EXAMPLE.jp」
↓ Punycodeによる変換
「xn--example-6q4fyliikhk162btq3b2zd4y2o.jp」
2段目の Punycode は可逆変換だが、1段目の NAMEPREP は非可逆変換となる。
ユーザー入力をDatabase等に保存して表示、編集できるようにする場合、保存前に NAMEPREP をかけるとユーザー入力と保存値が一致しなくなるという問題がある。
メールアドレスにおける国際化ドメインの仕様
メールアドレスは {local}@{domain}
という構造になっており、 domain 部分は国際化ドメインに対応する必要がある。IDNの仕様により、これはメールアプリが変換を担当し、 SMTP プロトコル上では 7bit-ascii を送ることになっている。
local 部分についても原理的には国際化可能だが、ドメイン部と違ってサーバ管理者が好きにルールを決めれば良い部分なので、 IDN では変換を規定していない (つまり普通の ascii しか使えない)
HTML form の挙動
<input type="email">
とすると、 safari では日本語ドメインがバリデーションエラーになる。
chrome 系では初期値だとエラーになるが
何かしら編集するとドメイン部分でNAMEPREPとPunycode変換が実行された上で Javascript に値が渡る。(バグじゃね?
type='url'
の方は何ら変換されない(なぜだ。。。?
ここまでの話をまとめると、
- ブラウザの type='email' 対応は怪しいので当てにならない。普通の type='text' で受けとるのが無難。 Rails 側で変換する。
- DB に入れる前に変換すると表示や編集で元の値を出せなくなるので、変換前の値をDBに保存する。
- サーバから送信する直前に変換する。
という戦略が良さそうだ。
SimpleIDN gem
というわけで、ここからは ruby と rails の話です。SimpleIDN.to_ascii("日本語.jp")
で xn--wgv71a119e.jp
を得られる。 NAMEPREP と Punycode 変換が両方実行される。 email アドレス形式に対応しているわけではないので、 email を変換する場合はdomain部を切り出して渡す必要がある。
Punycode は文字列の中のどの位置に非 ascii 文字が入っていたかを記録しているので、このように結果が変わってしまうのだ。
> SimpleIDN.to_ascii("日本語.jp")
=> "xn--wgv71a119e.jp"
> SimpleIDN.to_ascii("a@日本語.jp")
=> "xn--a@-7t7du0ck91h.jp"
> SimpleIDN.to_ascii("aa@日本語.jp")
=> "xn--aa@-v08fl0dtz6h.jp"
さてコイツを ActionMailer に仕込むのだが、 ActionMailer は内部で mail という gem を使っている。
何はともあれ、私はそれを読み解き、最終的に以下のコードを得たので、皆さんはこれをコピペして使ってください。
class ApplicationMailer < ActionMailer::Base
after_action do
mail[:to].field.addrs.each do |addr|
disp_name = addr.display_name
local, domain = addr.address.split("@")
addr.address = "#{disp_name} <#{local}@#{SimpleIDN.to_ascii(domain)}>"
end
end
Discussion