java+php+阿里云组件纯手工实现pop、smtp、imap开发邮件服务器(二)
java开发邮件服务器的接收模块 用java建立socket服务端,监听端口25,实现smtp协议。即可完成邮件服务器的接收模块。
这里要注意的是,smtp协议其实可以分为两种。一种是你用手机、pc等客户端发邮件到邮件服务商的服务器的时候用的smtp协议,这一类是需要登录验证的。一种是邮件服务商之间传递邮件的smtp协议,此类协议是不需要登录的。比如你用foxmail上你的qq邮箱发送了一封邮件到163的邮箱。过程是这样的:
邮件从foxmail通过smtp协议发送到qq邮箱的服务器。
qq邮箱的服务器通过smtp协议将邮件投递到163的邮件服务器。
对方通过imap或者pop协议从163邮箱服务器拿到这封邮件。
本文将实现的是不需要登录的smtp协议。下面展示该协议:
c:telnet smtp.126.com 25 /* 以telnet方式连接126邮件服务器 */s:220 126.com anti-spam gt for coremail system (126com[071018]) /* 220为响应数字,其后的为欢迎信息,会应服务器不同而不同*/c:helo smtp.126.com /* helo 后用来填写返回域名(具体含义请参阅rfc821),但该命令并不检查后面的参数*/s:250 okc: mail from: [email protected] /* 发送者邮箱 */s:250 … ./* “…”代表省略了一些可读信息 */c:rcpt to: [email protected]/* 接收者邮箱 */s:250 … ./* “…”代表省略了一些可读信息 */c:data /* 请求发送数据 */s:354 enter mail, end with . on a line by itselfc:enjoy protocol studingc:.s:250 message sentc:quit /* 退出连接 */s:221 bye
所以,我们建立socket服务端,并监听25端口后,只要检测客户端发来的信息,并给出相应的回复,即可完成邮件的接收。具体java实现socket可以百度。下面解释几个重要的语法用法:
mail from:这里后面跟的参数是发送者的邮箱。当你接收到邮件的正文(eml后缀的文件),并解析后,里面还有一个标签标记的是邮件的发件人。这两个参数理论上应该是一样的。当不一样的时候,qq邮箱等会显示“该邮件由***代发”。所以我们接收邮件的时候,也要检测一下这两个邮件地址是否相同,避免有人恶意欺骗。另外,最重要的是spf检查,我们下一篇单独介绍。spf在反垃圾邮件方面的作用很大。比如mail from:[email protected] ,我们就会去查cliyun.com的spf记录,看看里面有没有包含当前连接我们socket服务端的客户端的ip地址,如果不包含,说明该客户端是欺骗我们的。
rcpt to:这里标记的是收件人邮箱。当你完成邮件服务器的时候,你会发现网络上很多恶意的程序在大肆发送垃圾邮件,会给本来不存在的用户邮箱所在的服务器发送垃圾邮件。比如你的域名是cliyun.com,但是你的邮箱用户里,[email protected][email protected]?,应该把该邮件发送者的ip加入敏感列表,利用算法进行过滤。
data:data之后,就是邮件eml正文,知道遇到单独一行的 . 结束。通常我们在解析这份eml文件之前,会把它本地保存一下,方便解析出问题后(比如有文字乱码),可以在调整代码后再次解析。
邮件接收器的重点是eml文件的解析。通常使用javamail模块来解析,网上教程很多,但是很多都是不全的。因为一个完整的eml文件,含有很多信息。比如标题、正文、发件人、收件人、抄送、回复地址、附件等等。下面贴一段比较好的解析类:
import java.io.bufferedinputstream;import java.io.bufferedoutputstream;import java.io.file;import java.io.fileoutputstream;import java.io.inputstream;import java.text.simpledateformat;import java.util.date;import java.util.properties;import javax.mail.bodypart;import javax.mail.flags;import javax.mail.folder;import javax.mail.message;import javax.mail.messagingexception;import javax.mail.multipart;import javax.mail.part;import javax.mail.session;import javax.mail.store;import javax.mail.internet.internetaddress;import javax.mail.internet.mimemessage;import javax.mail.internet.mimeutility; /** * @author yh * */public class showmail { private mimemessage mimemessage = null; private string saveattachpath = ; // 附件下载后的存放目录 private stringbuffer bodytext = new stringbuffer(); // 存放邮件内容的stringbuffer对象 private string dateformat = yy-mm-dd hh:mm; // 默认的日前显示格式 /** * 构造函数,初始化一个mimemessage对象 */ public showmail() { } public showmail(mimemessage mimemessage) { this.mimemessage = mimemessage; system.out.println(创建一个receiveemail对象....); } public void setmimemessage(mimemessage mimemessage) { this.mimemessage = mimemessage; system.out.println(设置一个mimemessage对象...); } /** * * 获得发件人的地址和姓名 */ public string getfrom() throws exception { internetaddress address[] = (internetaddress[]) mimemessage.getfrom(); string from = address[0].getaddress(); if (from == null) { from = ; system.out.println(无法知道发送者.); } string personal = address[0].getpersonal(); if (personal == null) { personal = ; system.out.println(无法知道发送者的姓名.); } string fromaddr = null; if (personal != null || from != null) { fromaddr = personal + ; system.out.println(发送者是: + fromaddr); } else { system.out.println(无法获得发送者信息.); } return fromaddr; } /** * * 获得邮件的收件人,抄送,和密送的地址和姓名,根据所传递的参数的不同 * * to----收件人 cc---抄送人地址 bcc---密送人地址 */ public string getmailaddress(string type) throws exception { string mailaddr = ; string addtype = type.touppercase(); internetaddress[] address = null; if (addtype.equals(to) || addtype.equals(cc) || addtype.equals(bcc)) { if (addtype.equals(to)) { address = (internetaddress[]) mimemessage .getrecipients(message.recipienttype.to); } else if (addtype.equals(cc)) { address = (internetaddress[]) mimemessage .getrecipients(message.recipienttype.cc); } else { address = (internetaddress[]) mimemessage .getrecipients(message.recipienttype.bcc); } if (address != null) { for (int i = 0; i < address.length; i++) { string emailaddr = address[i].getaddress(); if (emailaddr == null) { emailaddr = ; } else { system.out.println(转换之前的emailaddr: + emailaddr); emailaddr = mimeutility.decodetext(emailaddr); system.out.println(转换之后的emailaddr: + emailaddr); } string personal = address[i].getpersonal(); if (personal == null) { personal = ; } else { system.out.println(转换之前的personal: + personal); personal = mimeutility.decodetext(personal); system.out.println(转换之后的personal: + personal); } string compositeto = personal + ; system.out.println(完整的邮件地址: + compositeto); mailaddr += , + compositeto; } mailaddr = mailaddr.substring(1); } } else { throw new exception(错误的电子邮件类型!); } return mailaddr; } /** * * 获得邮件主题 */ public string getsubject() throws messagingexception { string subject = ; try { system.out.println(转换前的subject: + mimemessage.getsubject()); subject = mimeutility.decodetext(mimemessage.getsubject()); system.out.println(转换后的subject: + mimemessage.getsubject()); if (subject == null) { subject = ; } } catch (exception exce) { exce.printstacktrace(); } return subject; } /** * * 获得邮件发送日期 */ public string getsentdate() throws exception { date sentdate = mimemessage.getsentdate(); system.out.println(发送日期 原始类型: + dateformat); simpledateformat format = new simpledateformat(dateformat); string strsentdate = format.format(sentdate); system.out.println(发送日期 可读类型: + strsentdate); return strsentdate; } /** * * 获得邮件正文内容 */ public string getbodytext() { return bodytext.tostring(); } /** * * 解析邮件,把得到的邮件内容保存到一个stringbuffer对象中,解析邮件 * * 主要是根据mimetype类型的不同执行不同的操作,一步一步的解析 */ public void getmailcontent(part part) throws exception { string contenttype = part.getcontenttype(); // 获得邮件的mimetype类型 system.out.println(邮件的mimetype类型: + contenttype); int nameindex = contenttype.indexof(name); boolean conname = false; if (nameindex != -1) { conname = true; } system.out.println(邮件内容的类型: + contenttype); if (part.ismimetype(text/plain) && conname == false) { // text/plain 类型 bodytext.append((string) part.getcontent()); } else if (part.ismimetype(text/html) && conname == false) { // text/html 类型 bodytext.append((string) part.getcontent()); } else if (part.ismimetype(multipart/*)) { // multipart/* multipart multipart = (multipart) part.getcontent(); int counts = multipart.getcount(); for (int i = 0; i < counts; i++) { getmailcontent(multipart.getbodypart(i)); } } else if (part.ismimetype(message/rfc822)) { // message/rfc822 getmailcontent((part) part.getcontent()); } else { } } /** * * 判断此邮件是否需要回执,如果需要回执返回true,否则返回false */ public boolean getreplysign() throws messagingexception { boolean replysign = false; string needreply[] = mimemessage .getheader(disposition-notification-to); if (needreply != null) { replysign = true; } if (replysign) { system.out.println(该邮件需要回复); } else { system.out.println(该邮件不需要回复); } return replysign; } /** * 获得此邮件的message-id */ public string getmessageid() throws messagingexception { string messageid = mimemessage.getmessageid(); system.out.println(邮件id: + messageid); return messageid; } /** * 判断此邮件是否已读,如果未读返回false,反之返回true */ public boolean isnew() throws messagingexception { boolean isnew = false; flags flags = ((message) mimemessage).getflags(); flags.flag[] flag = flags.getsystemflags(); system.out.println(flags的长度: + flag.length); for (int i = 0; i < flag.length; i++) { if (flag[i] == flags.flag.seen) { isnew = true; system.out.println(seen email...); // break; } } return isnew; } /** * 判断此邮件是否包含附件 */ public boolean iscontainattach(part part) throws exception { boolean attachflag = false; // string contenttype = part.getcontenttype(); if (part.ismimetype(multipart/*)) { multipart mp = (multipart) part.getcontent(); for (int i = 0; i < mp.getcount(); i++) { bodypart mpart = mp.getbodypart(i); string disposition = mpart.getdisposition(); if ((disposition != null) && ((disposition.equals(part.attachment)) || (disposition .equals(part.inline)))) attachflag = true; else if (mpart.ismimetype(multipart/*)) { attachflag = iscontainattach((part) mpart); } else { string contype = mpart.getcontenttype(); if (contype.tolowercase().indexof(application) != -1) attachflag = true; if (contype.tolowercase().indexof(name) != -1) attachflag = true; } } } else if (part.ismimetype(message/rfc822)) { attachflag = iscontainattach((part) part.getcontent()); } return attachflag; } /** * * 保存附件 */ public void saveattachment(part part) throws exception { string filename = ; if (part.ismimetype(multipart/*)) { multipart mp = (multipart) part.getcontent(); for (int i = 0; i < mp.getcount(); i++) { bodypart mpart = mp.getbodypart(i); string disposition = mpart.getdisposition(); if ((disposition != null) && ((disposition.equals(part.attachment)) || (disposition .equals(part.inline)))) { filename = mpart.getfilename(); if (filename.tolowercase().indexof(gb2312) != -1) { filename = mimeutility.decodetext(filename); } savefile(filename, mpart.getinputstream()); } else if (mpart.ismimetype(multipart/*)) { saveattachment(mpart); } else { filename = mpart.getfilename(); if ((filename != null) && (filename.tolowercase().indexof(gb2312) != -1)) { filename = mimeutility.decodetext(filename); savefile(filename, mpart.getinputstream()); } } } } else if (part.ismimetype(message/rfc822)) { saveattachment((part) part.getcontent()); } } /** * 设置附件存放路径 */ public void setattachpath(string attachpath) { this.saveattachpath = attachpath; } /** * * 设置日期显示格式 */ public void setdateformat(string format) throws exception { this.dateformat = format; } /** * * 获得附件存放路径 */ public string getattachpath() { return saveattachpath; } /** * * 真正的保存附件到指定目录里 */ private void savefile(string filename, inputstream in) throws exception { string osname = system.getproperty(os.name); string storedir = getattachpath(); string separator = ; if (osname == null) { osname = ; } if (osname.tolowercase().indexof(win) != -1) { separator = \\; if (storedir == null || storedir.equals()) storedir = c:\\tmp; } else { separator = /; storedir = /tmp; } file storefile = new file(storedir + separator + filename); system.out.println(附件的保存地址: + storefile.tostring()); // for(int i=0;storefile.exists();i++){ // storefile = new file(storedir+separator+filename+i); // } bufferedoutputstream bos = null; bufferedinputstream bis = null; try { bos = new bufferedoutputstream(new fileoutputstream(storefile)); bis = new bufferedinputstream(in); int c; while ((c = bis.read()) != -1) { bos.write(c); bos.flush(); } } catch (exception exception) { exception.printstacktrace(); throw new exception(文件保存失败!); } finally { bos.close(); bis.close(); } } /** * receiveemail类测试 */ public static void main(string args[]) throws exception { string host = pop.sina.com; string username = ***; string password = ***; properties props = new properties(); session session = session.getdefaultinstance(props, null); store store = session.getstore(pop3); store.connect(host, username, password); folder folder = store.getfolder(inbox); folder.open(folder.read_only); message message[] = folder.getmessages(); system.out.println(邮件数量: + message.length); showmail re = null; for (int i = 0; i < message.length; i++) { re = new showmail((mimemessage) message[i]); system.out.println(邮件 + i + 主题: + re.getsubject()); system.out.println(邮件 + i + 发送时间: + re.getsentdate()); system.out.println(邮件 + i + 是否需要回复: + re.getreplysign()); system.out.println(邮件 + i + 是否已读: + re.isnew()); system.out.println(邮件 + i + 是否包含附件: + re.iscontainattach((part) message[i])); system.out.println(邮件 + i + 发送人地址: + re.getfrom()); system.out .println(邮件 + i + 收信人地址: + re.getmailaddress(to)); system.out.println(邮件 + i + 抄送: + re.getmailaddress(cc)); system.out.println(邮件 + i + 暗抄: + re.getmailaddress(bcc)); re.setdateformat(yy年mm月dd日 hh:mm); system.out.println(邮件 + i + 发送时间: + re.getsentdate()); system.out.println(邮件 + i + 邮件id: + re.getmessageid()); re.getmailcontent((part) message[i]); system.out.println(邮件 + i + 正文内容: \r\n + re.getbodytext()); re.setattachpath(e:\\); re.saveattachment((part) message[i]); } }}
解析完eml文件,将信息存入数据库后,邮件服务器的接收器就完成工作了。实际生产环境中,为了项目的健壮性,接收器会有很多个,比如颗粒云邮箱的邮件接收器有15台,一封邮件发过来,怎么知道该投递到哪台服务器呢?这是取决于域名的mx记录的。比如qq.com的mx记录里有很多台邮件接收服务器,他们每个的优先级不同。一般都是根据优先级由高到低投递邮件。当一台投递失败后,就切换到另一台。如何通过命令行来查看一个域名的mx记录呢?
这部分是邮件服务器的发件器需要做的工作。我们会在后面的博文中,介绍如何用java检测邮件地址的mx记录。
原文地址:java+php+阿里云组件纯手工实现pop、smtp、imap开发邮件服务器(二)
