再谈用 Java 实现 SMTP 发送邮件之 Socket 编程

    前几天利用Socket实现了用java语言搭建web服务器,全程下来应该会对Socket这个东西已经使用的非常熟悉了,虽然抽象,但是使用过一次之后就会感受到它在网络通信上的作用是多么的强大!正好,今天就继续用Socket来练习使用以下Smtp协议发送一封简单的电子邮件。今天的故事呢,是我要约我女神出去吃饭啦啦啦~~~所以,面对Smtp,只许成功,不许失败!

    全局假定我的邮箱为cnsmtp01@163.com 女神的邮箱为cnsmtp02@163.com 密码都是 computer (呦,还是个情侣邮箱~)

    为了更加体现程序员的高大上,所以我选择使用命令行的方式(SB程序员。。。)。打开cmd,在命令行里输入telnet,“嗯,居然给我显示telnet不是内部或外部命令”!完蛋,怎么办,这才第一步,就出师不利啊,看来今天要跪,赶快想解决办法。进入 控制面板—程序和功能—启动或关闭windows功能—telnet客户端,勾选上然后确定即可(有些人的电脑还会看到“telnet服务端”,注意别选错了,服务端是指你的电脑作为服务器让别人登陆的,而咱们现在要做的是自己的电脑作为客户端登陆邮箱服务器)

