001 /**************************************************************** 002 * Licensed to the Apache Software Foundation (ASF) under one * 003 * or more contributor license agreements. See the NOTICE file * 004 * distributed with this work for additional information * 005 * regarding copyright ownership. The ASF licenses this file * 006 * to you under the Apache License, Version 2.0 (the * 007 * "License"); you may not use this file except in compliance * 008 * with the License. You may obtain a copy of the License at * 009 * * 010 * http://www.apache.org/licenses/LICENSE-2.0 * 011 * * 012 * Unless required by applicable law or agreed to in writing, * 013 * software distributed under the License is distributed on an * 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 015 * KIND, either express or implied. See the License for the * 016 * specific language governing permissions and limitations * 017 * under the License. * 018 ****************************************************************/ 019 020 package com.hs.mail.dns; 021 022 import java.io.IOException; 023 import java.net.InetAddress; 024 import java.net.UnknownHostException; 025 import java.util.ArrayList; 026 import java.util.Arrays; 027 import java.util.Collection; 028 import java.util.Collections; 029 import java.util.Comparator; 030 import java.util.Iterator; 031 import java.util.List; 032 import java.util.Random; 033 034 import org.apache.log4j.Logger; 035 import org.springframework.beans.factory.InitializingBean; 036 import org.xbill.DNS.CNAMERecord; 037 import org.xbill.DNS.Cache; 038 import org.xbill.DNS.Credibility; 039 import org.xbill.DNS.DClass; 040 import org.xbill.DNS.ExtendedResolver; 041 import org.xbill.DNS.Lookup; 042 import org.xbill.DNS.MXRecord; 043 import org.xbill.DNS.Message; 044 import org.xbill.DNS.Name; 045 import org.xbill.DNS.RRset; 046 import org.xbill.DNS.Rcode; 047 import org.xbill.DNS.Record; 048 import org.xbill.DNS.Resolver; 049 import org.xbill.DNS.ResolverConfig; 050 import org.xbill.DNS.SetResponse; 051 import org.xbill.DNS.TextParseException; 052 import org.xbill.DNS.Type; 053 054 import com.hs.mail.smtp.message.HostAddress; 055 056 /** 057 * Provides DNS client functionality. 058 * 059 */ 060 public class DnsServer implements InitializingBean { 061 062 static Logger logger = Logger.getLogger(DnsServer.class); 063 064 /** 065 * A resolver instance used to retrieve DNS records. 066 */ 067 protected Resolver resolver; 068 069 /** 070 * A TTL cache of results received from the DNS server. 071 */ 072 private Cache cache; 073 074 /** 075 * Maximum number of RR to cache. 076 */ 077 private int maxCacheSize = 50000; 078 079 /** 080 * Whether the DNS response is required to be authoritative 081 */ 082 private int dnsCredibility = Credibility.NONAUTH_ANSWER; 083 084 /** 085 * The DNS servers to be used by this service 086 */ 087 private List<String> dnsServers; 088 089 /** 090 * The MX Comparator used in the MX sort. 091 */ 092 private Comparator<MXRecord> mxComparator = new MXRecordComparator(); 093 094 public String[] getDnsServers() { 095 return dnsServers.toArray(new String[0]); 096 } 097 098 public void setDnsServers(List<String> dnsServers) { 099 this.dnsServers = dnsServers; 100 } 101 102 public void afterPropertiesSet() throws Exception { 103 if (dnsServers == null) { 104 dnsServers = new ArrayList<String>(); 105 } 106 107 // Trying to discover system's DNS servers 108 String[] servers = ResolverConfig.getCurrentConfig().servers(); 109 if (servers != null) { 110 for (String server : servers) { 111 dnsServers.add(server); 112 } 113 } 114 if (dnsServers.isEmpty()) { 115 logger.info("No DNS servers have been specified or found - adding localhost"); 116 dnsServers.add("127.0.0.1"); 117 } 118 119 try { 120 resolver = new ExtendedResolver(getDnsServers()); 121 Lookup.setDefaultResolver(resolver); 122 } catch (UnknownHostException e) { 123 logger.fatal("DNS server counld not be initialized. The DNS servers specified are not recognized hosts.", e); 124 throw e; 125 } 126 127 cache = new Cache(DClass.IN); 128 cache.setMaxEntries(maxCacheSize); 129 Lookup.setDefaultCache(cache, DClass.IN); 130 } 131 132 /** 133 * <p> 134 * Return a prioritized unmodifiable list of MX records obtained from the 135 * server. 136 * </p> 137 * 138 * @param hostname 139 * domain name to look up 140 * 141 * @return a list of MX records corresponding to this mail domain 142 */ 143 public List<String> findMXRecordsRaw(String hostname) { 144 Record answers[] = lookup(hostname, Type.MX); 145 List<String> servers = new ArrayList<String>(); 146 if (answers == null) { 147 return servers; 148 } 149 150 MXRecord mxAnswers[] = new MXRecord[answers.length]; 151 for (int i = 0; i < answers.length; i++) { 152 mxAnswers[i] = (MXRecord) answers[i]; 153 } 154 155 Arrays.sort(mxAnswers, mxComparator); 156 157 for (int i = 0; i < mxAnswers.length; i++) { 158 servers.add(mxAnswers[i].getTarget().toString()); 159 } 160 return servers; 161 } 162 163 /** 164 * <p> 165 * Return a prioritized unmodifiable list of host handling mail for the 166 * domain. 167 * </p> 168 * 169 * <p> 170 * First lookup MX hosts, then MX hosts of the CNAME adress, and if no 171 * server is found return the IP of the hostname 172 * </p> 173 * 174 * @param hostname 175 * domain name to look up 176 * 177 * @return a unmodifiable list of handling servers corresponding to this 178 * mail domain name 179 */ 180 public Collection<String> findMXRecords(String hostname) { 181 List<String> servers = new ArrayList<String>(); 182 try { 183 servers = findMXRecordsRaw(hostname); 184 return Collections.unmodifiableCollection(servers); 185 } finally { 186 // If we found no results, we'll add the original domain name if 187 // it's a valid DNS entry 188 if (servers.size() == 0) { 189 StringBuffer logBuffer = 190 new StringBuffer(128) 191 .append("Couldn't resolve MX records for domain ") 192 .append(hostname) 193 .append("."); 194 logger.info(logBuffer.toString()); 195 Record cnames[] = lookup(hostname, Type.CNAME); 196 Collection<String> cnameMXrecords = null; 197 if (cnames != null && cnames.length > 0) { 198 cnameMXrecords = findMXRecordsRaw(((CNAMERecord) cnames[0]).getTarget().toString()); 199 } else { 200 logBuffer = new StringBuffer(128) 201 .append("Couldn't find CNAME records for domain ") 202 .append(hostname) 203 .append("."); 204 logger.info(logBuffer.toString()); 205 } 206 if (cnameMXrecords == null) { 207 try { 208 getByName(hostname); 209 servers.add(hostname); 210 } catch (UnknownHostException uhe) { 211 // The original domain name is not a valid host, 212 // so we can't add it to the server list. In this 213 // case we return an empty list of servers 214 logBuffer = new StringBuffer(128) 215 .append("Couldn't resolve IP address for host ") 216 .append(hostname) 217 .append("."); 218 logger.error(logBuffer.toString()); 219 } 220 } else { 221 servers.addAll(cnameMXrecords); 222 } 223 } 224 } 225 } 226 227 /** 228 * Looks up DNS records of the specified type for the specified name. 229 * 230 * This method is a public wrapper for the private implementation method 231 * 232 * @param name 233 * the name of the host to be looked up 234 * @param type 235 * the type of record desired 236 */ 237 public Record[] lookup(String name, int type) { 238 return rawDNSLookup(name, false, type); 239 } 240 241 /** 242 * Looks up DNS records of the specified type for the specified name 243 * 244 * @param namestr 245 * the name of the host to be looked up 246 * @param querysent 247 * whether the query has already been sent to the DNS servers 248 * @param type 249 * the type of record desired 250 */ 251 private Record[] rawDNSLookup(String namestr, boolean querysent, int type) { 252 Name name = null; 253 try { 254 name = Name.fromString(namestr, Name.root); 255 } catch (TextParseException e) { 256 logger.error("Couldn't parse name " + namestr, e); 257 return null; 258 } 259 int dclass = DClass.IN; 260 261 SetResponse cached = cache.lookupRecords(name, type, dnsCredibility); 262 if (cached.isSuccessful()) { 263 if (logger.isDebugEnabled()) 264 logger.debug(new StringBuffer(256) 265 .append("Retrieving MX record for ") 266 .append(name).append(" from cache") 267 .toString()); 268 return processSetResponse(cached); 269 } else if (cached.isNXDOMAIN() || cached.isNXRRSET()) { 270 return null; 271 } else if (querysent) { 272 return null; 273 } else { 274 if (logger.isDebugEnabled()) 275 logger.debug(new StringBuffer(256) 276 .append("Looking up MX record for ") 277 .append(name) 278 .toString()); 279 Record question = Record.newRecord(name, type, dclass); 280 Message query = Message.newQuery(question); 281 Message response = null; 282 283 try { 284 response = resolver.send(query); 285 } catch (IOException e) { 286 logger.warn("Query error!", e); 287 return null; 288 } 289 290 int rcode = response.getHeader().getRcode(); 291 if (rcode == Rcode.NOERROR || rcode == Rcode.NXDOMAIN) { 292 cached = cache.addMessage(response); 293 if (cached != null && cached.isSuccessful()) { 294 return processSetResponse(cached); 295 } 296 } 297 298 if (rcode != Rcode.NOERROR) { 299 return null; 300 } 301 302 return rawDNSLookup(namestr, true, type); 303 } 304 } 305 306 protected Record[] processSetResponse(SetResponse sr) { 307 Record[] answers; 308 int answerCount = 0, n = 0; 309 310 RRset[] rrsets = sr.answers(); 311 answerCount = 0; 312 for (int i = 0; i < rrsets.length; i++) { 313 answerCount += rrsets[i].size(); 314 } 315 316 answers = new Record[answerCount]; 317 318 for (int i = 0; i < rrsets.length; i++) { 319 Iterator iter = rrsets[i].rrs(); 320 while (iter.hasNext()) { 321 Record r = (Record) iter.next(); 322 answers[n++] = r; 323 } 324 } 325 return answers; 326 } 327 328 /* 329 * RFC 2821 section 5 requires that we sort the MX records by their 330 * preference, and introduce a randomization. This Comparator does 331 * comparisons as normal unless the values are equal, in which case it 332 * "tosses a coin", randomly speaking. 333 * 334 * This way MX record w/preference 0 appears before MX record w/preference 335 * 1, but a bunch of MX records with the same preference would appear in 336 * different orders each time. 337 * 338 * Reminder for maintainers: the return value on a Comparator can be 339 * counter-intuitive for those who aren't used to the old C strcmp function: 340 * 341 * < 0 ==> a < b = 0 ==> a = b > 0 ==> a > b 342 */ 343 private static class MXRecordComparator implements Comparator<MXRecord> { 344 private final static Random random = new Random(); 345 public int compare(MXRecord o1, MXRecord o2) { 346 int p1 = o1.getPriority(); 347 int p2 = o2.getPriority(); 348 return (p1 == p2) ? (512 - random.nextInt(1024)) : p1 - p2; 349 } 350 } 351 352 /* 353 * Returns an Iterator over org.apache.mailet.HostAddress, a specialized 354 * subclass of javax.mail.URLName, which provides location information for 355 * servers that are specified as mail handlers for the given hostname. This 356 * is done using MX records, and the HostAddress instances are returned 357 * sorted by MX priority. If no host is found for domainName, the Iterator 358 * returned will be empty and the first call to hasNext() will return false. 359 * The Iterator is a nested iterator: the outer iteration is over the 360 * results of the MX record lookup, and the inner iteration is over 361 * potentially multiple A records for each MX record. DNS lookups are 362 * deferred until actually needed. 363 * 364 * @since v2.2.0a16-unstable 365 * 366 * @param domainName - the domain for which to find mail servers 367 * 368 * @return an Iterator over HostAddress instances, sorted by priority 369 */ 370 public Iterator<HostAddress> getSmtpHostAddress(final String domainName) { 371 return new Iterator<HostAddress>() { 372 private Iterator<String> mxHosts = findMXRecords(domainName).iterator(); 373 private Iterator<HostAddress> addresses = null; 374 375 public boolean hasNext() { 376 /* 377 * Make sure that when next() is called, that we can provide a 378 * HostAddress. This means that we need to have an inner 379 * iterator, and verify that it has addresses. We could, for 380 * example, run into a situation where the next mxHost didn't 381 * have any valid addresses. 382 */ 383 if ((addresses == null || !addresses.hasNext()) && mxHosts.hasNext()) { 384 do { 385 final String nextHostname = (String)mxHosts.next(); 386 InetAddress[] addrs = null; 387 try { 388 addrs = getAllByName(nextHostname); 389 } catch (UnknownHostException e) { 390 // this should never happen, since we just got 391 // this host from mxHosts, which should have 392 // already done this check. 393 StringBuffer logBuffer = new StringBuffer(128) 394 .append("Couldn't resolve IP address for discovered host ") 395 .append(nextHostname) 396 .append("."); 397 logger.error(logBuffer.toString()); 398 } 399 final InetAddress[] ipAddresses = addrs; 400 401 addresses = new Iterator<HostAddress>() { 402 int i = 0; 403 404 public boolean hasNext() { 405 return ipAddresses != null && i < ipAddresses.length; 406 } 407 408 public HostAddress next() { 409 return new HostAddress(nextHostname, "smtp://" + ipAddresses[i++].getHostAddress()); 410 } 411 412 public void remove() { 413 throw new UnsupportedOperationException ("remove not supported by this iterator"); 414 } 415 }; 416 } while (!addresses.hasNext() && mxHosts.hasNext()); 417 } 418 return addresses != null && addresses.hasNext(); 419 } 420 421 public HostAddress next() { 422 return addresses != null ? addresses.next() : null; 423 } 424 425 public void remove() { 426 throw new UnsupportedOperationException ("remove not supported by this iterator"); 427 } 428 }; 429 } 430 431 /* 432 * java.net.InetAddress.get[All]ByName(String) allows an IP literal to be 433 * passed, and will recognize it even with a trailing '.'. However, 434 * org.xbill.DNS.Address does not recognize an IP literal with a trailing 435 * '.' character. The problem is that when we lookup an MX record for some 436 * domains, we may find an IP address, which will have had the trailing '.' 437 * appended by the time we get it back from dnsjava. An MX record is not 438 * allowed to have an IP address as the right-hand-side, but there are still 439 * plenty of such records on the Internet. Since java.net.InetAddress can 440 * handle them, for the time being we've decided to support them. 441 * 442 * These methods are NOT intended for use outside of James, and are NOT 443 * declared by the org.apache.james.services.DNSServer. This is currently a 444 * stopgap measure to be revisited for the next release. 445 */ 446 private static String allowIPLiteral(String host) { 447 if ((host.charAt(host.length() - 1) == '.')) { 448 String possible_ip_literal = host.substring(0, host.length() - 1); 449 if (org.xbill.DNS.Address.isDottedQuad(possible_ip_literal)) { 450 host = possible_ip_literal; 451 } 452 } 453 return host; 454 } 455 456 /** 457 * @see java.net.InetAddress#getByName(String) 458 */ 459 public static InetAddress getByName(String host) 460 throws UnknownHostException { 461 return org.xbill.DNS.Address.getByName(allowIPLiteral(host)); 462 } 463 464 /** 465 * @see java.net.InetAddress#getByAllName(String) 466 */ 467 public static InetAddress[] getAllByName(String host) 468 throws UnknownHostException { 469 return org.xbill.DNS.Address.getAllByName(allowIPLiteral(host)); 470 } 471 472 }