公司邮箱目前使用的是Zimbra,该邮件服务器目前不甚稳定,经常出现重发、漏发问题。经测试,每100封邮件仅可成功发送98封左右,以下是测试数据:
测试用例1:100封,总用时约:16min;实收97封,失败3次,3次错误信息均为:javax.mail.MessagingException: Could not connect to SMTP host
测试用例2:100封,总用时约:16min;实收100封,失败2次,错误同上。加失败重发机制,失败后等待10s重发,最多重发3次;
测试用例3:每发一封,停留10s,总用时32min;实收100封,失败1次,错误同上;重发机制同用例2.
关于MessagingException的问题,可以参考:
javax.mail.MessagingException: Could not connect to SMTP host
我看了一下几种解释:1.网络;2.防火墙;3.服务器的自我保护,比如防止大批量发送时挂掉或者垃圾邮件,我觉得第三种解释靠谱一些。
针对这种问题,我增加了邮件重发,
if(sendHtmlMail_(mail)){ return true; } else{ int i = 0; //包含群组邮件,失败不重发 boolean isNeedRe = isNeedRe(mail); while(!sendHtmlMail_(mail) && isNeedRe && i < 10){ try { i++; Thread.sleep(1000*60); } catch (InterruptedException e) { LOGGER.error("resend mail error", e); } } return true; }
但这种机制又产生了新的问题,因邮件服务器不稳定导致在仅发送一次的情况下也会向邮件收件人发送邮件,且同一封邮件的收件人(包括抄送、密送)可能部分收到邮件、部分收不到邮件。
针对以上的问题,我们将重发机制去除,仅针对不合法邮件(即服务器上不存在的邮件地址)进行剔除,剔除后再进行发送。而对其他原因导致的邮件发送失败不做重发(该问题将通过邮件服务器运维部门向厂商反映)。
下面是判断邮件是否合法的逻辑:
1.SMTP是工作在两种情况下:一是电子邮件从客户机传输到服务器;二是从某一个服务器传输到另一个服务器
2.SMTP是个请求/响应协议,命令和响应都是基于ASCII文本,并以CR和LF符结束。响应包括一个表示返回状态的三位数字代码
3.SMTP在TCP协议25号端口监听连接请求
4.连接和发送过程
SMTP协议说复杂也不复杂,说简单如果你懂得Socket。不过现在只是我们利用的就是第一条中说的,从客户机传输到服务器,当我们向一台服务器发送邮件时,邮件服务器会首先验证邮件发送地址是否真的存在于本服务器上。
5 操作的步骤如下:
连接服务器的25端口(如果没有邮件服务,连了也是白连)
发送helo问候
发送mail from命令,如果返回250表示正确可以,连接本服务器,否则则表示服务器需要发送人验证。
发送rcpt to命令,如果返回250表示则Email存在
发送quit命令,退出连接
基于上面这个逻辑,我们封装邮件服务器形成Socket,发送命令,根据返回值来判断邮件地址是否合法:
具体代码如下:
import java.io.*; import java.net.*; import java.util.*; import javax.naming.*; import javax.naming.directory.*; public class SMTPMXLookup { private static int hear( BufferedReader in ) throws IOException { String line = null; int res = 0; while ( (line = in.readLine()) != null ) { String pfx = line.substring( 0, 3 ); try { res = Integer.parseInt( pfx ); } catch (Exception ex) { res = -1; } if ( line.charAt( 3 ) != '-' ) break; } return res; } private static void say( BufferedWriter wr, String text ) throws IOException { wr.write( text + "\r\n" ); wr.flush(); return; } private static ArrayList getMX( String hostName ) throws NamingException { // Perform a DNS lookup for MX records in the domain Hashtable env = new Hashtable(); env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); DirContext ictx = new InitialDirContext( env ); Attributes attrs = ictx.getAttributes ( hostName, new String[] { "MX" }); Attribute attr = attrs.get( "MX" ); // if we don't have an MX record, try the machine itself if (( attr == null ) || ( attr.size() == 0 )) { attrs = ictx.getAttributes( hostName, new String[] { "A" }); attr = attrs.get( "A" ); if( attr == null ) throw new NamingException ( "No match for name '" + hostName + "'" ); } // Huzzah! we have machines to try. Return them as an array list // NOTE: We SHOULD take the preference into account to be absolutely // correct. This is left as an exercise for anyone who cares. ArrayList res = new ArrayList(); NamingEnumeration en = attr.getAll(); while ( en.hasMore() ) { String mailhost; String x = (String) en.next(); String f[] = x.split( " " ); // THE fix ************* if (f.length == 1) mailhost = f[0]; else if ( f[1].endsWith( "." ) ) mailhost = f[1].substring( 0, (f[1].length() - 1)); else mailhost = f[1]; // THE fix ************* res.add( mailhost ); } return res; } public static boolean isAddressValid( String address ) { // Find the separator for the domain name int pos = address.indexOf( '@' ); // If the address does not contain an '@', it's not valid if ( pos == -1 ) return false; // Isolate the domain/machine name and get a list of mail exchangers String domain = address.substring( ++pos ); ArrayList mxList = null; try { mxList = getMX( domain ); } catch (NamingException ex) { return false; } // Just because we can send mail to the domain, doesn't mean that the // address is valid, but if we can't, it's a sure sign that it isn't if ( mxList.size() == 0 ) return false; // Now, do the SMTP validation, try each mail exchanger until we get // a positive acceptance. It *MAY* be possible for one MX to allow // a message [store and forwarder for example] and another [like // the actual mail server] to reject it. This is why we REALLY ought // to take the preference into account. for ( int mx = 0 ; mx < mxList.size() ; mx++ ) { boolean valid = false; try { int res; // Socket skt = new Socket( (String) mxList.get( mx ), 25 ); BufferedReader rdr = new BufferedReader ( new InputStreamReader( skt.getInputStream() ) ); BufferedWriter wtr = new BufferedWriter ( new OutputStreamWriter( skt.getOutputStream() ) ); res = hear( rdr ); if ( res != 220 ) throw new Exception( "Invalid header" ); say( wtr, "EHLO rgagnon.com" ); res = hear( rdr ); if ( res != 250 ) throw new Exception( "Not ESMTP" ); // validate the sender address say( wtr, "MAIL FROM:" ); res = hear( rdr ); if ( res != 250 ) throw new Exception( "Sender rejected" ); say( wtr, "RCPT TO: <" + address + ">" ); res = hear( rdr ); // be polite say( wtr, "RSET" ); hear( rdr ); say( wtr, "QUIT" ); hear( rdr ); if ( res != 250 ) throw new Exception( "Address is not valid!" ); valid = true; rdr.close(); wtr.close(); skt.close(); } catch (Exception ex) { // Do nothing but try next host ex.printStackTrace(); } finally { if ( valid ) return true; } } return false; } public static void main( String args[] ) { String testData[] = { "[email protected]", "[email protected]", "[email protected]", // Invalid domain name "[email protected]", // Invalid address "[email protected]" // Failure of this method }; for ( int ctr = 0 ; ctr < testData.length ; ctr++ ) { System.out.println( testData[ ctr ] + " is valid? " + isAddressValid( testData[ ctr ] ) ); } return; } }
以上是判断邮件地址是否合法的逻辑,如果邮件地址不合法,则将邮件地址从收件人列表中剔除。
private static String[] removeInvalidateAddress(String[] addresses, String mailFrom) { ArrayListvalidateAddresses = new ArrayList (); String normalAddress = null; int code; SMTPTransport smptTrans = null; if(StringUtils.isEmpty(mailFrom) || null == addresses) { return new String[0]; } String sendCmd = "MAIL FROM:" + normalizeAddress(mailFrom); try { smptTrans = (SMTPTransport)sendSession.getTransport("smtp"); smptTrans.connect(); code = smptTrans.simpleCommand(sendCmd); if(code != 250 && code != 251) { logger.error("send from invalidate" + mailFrom); } else { for(String address : addresses) { normalAddress = normalizeAddress(address); String cmd = "RCPT TO:" + normalAddress; code = smptTrans.simpleCommand(cmd); if(code == 250 || code == 251) { validateAddresses.add(address); } } } } catch(MessagingException e) { logger.error("Validate mail address error. send from " + mailFrom, e); } String[] result = validateAddresses.toArray(new String[validateAddresses.size()]); return result; } private static String normalizeAddress(String addr) { if ((!addr.startsWith("<")) && (!addr.endsWith(">"))) return "<" + addr + ">"; else return addr; }
相关文章供参考:
Java Scoket实现发送Mail
java处理邮箱地址是否真实存在的实例
Java与邮件系统交互之使用Socket验证邮箱是否存在