ScalaでDNS lookupする

イタンジのなんでも屋エンジニアぽんこつです。最近業務の基幹機能がScalaで置き換えられることによってようやく沢山Scalaが書けるよやったね!

メールアドレスのホストの妥当性をチェックしたいときなど、DNSのlookupをする機会はまあまあありますが、それをScalaでやるときの話です。

基本的にはJavaに同梱されているjavax.naming.directoryを使えばいいです。すごいAPIまわりが古くさくてしんどいですが、枯れているので普通に使う分には問題ないでしょう。

ついでに今回要件としては

  • メールアドレスのホストの妥当性をチェックしたいのでMXを使う
  • 何度もDNSを引きたくないのでヒープにLRUCacheを持つ

の2つがあり、結果的に以下のように若干複雑な感じになりました。

import java.util
import java.util.Collections
import javax.naming.directory.InitialDirContext

import scala.util.Try
import scala.collection.JavaConverters._

object Main {
def main(args: Array[String]): Unit = {
val dns = new DNS()
println(s"itandi.co.jp: ${dns.hasMx("itandi.co.jp")}")
println(s"itandi.co.jp: ${dns.hasMx("itandi.co.jp")}")
println(s"example.com: ${dns.hasMx("example.com")}")
println(s"example.com: ${dns.hasMx("example.com")}")
}
}

class DNS {
val cache = Collections.synchronizedMap(new LRUCache[String, Boolean](1000))
val EnvTable = Map("java.naming.factory.initial" -> "com.sun.jndi.dns.DnsContextFactory")
val Env = new util.Hashtable[String, String](EnvTable.asJava)
val ictx = new InitialDirContext(Env)

def hasMx(host: String): Boolean = {
Option(cache.get(host)).getOrElse {
val attrs = Try { ictx.getAttributes(host, Array("MX", "A")) }.toOption
val res = attrs.exists(0 < _.size)
cache.put(host, res)
res
}
}
}

class LRUCache[K, V](limit: Int) extends java.util.LinkedHashMap[K, V](16, 0.75f, true) {
override def removeEldestEntry(entry: java.util.Map.Entry[K, V]): Boolean =
limit < size()
}

こんな感じで書けます。

注意事項とか

Cacheについて

真面目にやるならTTLでCacheを失効させるべきで、自前でDNSキャッシュサーバを立てて運用する方が良いです。ただメール送信前のホスト名のチェックという用途なら然程問題ではないと判断して今回はこうなってます。

ホストの存在は到着を保証しない

このチェックはMXレコードかAレコードの存在をチェックしますが、あったとしてもメールが受信できる状態になっているかは分かりません。なので不達を減らす効果しかありません。