如何準(zhǔn)確判斷郵件地址是否存在
我總結(jié)了幾種郵件出現(xiàn)重發(fā)、漏發(fā)的解釋:1.網(wǎng)絡(luò);2.防火墻;3.服務(wù)器的自我保護(hù),比如防止大批量發(fā)送時(shí)掛掉或者垃圾郵件,我覺得第三種解釋靠譜一些,對(duì)于遇到的這些問題在下面的文章中給出了補(bǔ)救措施。
公司郵箱目前使用的是Zimbra,該郵件服務(wù)器目前不甚穩(wěn)定,經(jīng)常出現(xiàn)重發(fā)、漏發(fā)問題。經(jīng)測試,每100封郵件僅可成功發(fā)送98封左右,以下是測試數(shù)據(jù):
測試用例1:100封,總用時(shí)約:16min;實(shí)收97封,失敗3次,3次錯(cuò)誤信息均為:javax.mail.MessagingException: Could not connect to SMTP host
測試用例2:100封,總用時(shí)約:16min;實(shí)收100封,失敗2次,錯(cuò)誤同上。加失敗重發(fā)機(jī)制,失敗后等待10s重發(fā),最多重發(fā)3次;
測試用例3:每發(fā)一封,停留10s,總用時(shí)32min;實(shí)收100封,失敗1次,錯(cuò)誤同上;重發(fā)機(jī)制同用例2.
關(guān)于MessagingException的問題,可以參考:
javax.mail.MessagingException: Could not connect to SMTP host
針對(duì)這種問題,我增加了郵件重發(fā),
if(sendHtmlMail_(mail)){
return true;
} else{
int i = 0;
//包含群組郵件,失敗不重發(fā)
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;
}
但這種機(jī)制又產(chǎn)生了新的問題,因郵件服務(wù)器不穩(wěn)定導(dǎo)致在僅發(fā)送一次的情況下也會(huì)向郵件收件人發(fā)送郵件,且同一封郵件的收件人(包括抄送、密送)可能部分收到郵件、部分收不到郵件。
針對(duì)以上的問題,我們將重發(fā)機(jī)制去除,僅針對(duì)不合法郵件(即服務(wù)器上不存在的郵件地址)進(jìn)行剔除,剔除后再進(jìn)行發(fā)送。而對(duì)其他原因?qū)е碌泥]件發(fā)送失敗不做重發(fā)(該問題將通過郵件服務(wù)器運(yùn)維部門向廠商反映)。
下面是判斷郵件是否合法的邏輯:
1.SMTP是工作在兩種情況下:一是電子郵件從客戶機(jī)傳輸?shù)椒?wù)器;二是從某一個(gè)服務(wù)器傳輸?shù)搅硪粋€(gè)服務(wù)器
2.SMTP是個(gè)請(qǐng)求/響應(yīng)協(xié)議,命令和響應(yīng)都是基于ASCII文本,并以CR和LF符結(jié)束。響應(yīng)包括一個(gè)表示返回狀態(tài)的三位數(shù)字代碼
3.SMTP在TCP協(xié)議25號(hào)端口監(jiān)聽連接請(qǐng)求
4.連接和發(fā)送過程
SMTP協(xié)議說復(fù)雜也不復(fù)雜,說簡單如果你懂得Socket。不過現(xiàn)在只是我們利用的就是第一條中說的,從客戶機(jī)傳輸?shù)椒?wù)器,當(dāng)我們向一臺(tái)服務(wù)器發(fā)送郵件時(shí),郵件服務(wù)器會(huì)首先驗(yàn)證郵件發(fā)送地址是否真的存在于本服務(wù)器上。
5 操作的步驟如下:
連接服務(wù)器的25端口(如果沒有郵件服務(wù),連了也是白連)
發(fā)送helo問候
發(fā)送mail from命令,如果返回250表示正確可以,連接本服務(wù)器,否則則表示服務(wù)器需要發(fā)送人驗(yàn)證。
發(fā)送rcpt to命令,如果返回250表示則Email存在
發(fā)送quit命令,退出連接
基于上面這個(gè)邏輯,我們封裝郵件服務(wù)器形成Socket,發(fā)送命令,根據(jù)返回值來判斷郵件地址是否合法:
具體代碼如下:
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: <tim@orbaker.com>" );
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[] = {
"real@rgagnon.com",
"you@acquisto.net",
"fail.me@nowhere.spam", // Invalid domain name
"arkham@bigmeanogre.net", // Invalid address
"nosuchaddress@yahoo.com" // 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)
{
ArrayList<String> validateAddresses = new ArrayList<String>();
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;
}
以上是本文的全部內(nèi)容,希望大家能夠理解,對(duì)大家有所幫助。
相關(guān)文章
spring?boot?使用Mybatis-plus查詢方法解析
這篇文章主要介紹了spring?boot?使用Mybatis-plus查詢方法解析,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
Java服務(wù)剛啟動(dòng)時(shí)接口超時(shí)排查全過程
這篇文章主要為大家介紹了Java服務(wù)剛啟動(dòng)時(shí),一小波接口超時(shí)排查全過程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
SpringCloud?Feign使用ApacheHttpClient代替默認(rèn)client方式
這篇文章主要介紹了SpringCloud?Feign使用ApacheHttpClient代替默認(rèn)client方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
在安卓系統(tǒng)中插入表情到光標(biāo)位置的代碼詳解
這篇文章主要介紹了在安卓系統(tǒng)中插入表情到光標(biāo)位置的代碼詳解,利用Java代碼在EditText控件中實(shí)現(xiàn),需要的朋友可以參考下2015-07-07
一文徹底弄懂零拷貝原理以及java實(shí)現(xiàn)
零拷貝(英語: Zero-copy) 技術(shù)是指計(jì)算機(jī)執(zhí)行操作時(shí),CPU不需要先將數(shù)據(jù)從某處內(nèi)存復(fù)制到另一個(gè)特定區(qū)域,下面這篇文章主要給大家介紹了關(guān)于零拷貝原理以及java實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2021-08-08

