最近遇到一个需求:需要验证用户填写的邮箱地址是否真实存在,是否可达。和普通的正则表达式不同,他要求尝试链接目标邮箱服务器并请求校验目标邮箱是否存在。
先来了解
DNS之MX记录
对于DNS不了解的,请移步百度搜索。
DNS中除了A记录(域名-IP映射)之外,还有MX记录(邮件交换记录),CNAME记录(别名,咱不管)。
MX记录就是为了在发送邮件时使用友好域名规则,比如我们发送到QQ邮箱[email protected]。我们填写地址是到“qq.com”,但实际上可能服务器地址千奇百怪/而且许有多个。在设置DNS时可以顺带设置MX记录。
说白了,“qq.com”只是域名,做HTTP请求响应地址,你邮件能发到Tomcat上吗?那我们发到“qq.com”上面的邮件哪里去了,我们把自己想象成一个邮件服务器,你的用户让你给[email protected]发一封信,你如何操作?找mx记录是必要的。
SMTP之纯Socket访问
对于SMTP不了解或Java Socket不了解的,请移步百度搜索。
邮件协议是匿名协议,我们通过SMTP协议可以让邮件服务器来验证目标地址是否真实存在。
代码实现
由以上介绍可知:通过DNS中MX记录可以找到邮件服务器地址,通过SMTP协议可以让邮件服务器验证目标邮箱地址的真实性。
那么我们就来进行编码实现。
首先需要查询DNS,这个需要用到一个Java查询DNS的组件dnsjava(下载),自己写太麻烦。
1 // 查找mx记录 2 Record[] mxRecords = new Lookup(host, Type.MX).run(); 3 if(ArrayUtils.isEmpty(mxRecords)) return false; 4 // 邮件服务器地址 5 String mxHost = ((MXRecord)mxRecords[0]).getTarget().toString(); 6 if(mxRecords.length > 1) { // 优先级排序 7 List<Record> arrRecords = new ArrayList<Record>(); 8 Collections.addAll(arrRecords, mxRecords); 9 Collections.sort(arrRecords, new Comparator<Record>() { 10 11 public int compare(Record o1, Record o2) { 12 return new CompareToBuilder().append(((MXRecord)o1).getPriority(), ((MXRecord)o2).getPriority()).toComparison(); 13 } 14 15 }); 16 mxHost = ((MXRecord)arrRecords.get(0)).getTarget().toString(); 17 }
mx
(上面代码中的生僻类型就是来自dnsjava,我使用apache-commons组件来判断空值和构建排序,return false是在查询失败时。)
接下来通过优先级排序(mx记录有这个属性)取第一个邮件服务器地址来链接。
这里的主要代码是通过SMTP发送RCPT TO指令来指定邮件接收方,如果这个地址存在则服务器返回成功状态,如果没有的话则返回错误指令。
1 // 验证 2 bufferedWriter.write("RCPT TO: <" + toMail + ">\r\n"); 3 bufferedWriter.flush(); 4 if(getResponseCode(timeout, sleepSect, bufferedReader) != 250) { 5 return false; 6 }
(上面代码中toMail即要验证的邮件地址)
下面是全部代码
1 import java.io.BufferedInputStream; 2 import java.io.BufferedReader; 3 import java.io.BufferedWriter; 4 import java.io.IOException; 5 import java.io.InputStreamReader; 6 import java.io.OutputStreamWriter; 7 import java.net.InetSocketAddress; 8 import java.net.Socket; 9 import java.util.ArrayList; 10 import java.util.Collections; 11 import java.util.Comparator; 12 import java.util.List; 13 14 import org.apache.commons.lang.ArrayUtils; 15 import org.apache.commons.lang.StringUtils; 16 import org.apache.commons.lang.builder.CompareToBuilder; 17 import org.xbill.DNS.Lookup; 18 import org.xbill.DNS.MXRecord; 19 import org.xbill.DNS.Record; 20 import org.xbill.DNS.TextParseException; 21 import org.xbill.DNS.Type; 22 23 24 public class MailValid { 25 26 public static void main(String[] args) { 27 System.out.println(new MailValid().valid("[email protected]", "jootmir.org")); 28 } 29 30 /** 31 * 验证邮箱是否存在 32 * <br> 33 * 由于要读取IO,会造成线程阻塞 34 * 35 * @param toMail 36 * 要验证的邮箱 37 * @param domain 38 * 发出验证请求的域名(是当前站点的域名,可以任意指定) 39 * @return 40 * 邮箱是否可达 41 */ 42 public boolean valid(String toMail, String domain) { 43 if(StringUtils.isBlank(toMail) || StringUtils.isBlank(domain)) return false; 44 if(!StringUtils.contains(toMail, ‘@‘)) return false; 45 String host = toMail.substring(toMail.indexOf(‘@‘) + 1); 46 if(host.equals(domain)) return false; 47 Socket socket = new Socket(); 48 try { 49 // 查找mx记录 50 Record[] mxRecords = new Lookup(host, Type.MX).run(); 51 if(ArrayUtils.isEmpty(mxRecords)) return false; 52 // 邮件服务器地址 53 String mxHost = ((MXRecord)mxRecords[0]).getTarget().toString(); 54 if(mxRecords.length > 1) { // 优先级排序 55 List<Record> arrRecords = new ArrayList<Record>(); 56 Collections.addAll(arrRecords, mxRecords); 57 Collections.sort(arrRecords, new Comparator<Record>() { 58 59 public int compare(Record o1, Record o2) { 60 return new CompareToBuilder().append(((MXRecord)o1).getPriority(), ((MXRecord)o2).getPriority()).toComparison(); 61 } 62 63 }); 64 mxHost = ((MXRecord)arrRecords.get(0)).getTarget().toString(); 65 } 66 // 开始smtp 67 socket.connect(new InetSocketAddress(mxHost, 25)); 68 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new BufferedInputStream(socket.getInputStream()))); 69 BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); 70 // 超时时间(毫秒) 71 long timeout = 6000; 72 // 睡眠时间片段(50毫秒) 73 int sleepSect = 50; 74 75 // 握手 76 bufferedWriter.write("HELO " + domain + "\r\n"); 77 bufferedWriter.flush(); 78 if(getResponseCode(timeout, sleepSect, bufferedReader) != 220) { 79 return false; 80 } 81 // 身份 82 bufferedWriter.write("MAIL FROM: <[email protected]" + domain + ">\r\n"); 83 bufferedWriter.flush(); 84 if(getResponseCode(timeout, sleepSect, bufferedReader) != 250) { 85 return false; 86 } 87 // 验证 88 bufferedWriter.write("RCPT TO: <" + toMail + ">\r\n"); 89 bufferedWriter.flush(); 90 if(getResponseCode(timeout, sleepSect, bufferedReader) != 250) { 91 return false; 92 } 93 // 断开 94 bufferedWriter.write("QUIT\r\n"); 95 bufferedWriter.flush(); 96 return true; 97 } catch (NumberFormatException e) { 98 } catch (TextParseException e) { 99 } catch (IOException e) { 100 } catch (InterruptedException e) { 101 } finally { 102 try { 103 socket.close(); 104 } catch (IOException e) { 105 } 106 } 107 return false; 108 } 109 110 private int getResponseCode(long timeout, int sleepSect, BufferedReader bufferedReader) throws InterruptedException, NumberFormatException, IOException { 111 int code = 0; 112 for(long i = sleepSect; i < timeout; i += sleepSect) { 113 Thread.sleep(sleepSect); 114 if(bufferedReader.ready()) { 115 String outline = bufferedReader.readLine(); 116 // FIXME 读完…… 117 while(bufferedReader.ready()) 118 bufferedReader.readLine(); 119 //System.out.println(outline); 120 code = Integer.parseInt(outline.substring(0, 3)); 121 break; 122 } 123 } 124 return code; 125 } 126 }
(解锁119行代码和输出118行数据可以让你更加清晰SMTP协议)
对于企业邮箱,可能无法正常验证,这个是因为服务器问题。另外,dnsjava查询DNS是有缓存的。
现在工作越来越紧张,对于技术的理解也较以前深刻。现在写出的代码可能较为精简(这意味着如果你是新手可能不容易理解)。
联系我,一起交流
欢迎您移步我们的交流群,无聊的时候大家一起打发时间:
或者通过QQ与我联系:
(最后编辑时间2015-04-28 22:03:03)