ZhangLiHai.Com Blog


JAVA 平台的MAIL实战精华(一)

张利海 于 2003年05月26日 发表

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 原创

新版本Blog中有更多内容
Copyright (C)2002-2007 All Rights Reserved Powered By:ZhangLiHai.Com