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 }