使用java语言基于SMTP协议手写邮件客户端
1. 说明

电子邮件是互联网上常见的应用,他是互联网早期的产品,直至今日依然受到广大用户的喜爱(在中国可能因为文化背景不同,电子邮件只在办公的时候常用)。

电子邮件系统由以下几个部分组成:

  • 用户代理
  • 邮件服务器
  • 邮件传输协议

总所周知,目前市面上流行的电子邮箱有qq邮箱,163邮箱等,我们可以去申请一个qq邮箱或者163邮箱,原因是因为腾讯和网易提供了邮件服务器。

同时我们也知道我们不仅仅可以通过qq邮箱的官方客户端收发邮件,而且可以通过其他客户端登入qq邮箱,比如说网易邮箱大师等。这说明邮件服务商提供了邮件服务器,但是用户代理却不局限与该厂商,而用户代理是通过邮件传输协议与邮件服务器进行通信的,也就是说我们只要理解了邮件传输协议,了解一门网络编程语言,就可以动手实现我们自己的邮件客户端了。

那么我们开始实现吧;

2. SMTP协议

SMTP的全称是Simple Mail Transfer Protocol,简单邮件传输协议。顾名思义,这个协议十分的简单,通过对该协议的RFC文档的阅读,我们可以掌握该协议的基本内容,了解从用户代理与邮件服务器的通信规则。

3 准备工作
  1. 阅读SMTP协议的RFC文档
  2. 搭Maven环境
  3. 编写代码
4. SMTP协议精要

RFC文档当然是英文的,本来非常害怕,但是发现他的内容其实很少,所以边看文档边查词典读了两遍(千万别怕),下面是主要内容介绍。

SMTP协议分为标准SMTP协议和扩展SMTP协议,标准SMTP协议是1982年在RFC821 文档中定义的,而扩展SMTP协议是1995年在RFC1869 文档中定义的。扩展SMTP协议在标准 SMTP协议基础上的改动非常小,主要增加了邮件安全方面的认证功能,现在我们说的SMTP协议基本上都是扩展SMTP协议。

  • introduction介绍:
    主要介绍了SMTP扩展协议为消息传输代理提供了一个稳定的高效的基础(也就是说SMTP协议可以用来实现消息传输代理客户端,也急速邮件客户端)。然后说明了扩展的内容。
  • SMTP扩展协议的框架
    SMTP传输的是邮件对象,邮件对象包括封面和内容
  • 封面包括发件人的地址,多个收件人的地址和交付模式,使用一系列的协议单元发送。
  • 内容包括头部和主题两个部分,使用SMTP数据协议单元发送,头部包括一系列键值对,头部总是使用ASCII编码
  • SMTP协议包含得多指令,但是只需要用到以下指令就可以完成简单的邮件发送
  • ehlo <domain>如 ehlo zeng
    与SMTP协议建立连接后需要发送的第一条命令。
  • auth para设置验证方式,如auth login
  • mail from: <发送者邮箱>设置发送者邮箱,如mail from:<xxxx@qq.com>
  • rcpt to:<收件者邮箱>设置收件者邮箱,如rcpt to:<xxxx@qq.com>
  • data表示将要发送邮件的内容,这个命令后面的发送都是邮件内容
  • quit结束邮件发送

所有命令末尾都是回车换行

5. 源码
import com.sun.xml.internal.messaging.saaj.util.Base64;
import lombok.Data;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * @author zeng
 */
