|
JAVA 平台的MAIL实战精华 本人承诺:以下内容为100%原创,绝对没有参考或引用<<any book>>的PAGE any中的nay line的any char
JAVA平台事实上已经为我们提供了MAIL实现(JAVAMAIL API),但是,JAVAMAIL的实现实在不值一提 无论其易用性还是性能,都差强人意.SUN的开发小组成员他们只能是JAVA精英,但他们不是MAIL的行家,对我 而言,JAVAMAIL最多只能算是一个标准,一种接口,而SUN根本不应该自己去实现.
一种技术,在任何平台上实现都是同样的技术,语言本身只是一种工具,而不应该让技术服从于语言. 但JAVAMAIL就把MAIL技术服从JAVA语言的层次,所以它已经不具有MAIL自己本来的性能优势. 那么,本文就把MAIL技术还它本来面目,它不是JAVA的API,而是MAIL技术在JAVA平台上的实现.
当然,本文不会教你如何从最底层来实现MAIL技术的各种协议,也不会自己实现SMTP和POP,IMAP等 服务程序----和JAVAMAIL在同一起跑线上,基于已有的服务程序来应用.纵观整个JAVA网络编程,90%是对应 用层编程,很少要我们自己用JAVA写服务的,那不是一两个人做的事.
好了,言归正传. 一.MTA部分的实现: MTA部份,说到底,我们不必关心一个MAIL实体是如何路由的,然后如何最终转发到目标服务器上,其间 要遵循哪些协议等问题,我们只关心,如何把一封信发出去?
把一封信发出去,传统的做法是把个MAIL实体提交到一个SMTP的发送队列中,我们在JAVA平台上要做 的事也就是实现如何和SMTP服务打交道.当然如果你没有SMTP服务,也可以直接把一个MAIL实体直接发送到目标 地址的SMTP上,而且后一种更有效率.
我们先来看一下如何把一个MAIL实体提交给本地的SMTP服务器: 1.连结SMTP的25端口 2.可选的认证 3.提交信件来源 4.提交目的地址 5.提交MAIL实体 6.断开连结
在和一个SMTP服务听一次会话中,每个命令参数的规范请自己参看RFC822.命令参数没有太多的技术可 言.你只要在DOS命令行(或Bash Shell)上起一个telnet服务试一下就明白了所有过程: 不要认证的过程: tlent mailhost 25 < 220 xxx.xxx SMTP server ............ > HELO < 250 xxx.xxx sourcehost(ip) okay > MAIL FROM: <aaa@aaa.com> < 250 <aaa@aaa.com>,sender ok > RCPT TO: <bbb@bbb.com> < 250 ok > DATA < 354 go ahead > sommessage > . < 250 ok > QUIT < 221 xxx.xxx < Connection closed by host. 如果要求认证,只是发送的命令参数不同,把用户名和密码提交过去而已.这样我们只要建立一个socket, 就直接发送和服务器打交道的命令行,再也不要建立什么JAVAMAIL的会话对象,认证对象等一系列复杂的对象.
下面的代码,我按整个实现过程顺序解释,为了照顾代码的完全性,把说明的内容和整个代码放在一起,从 ---------begin-------开始到--------end--------结束中是一个完整的JAVA源程序中加上说明的
---------------------------------begin-------------------------------------- import java.net.*; import java.io.*; import java.util.*; public class SendMail { private Socket sc; //一个发送会话的SOCKET连结 private int PORT = 25; //SMTP端口 private BufferedReader in; //SOCKET的输入流,用于接收命令响应 private PrintWriter out; //SOCKET的输出流,用于发送命令 private String smtpServer; //SMTP主机 private boolean htmlStyle = false; //是否用HTML格式发送 private boolean authentication = false; //服务器是否要求认证 private String authorName = "guest"; //用于认证的默认用户名 private String authorPasswd = "guest"; //用于认证的默认口令 private String[] toArr; //同时发送的目标地址数组 private String[] ccArr; //同时抄送的目标地址数组 private String[] bccArr; //同时暗送的目标地址数组 private String from; //发信人的地址 private String charset = "gb2312"; //默认的字符编码 private int priority = 3; //优先级
以下对上面的属性提供存取方法 public void setSmtpServer(String smtpServer) { this.smtpServer = smtpServer; } public void setHtmlStyle(boolean htmlStyle) { this.htmlStyle = htmlStyle; } public void setAuthentication(boolean authentication) { this.authentication = authentication; } public void setAuthorName(String authorName) { this.authorName = authorName; } public void setAuthorPasswd(String authorPasswd) { this.authorPasswd = authorPasswd; } public void setToArr(String[] toArr) { this.toArr = toArr; } public void setCcArr(String[] ccArr) { this.ccArr = ccArr; } public void setBccArr(String[] bccArr) { this.bccArr = bccArr; } public void setCharset(String charset) { this.charset = charset; } public void setFrom(String from) { this.from = from; } public void setPriority(int priority) { this.priority = priority; }
开始建立SOCKET ,同时初始化输入输出,如果是应用程序本方法的功能应用在构造方法中完成 public boolean createConnect() { if (smtpServer == null) { smtpServer = "localhost"; } try { sc = new Socket(smtpServer,PORT); in = new BufferedReader(new InputStreamReader(sc.getInputStream())); out = new PrintWriter(sc.getOutputStream()); } catch (IOException e) { return false; } return true; }
为了方便调试,在一次会话中一个命令发送应该有一个响应,所以把一个命令发送和响应过程封装到一个 方法中 public String do_command(String s) throws IOException { if (s != null) { out.print(s); out.flush(); } String line; if ((line = in.readLine()) != null) { return line; } else { return ""; } }
在发送MAIL实体前,认证和非认证的服务器发送命令不同,所以把发送实体前的会话封装到本方法中 注意本方法返回boolean类型是调试成功后封装的,为了在send方法中调用方便,但在具体调试时,本方法 应用返回String类型,也就是每次把do_command("AUTH LOGIN\r\n").indexOf("334")赋给line并把line 返回出来以便能在错误时知道返回的错误码
public boolean sendHeader() { try { String line; do_command(null); if(authentication) { 如果是服务器要求认证,可能是有两种加密方法,一是MD5,一是BASE64,目前很少用MD5认证的,所以本方法 中用BASE64对明码用户名和口令编码, MailEncode.Base64Encode是MailEncode的静态方法,在以下的介绍 中会提供相应的编码和加密方法源程序
authorName = MailEncode.Base64Encode(authorName); authorPasswd = MailEncode.Base64Encode(authorPasswd); if (-1 == do_command("EHLO "+ smtpServer+"\r\n").indexOf("250")) return false; while(true) { if(-1 != in.readLine().indexOf("250 ")) break; } if (-1 == do_command("AUTH LOGIN\r\n").indexOf("334")) return false; if (-1 == do_command(authorName+"\r\n").indexOf("334")) return false; if (-1 == do_command(authorPasswd+"\r\n").indexOf("235")) return false; } else { if (-1 == do_command("HELO "+ smtpServer+"\r\n").indexOf("250")) return false; } if (-1 == (line = do_command("MAIL FROM: "+ from+"\r\n")).indexOf("250")) return false; 对于目标地址,发送,抄送和暗送,在发送过程中没有任何区别.区别只是在MAIL实体中它们的位置而在 SMTP会话中它们只以相同的RCPT TO命令发送,注意,有些服务器不允许一次连结发送给太多的地址.那么 你应该限制toArr,ccArr,bccArr三个数组的总长度不超它们设定的最大值.当然如果你只有一个发送地址 你就不必要在FOR回圈中处理,但本方法为了兼容同时发送给多人(而不是写在抄送中),用FOR回圈中来处理 假你是一个目标地址,你应该生成一个元素的数组String[] toArr = ;或者你可以重载本 方法让to只是一个字符串
if(toArr != null) { for(int i=0;i<toArr.length;i++) { if (-1 == (line = do_command("RCPT TO: "+ toArr[i]+"\r\n")).indexOf("250")) return false; } } else return false; 其实,从程序本身来说如果没有toArr只要有ccArr或bccArr还是可以发送的,但这样的信件没有目标地址却有抄送(暗送 看不到)不合逻辑,在MAIL协议中一个重要原则是宽进严出,也就是我们接收别人的信格式可以放宽,他们发给我的只要符合 协议我就应该接收和解析,而我发送出去的一定要非常严格地遵循标准,所以本处如果没有写发送就直接返回 if(ccArr != null) { for(int i=0;i<ccArr.length;i++) { if (-1 == (line = do_command("RCPT TO: "+ ccArr[i]+"\r\n")).indexOf("250")) return false; } } if(bccArr != null) { for(int i=0;i<bccArr.length;i++) { if (-1 == (line = do_command("RCPT TO: "+ bccArr[i]+"\r\n")).indexOf("250")) return false; } } if (-1 == (line = do_command("DATA\r\n")).indexOf("354")) return false; } catch (IOException e) { return false; } return true; }
在发送MAIL实体时,为了处理方便和性能的原因,我把有附件和没有附件的方法分开来 BASE64是目前任何MUA都能处理的编码,本着宽进严出的原则我们严格使用BASE64编码
public boolean send(String subject,String message) { subject = MailEncode.Base64Encode(subject); subject = "=?GB2312?B?"+subject + "?="; message = MailEncode.Base64Encode(message); try { String line; if(!sendHeader()) return false; message = "MIME-Version: 1.0\r\n\r\n"+message; message = "Content-Transfer-Encoding: base64\r\n"+message; if(htmlStyle) message = "Content-Type: text/html;charset=\""+charset+"\"\r\n"+message; else message = "Content-Type: text/plain;charset=\""+charset+"\"\r\n"+message;
message = "Subject: "+subject+"\r\n"+message;
这儿是发送和抄送的列表,它只是在信体中的标记不同,暗送不必写,在和SMTP会话中直接RCPT过去 String target = ""; String ctarget = ""; for(int i=0;i< toArr.length;i++) { target += toArr[i]; if(i < toArr.length-1) target += ";"; } if(ccArr != null) { for(int i=0;i<ccArr.length;i++) { ctarget += ccArr[i]; if(i < ccArr.length-1) ctarget += ";"; } } //不能把bccArr加入 message = "To: "+target+"\r\n"+message; if(ctarget.length() !=0) message = "Cc: "+ctarget+"\r\n"+message; message = "From: "+from+"\r\n"+message; out.print(message+"\r\n"); if (-1 == (line=do_command("\r\n.\r\n")).indexOf("250")) return false; in.close(); out.close(); sc.close(); } catch (IOException e) { return false; } return true; }
下面是对有附件的发送,因为信体中的文本和附件本要经过不同的处理,它们中间要加入各种分隔符和MIME类型,所以 按顺序把每一行先放入ArrayList中,最后一次取出来发送,其中把附件编码成字符串分行的方法会在以下介绍上给出
public boolean send(String subject,String message,String[] att) {
subject = MailEncode.Base64Encode(subject); subject = "=?GB2312?B?"+subject + "?="; message = MailEncode.Base64Encode(message); String target=""; String ctarget = ""; for(int i=0;i< toArr.length;i++) { target += toArr[i]; if(i < toArr.length-1) target += ";"; } if(ccArr != null) { for(int i=0;i<ccArr.length;i++) { ctarget += ccArr[i]; if(i < ccArr.length-1) ctarget += ";"; } } ArrayList al = new ArrayList(); al.clear(); al.add("Message-Id: "+System.currentTimeMillis()); al.add("Date: "+new java.util.Date()); al.add("X-Priority: "+priority); al.add("From: "+from); al.add("To: "+target); if(ctarget.length() !=0) al.add("Cc: "+ctarget); al.add("Subject: "+subject); al.add("MIME-Version: 1.0"); String s = "------=_NextPart_"+System.currentTimeMillis(); al.add("Content-Type: multipart/mixed;boundary=\""+s+"\""); al.add("X-Mailer: Axman SendMail bate 1.0"); al.add(""); al.add("This is a MIME Encoded Message"); al.add(""); al.add("--"+s); if(htmlStyle) al.add("Content-Type: text/html; charset=\""+charset+"\""); else al.add("Content-Type: text/plain; charset=\""+charset+"\""); al.add("Content-Transfer-Encoding: base64"); al.add(""); al.add(message); al.add(""); if(att != null) { for(int i=0;i<att.length;i++) { int kk = att[i].lastIndexOf("/"); if(-i == kk) kk = att[i].lastIndexOf("\"); if(-1 == kk) kk = att[i].lastIndexOf("_"); String name = att[i].substring(kk+1); al.add("--"+s); al.add("Content-Type: application/octet-stream; name=\""+name+"\""); al.add("Content-Transfer-Encoding: base64"); al.add("Content-Disposition: attachment; filename=\""+name+"\""); al.add(""); MailEncode.Base64EncodeFile(att[i],al); al.add(""); } } al.add("--"+s+"--"); al.add(""); try { String line; if(!sendHeader()) return false; for(int i =0;i< al.size();i++) out.print(al.get(i)+"\r\n"); if (-1 == do_command("\r\n.\r\n").indexOf("250")) return false; in.close(); out.close(); sc.close(); } catch (IOException e) { return false; } return true; }
这个SAVE方法只是把要发的信件保存到本地文件中,其实应该重载一个不带附件的方法和send方法想对应, 大家可以自己加入 public void save(String subject,String message,String[] att,String path) {
subject = MailEncode.Base64Encode(subject); subject = "=?GB2312?B?"+subject + "?="; message = MailEncode.Base64Encode(message); String target=""; String ctarget = ""; for(int i=0;i< toArr.length;i++) { target += toArr[i]; if(i < toArr.length-1) target += ";"; } if(ccArr != null) { for(int i=0;i<ccArr.length;i++) { ctarget += ccArr[i]; if(i < ccArr.length-1) ctarget += ";"; } } ArrayList al = new ArrayList(); al.clear(); al.add("Message-Id: "+System.currentTimeMillis()); al.add("Date: "+new java.util.Date()); al.add("X-Priority: "+priority); al.add("From: "+from); al.add("To: "+target); if(ctarget.length() !=0) al.add("Cc: "+ctarget); al.add("Subject: "+subject); al.add("MIME-Version: 1.0"); String s = "------=_NextPart_"+System.currentTimeMillis(); al.add("Content-Type: multipart/mixed;boundary=\""+s+"\""); al.add("X-Mailer: Axman SendMail bate 1.0"); al.add(""); al.add("This is a MIME Encoded Message"); al.add(""); al.add("--"+s); if(htmlStyle) al.add("Content-Type: text/html; charset=\""+charset+"\""); else al.add("Content-Type: text/plain; charset=\""+charset+"\""); al.add("Content-Transfer-Encoding: base64"); al.add(""); al.add(message); al.add(""); if(att != null) { for(int i=0;i<att.length;i++) { int kk = att[i].lastIndexOf("/"); if(-i == kk) kk = att[i].lastIndexOf("\"); if(-1 == kk) kk = att[i].lastIndexOf("_"); String name = att[i].substring(kk+1); al.add("--"+s); al.add("Content-Type: application/octet-stream; name=\""+name+"\""); al.add("Content-Transfer-Encoding: base64"); al.add("Content-Disposition: attachment; filename=\""+name+"\""); al.add(""); MailEncode.Base64EncodeFile(att[i],al); al.add(""); } } al.add("--"+s+"--"); al.add(""); try { PrintWriter pw = new PrintWriter(new FileWriter(path,true),true); for(int i=0;i<al.size();i++) pw.println((String)al.get(i)); pw.close(); } catch(IOException e){} } public static void main(String[] args) { SendMail sm = new SendMail(); sm.setSmtpServer("10.0.0.1"); if(sm.createConnect()) { String[] to = ; String[] cc = ; String[] bcc = ; sm.setToArr(to); sm.setCcArr(cc); sm.setBccArr(bcc); sm.setFrom("axman@staff.coremsg.com"); //sm.setAuthentication(true); //sm.setAuthorName("axman"); //sm.setAuthorPasswd("11111"); sm.setHtmlStyle(true); String subject = "中文测试!"; String message = "大家好啊!"; //String[] att = ; System.out.print(sm.send(subject,message,null)); } else { System.out.println("怎么连不上SMTP服务器啊?\r\n"); return; } } }
------------------------------------------- end -----------------------------------------
如果你自己有BASE64编码方法可以先替换我的程序中的方法,然后把发附件的SEND方法注释(里面没有把文件编码的方法) 你可以先用本代码发一封文本的MAIL看看,我现在来不急写那个方法的说明,所以不好直接把光秃秃的代码贴上来.
好了,今晚先写到这儿,代码中详细的解释周末再写.先把本代码读懂吧,不要急.下次会接着再介绍的 --------------------------------------------------------------------------------------- 转自:cnjsp.com axman 原创
|