image

    再在cmd中试一下,直接输入 telnet smtp.163.com 25 ,第一行会显示邮件服务器返回的欢迎信息,我这里返回的就是 “220 163.com Anti-spam GT for Coremail System (163com[20121016])” 其中220说明邮件服务器跟我已经建立了连接,它已经开始想要帮帮我了,哈哈。友好一点,跟服务器大哥打个招呼吧,输入HELO huan ,(huan是随便输入的,输入什么都行,服务器是不会鸟你到底输入的是什么),这时服务器返回的是“250 OK”,说明现在服务器等着我继续发送命令了。输入 MAIL FROM:<cnsmtp01@163.com> 这时服务器居然给我返回了553,并告诉我需要认证。也是,既然登陆人家的服务器,总得有个人家的账号吧。所以接下来输入AUTH LOGIN,服务器返回了334和我看不懂的东西,这是要求咱们要输入用户名和密码信息了。但是都知道,像用户名密码这种信息是不能在互联网里进行明文传输的,而smtp服务器用户名和密码采用的是base64编码加密方式,所以在百度搜一个在线base64加密网站就好了,(比如http://base64.xpcha.com/),如图

image

    把得到的密文往控制台中粘贴后回车,这时服务器会再要求我输入密码,跟刚才的方式一样即可,如果正确的话,会返回235,并告诉我认证成功了。Perfect!已经进展到一半了,继续!我下面就要告诉服务器从哪个邮箱发,发给谁,所以依次输入MAIL FROM:<cnsmtp01@163.com> 回车 RCPT TO:<cnsmtp02@163.com> 回车 发件人和收件人设置好后,自然该告诉服务器我要发的内容是什么了,所以输入DATA后回车,服务器返回给我354,并让我输入,以<CR><LF>.<CR><LF>结尾,好,那就直接输入正文: Can I date with you? 然后“回车” “.” “回车”来告诉服务器我的内容输入完毕了,它可以发送了。这个时候触目惊心的一幕出现了,服务器并没有给我返回2XX的正确码,而是给我返回了554,这是为什么,服务器大哥不肯帮我了?
image

    看着它给我的链接,像是163自己的错误说明文档,我就复制下来了,打开浏览器查看了一下,原来是163服务器认为我发的是垃圾邮件,所以它拒绝给我发信。可是大哥,我这是真心的啊,哪是什么垃圾邮件啊,求求你就让我发送吧(想想也是,如果好多人都这样给女神发了一堆的垃圾邮件,我会不高兴的%>_<%)。分析一下为什么会被认为是垃圾邮件吧:邮件得有主题(subject),发件人(from),收件人(to),邮件正文等,可是我只写了个邮件正文,也许服务器就把这个当成是垃圾邮件了。嗯,越臆想就觉得越有道理,来试一下吧。这次我输入了DATA后,服务器让我输入内容,我先输入了subject:Would you go on a date with me ? 然后回车 from:cnsmtp01@163.com 回车 to:cnsmtp02@163.com 回车 Can you eat a meal together? 回车 . 回车 哈哈,这个时候服务器给我返回的是Mail OK,我发送成功了!接下来就是要等待女神的答复了。。。全部过程见下图:(留个问题,女神的邮箱里肯定会收到这封邮件,但是会收到我原本想发的正文“Can you eat a meal together? ”吗?如果不知道,查一下报文格式,或见下面的java程序,对比一下正文部分后面的数据格式区别)
image

    全部过程:

telnet smtp.163.com 25
S: 220 163.com Anti-spam GT for Coremail System <163com[20121016]>
C: HELO huan
S: 250 OK
C: AUTH LOGIN
S: 334 dXNlcm5hbWU6
C: Y25zbXRwMDE=
S: 334 UGFzc3dvcmQ6
C: Y29tcHV0ZXI=
S: 235 Authentication successful
C: MAIL FROM:<cnsmtp01@163.com>
S: 250 Mail OK
C: RCPT TO:<cnsmtp02@163.com>
S: 250 Mail OK
C: DATA
S: 354 End data with <CR><LF>.<CR><LF>
C: subject:Would you go on a date with me ?
C: from:cnsmtp01@163.com
C: to:cnsmtp02@163.com
C: Can you eat a meal together?
C: .
S: 250 Mail OK queued as smtp7,C8CowEDpwFYP_ERUQxX8AA--.14S2 1413807190
C: RSET
S: 250 OK
C: QUIT
S: 221 Bye

    时候差不多了,我觉得女神应该会给我回复邮件了,怎么看,打开浏览器进入163邮箱吗?太out、太low了,直接命令行吧! telnet pop.163.com 110 ,刚才已经演示了全部的smtp命令,所以操作起来pop的应该很简单了,直接上图:
image

    看到女神回复我什么了吗。。。简直狗血。。。

    好了,一次的收发邮件过程全都解决了,而且结果是,,,被拒了。其实呢,刚才没说,我的女神是编号的,从女神0号,女神1号…女神n号,难道刚刚第0个女神拒绝我后我就颓了吗?那怎么行,我得越挫越勇啊,继续给剩下的女神发邮件。可是,我这么多女神,我不能给每个女神发邮件都用这种命令行方式吧,那不虚死我,那么问题来了——发邮件技术哪家强? 既然咱们是学计算机的,那就编软件呗,让软件替咱们批量发,简直Perfect!

    所以又回到这次的议题上来了,怎么用java实现smtp发送邮件?对,还是要请出大神Socket来帮忙。经历了上次Socket的折磨和刚才命令行的磨练,接下来就是把他们巧妙融合的时候了,所以,别走开。

    1、定义一些咱们一会会用到的邮箱名,用户名密码等信息(正常编程没人会把用户名和密码写的这么明白。。。):

String sender = "cnsmtp01@163.com"; 
String receiver = "cnsmtp02@163.com"; 
String password = "computer";
//上文说过,这个用户名和密码是要使用base64进行加密的,加密方法见下文附录1详解 
String user = new BASE64Encoder().encode(sender.substring(0, sender.indexOf("@")).getBytes());  //截取出“cnsmtp01”并加密 
String pass = new BASE64Encoder().encode(password.getBytes());   //加密 “computer”

    2、建立Socket连接:

Socket socket = new Socket("smtp.163.com", 25);  //smtp服务使用25号端口监听

    3、获取该socket的输入输出流

InputStream inputStream = socket.getInputStream(); 
OutputStream outputStream = socket.getOutputStream(); 
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 
PrintWriter writter = new PrintWriter(outputStream, true);  //我TM去 这个true太关键了,我刚才没写这个别坑了!你可以不加这个试下效果,下文附录2会写到为什么加true

    4、发送HELO信息

//HELO 
writter.println("HELO huan"); 
System.out.println(reader.readLine());

    5、发送AUTH LOGIN信息

//AUTH LOGIN 
writter.println("auth login"); 
System.out.println(reader.readLine()); 
writter.println(user); 
System.out.println(reader.readLine()); 
writter.println(pass); 
System.out.println(reader.readLine()); 
//Above   Authentication successful

    6、发送发件人和收件人信息

//Set mail from   and   rcpt to 
writter.println("mail from:<" + sender +">"); 
System.out.println(reader.readLine()); 
writter.println("rcpt to:<" + receiver +">"); 
System.out.println(reader.readLine());

    7、告诉服务器我要传数据

//Set data 
writter.println("data"); 
System.out.println(reader.readLine());

    8、发邮件主题,收件人,发件人,正文

writter.println("subject:女神,是我");   
writter.println("from:" + sender);   
writter.println("to:" + receiver);   
writter.println("Content-Type: text/plain;charset=\"gb2312\"");//如果发送正文必须加这个,而且下面要有一个空行   
writter.println();   
writter.println("女神,晚上可以共进晚餐吗?");   
writter.println(".");//告诉服务器我发送的内容完毕了   
writter.println("");   
System.out.println(reader.readLine());  

    9、帮你发了邮件,感谢服务器,和它Say Goodbye吧,都不用请它吃饭,多好

writter.println("rset"); 
System.out.println(reader.readLine()); 
writter.println("quit"); 
System.out.println(reader.readLine());

    所以,加上异常等操作,所有的代码如下:

public class SMTPMain {
public static void main(String[] args) { 
    String sender = "cnsmtp01@163.com"; 
    String receiver = "cnsmtp02@163.com"; 
    String password = "computer"; 
    String user = new BASE64Encoder().encode(sender.substring(0, sender.indexOf("@")).getBytes()); 
    String pass = new BASE64Encoder().encode(password.getBytes());
    try { 
        Socket socket = new Socket("smtp.163.com", 25); 
        InputStream inputStream = socket.getInputStream(); 
        OutputStream outputStream = socket.getOutputStream(); 
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 
        PrintWriter writter = new PrintWriter(outputStream, true);  //我TM去 这个true太关键了! 
        System.out.println(reader.readLine()); 
        //HELO 
        writter.println("HELO huan"); 
        System.out.println(reader.readLine()); 
        //AUTH LOGIN 
        writter.println("auth login"); 
        System.out.println(reader.readLine()); 
        writter.println(user); 
        System.out.println(reader.readLine()); 
        writter.println(pass); 
        System.out.println(reader.readLine()); 
        //Above   Authentication successful             

        //Set mail from   and   rcpt to 
        writter.println("mail from:<" + sender +">"); 
        System.out.println(reader.readLine()); 
        writter.println("rcpt to:<" + receiver +">"); 
        System.out.println(reader.readLine()); 

        //Set data 
        writter.println("data"); 
        System.out.println(reader.readLine()); 
        writter.println("subject:女神,是我"); 
        writter.println("from:" + sender); 
        writter.println("to:" + receiver); 
        writter.println("Content-Type: text/plain;charset=\"gb2312\""); 
        writter.println(); 
        writter.println("女神,晚上可以共进晚餐吗?"); 
        writter.println("."); 
        writter.println(""); 
        System.out.println(reader.readLine()); 

        //Say GoodBye
        writter.println("rset"); 
        System.out.println(reader.readLine()); 
        writter.println("quit"); 
        System.out.println(reader.readLine()); 
        } catch (Exception e) {
             e.printStackTrace(); 
        } 
    } 
}

    这下,我完全不怵蓝翔的挖掘机了,这简直就是我的约会神器,只要把女神x号的邮箱一改,程序一运行,我这邮件就瞬间发出去了,哈哈,简直机智如狗。
现在运行程序,看控制台输出
image

    多次运行程序,女神1号,2号…的邮箱里都收到了如下图的邮件(可以把多个女神的邮箱加到集合[list map vector…]里,然后再一循环,分分钟搞定)
image

    至于什么界面什么的,就仁者见仁智者见智吧,习惯java的就swing,习惯android也可以xml,其实我是觉得android更方便一些,控件更容易用一些。若是用java写界面忘了的话,推荐使用 windows builder , 可以帮你很快绘制出界面来,然后你要做的就是获取控件,写监听器等等。

  • 附录1:
    关于base64加密:
    具体什么是base64加密,这种概念性的东西能在百度百科找到的我就不说了,说一下在java里怎么去用它。
    1、首先下载一个工具jar文件,叫做“sun.misc.BASE64Decoder.jar” (百度搜就能找到,如果找不到可以到我最后提供的本人github上去下载下来使用)
    2、有了jar文件后,需要把jar包导入工程,方法为:右键工程名—Build Path—Configure Build Path—Add External JARS,选择你刚下载的jar包后确定就可以了
    3、之后在java文件里写到 new BASE64Encoder().encode(password.getBytes()); 时,会提示没有导入对应的包,可以按Ctrl + Shift + O(欧)来让IDE自动为我们导入(前提是你的jar包导入没问题)

  • 附录2:
    说说PrintWriter.println()方法(或write()方法,其实就差了一个换行,剩下参数什么的都一样)。PrintWriter writter = new PrintWriter(outputStream, true); 在PrintWritter的构造函数中,可以不加true,也可以加true,区别在于:加了true的话,在下面进行writter.println(“helloworld”);后,“helloworld”就会立即发送出去;相反,不加true的话,必须在writter.println(“helloworld”);后 再调用writter.flush();来清空缓冲区,强制发送出去。我开始就没有加true,而且没调用flush()方法,我以为是服务器SB了,结果。。。。

  • 附录3:
    为什么telnet pop协议时登录服务器输入用户名和密码时会明文,这让我很奇怪,希望有人帮我解答。
  • 附录4:
    有没有注意到在使用smtp协议时,认证的时候需要你输入发送人信息,输入data后又要写一遍发件人的信息?难道服务器傻吗,非要让你输入两遍?可以想一想为什么,然后自己亲自尝试一下,由于咱们平时用的邮件代理都把这两个认为是同一个了,所以掩盖了一个发送邮件时候的小技巧,即可以伪装欺骗,具体的自己试一下,印象才会更深刻。

  • 附录5:
    大小写问题。有没有注意到我在cmd里输入的命令部分都是大写,而在java程序里输出的命令部分都是小写?我是想说,命令部分是不区分大小写的,但是命令后面的参数是严格区分大小写的,自己可以试一下。(例如邮箱用户名和密码本来就是区分大小写的吧)

  • 附录6:
    关于telnet本身的问题。在telnet里的一行输入了错误的数据,我想删除,然后再继续输入正确的,回车。这个时候你会发现明明输入的没有问题,可是服务器返回的却是错误代码,比如 500 Error:bad syntax 原因就在于命令你要一次性输入正确,如果中途输错了,不用退格,已经晚了,直接打个回车,肯定会报错,再出入正确的就好了。如果总是打错,像163服务器就直接给你断开连接,它以为你恶意要攻击它呢。像新浪的邮箱服务器,就很忠诚,在有效连接时间内一直等着你输入正确的命令,这点可以自己试一下。

  • 附录7:
    记得之前在linux中用shell发送邮件时,并没有强制要求我必须用合法的身份登录邮箱服务器才能进行发邮件操作,为什么?因为那个时候我自己的本机相当于是SMTP服务器,我自己当然就不用验证身份了。而现在是想用SMTP协议登录别人的服务器(如163),此时163就必须要合法的用户身份才能使其登录并发邮件了。

    针对此篇文章还要说明的:这只是为了理解SMTP的一篇很基础性的讲解,对于代码部分,为了体现主体,明显缺少通过服务器返回代码判断语句,因而以上程序缺少健壮性,自己在理解好实际编程时应该考虑到真正的网络请求情况,考虑丢包情况,根据服务器返回代码进行相应判断。

    好了,就写到这了,我要去和女神吃饭饭了~哪里有问题可以在下面留言~

###个人github: http://github.com/icodeu

###代码托管地址:http://github.com/icodeu/JavaForSMTP

###CSDN博客:http://blog.csdn.net/icodeyou

###个人微信号:qqwanghuan 只为技术交流

image