public class MyEmailClient {
    public static void main(String[] args) throws IOException {
        //敏感信息。。
        Token token=new Token("",25,"","");
        Socket socket=null;
        PrintWriter printWriter=null;
        BufferedReader br=null;
        
        try {
            //1. 连接smtp邮箱服务器
            socket=new Socket(token.getAddress(),token.getPort());
            printWriter=new PrintWriter(socket.getOutputStream(),true);
            br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
            //2. 第一条命令 ehlo
            printWriter.println("ehlo zeng");
            System.out.println(br.readLine());
            //3. 发送,auth
            printWriter.println("auth login");
            System.out.println(br.readLine());
            //4. 用户名和密码
            printWriter.println(token.getUserName());
            printWriter.println(token.getPassWord());
            //会有一大串信息返回,如果最后返回235 Authentication successful则成功
            String temp=null;
            while ((temp=br.readLine())!=null){
                System.out.println(temp);
                if ("235 Authentication successful".equals(temp)){
                    break;
                }
            }
            System.out.println("认证成功");

            //设置发件人和收件人,敏感信息
            String sentUser="";
            String recUser="";
            printWriter.println("mail from:<"+sentUser+">");
            System.out.println(br.readLine());
            printWriter.println("rcpt to:<"+recUser+">");
            System.out.println(br.readLine());

            //设置data
            printWriter.println("data");
            System.out.println(br.readLine());

            //设置邮件主题
            printWriter.println("subject:test");
            printWriter.println("from:"+sentUser);
            printWriter.println("to:"+recUser);
            //设置邮件格式
            printWriter.println("Content-Type: text/plain;charset=\"utf8\"");
            printWriter.println();
            //邮件正文
            printWriter.println("来自java手写smtp邮件客户端");
            printWriter.println(".");
            printWriter.print("");
            System.out.println(br.readLine());

            //退出
            printWriter.println("rset");
            System.out.println(br.readLine());
            printWriter.println("quit");
            System.out.println(br.readLine());

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //释放连接
            socket.close();
            printWriter.close();
            br.close();
        }
    }
}
@Data
class Token{
    String address;
    Integer port;
    String userName;
    String passWord;

   Token(String address, Integer port, String userName, String passWord) {
        this.address = address;
        this.port = port;
        this.userName = new String(Base64.encode(userName.getBytes()));
        this.passWord = new String(Base64.encode(passWord.getBytes()));
    }
}

import com.sun.xml.internal.messaging.saaj.util.Base64;
import lombok.Data;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * @author zeng
 */
public class MyEmailClient {
    public static void main(String[] args) throws IOException {
        //敏感信息。。
        Token token=new Token("",25,"","");
        Socket socket=null;
        PrintWriter printWriter=null;
        BufferedReader br=null;
        
        try {
            //1. 连接smtp邮箱服务器
            socket=new Socket(token.getAddress(),token.getPort());
            printWriter=new PrintWriter(socket.getOutputStream(),true);
            br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
            //2. 第一条命令 ehlo
            printWriter.println("ehlo zeng");
            System.out.println(br.readLine());
            //3. 发送,auth
            printWriter.println("auth login");
            System.out.println(br.readLine());
            //4. 用户名和密码
            printWriter.println(token.getUserName());
            printWriter.println(token.getPassWord());
            //会有一大串信息返回,如果最后返回235 Authentication successful则成功
            String temp=null;
            while ((temp=br.readLine())!=null){
                System.out.println(temp);
                if ("235 Authentication successful".equals(temp)){
                    break;
                }
            }
            System.out.println("认证成功");

            //设置发件人和收件人,敏感信息
            String sentUser="";
            String recUser="";
            printWriter.println("mail from:<"+sentUser+">");
            System.out.println(br.readLine());
            printWriter.println("rcpt to:<"+recUser+">");
            System.out.println(br.readLine());

            //设置data
            printWriter.println("data");
            System.out.println(br.readLine());

            //设置邮件主题
            printWriter.println("subject:test");
            printWriter.println("from:"+sentUser);
            printWriter.println("to:"+recUser);
            //设置邮件格式
            printWriter.println("Content-Type: text/plain;charset=\"utf8\"");
            printWriter.println();
            //邮件正文
            printWriter.println("来自java手写smtp邮件客户端");
            printWriter.println(".");
            printWriter.print("");
            System.out.println(br.readLine());

            //退出
            printWriter.println("rset");
            System.out.println(br.readLine());
            printWriter.println("quit");
            System.out.println(br.readLine());

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //释放连接
            socket.close();
            printWriter.close();
            br.close();
        }
    }
}
@Data
class Token{
    String address;
    Integer port;
    String userName;
    String passWord;

   Token(String address, Integer port, String userName, String passWord) {
        this.address = address;
        this.port = port;
        this.userName = new String(Base64.encode(userName.getBytes()));
        this.passWord = new String(Base64.encode(passWord.getBytes()));
    }
}
6.题外话

之所以写这个的原因是自己把计网考完后阅读了《计算机网络 自顶向下方法》这本书,该书应用层协议的课后习题就有一个实现邮件客户端,当时看到这个题目的时候,感觉不可思议,因为之前课堂上学计网的时候都是一些理论的知识,真没想过自己动手写代码。之前在写Spring代码的时候也用过mail相关的类,所以自己也决定通过读RFC文档去实现一个自己的邮件客户端,以帮助我发现更多的乐趣。