使用STMP协议发送邮件

前言

最近拿到一个需求,需要给一个基于jdk1.6开发的系统增加登录验证码,这个过程中需要调用第三方接口,如果第三方接口调用失败,则需发送告警邮件到指定邮箱。比较坑的是,因为系统比较老,加了 jar 包之后一直不生效,所以不能引入其他依赖,比如 mail-api 之类的。

基本步骤

只基于一些工具包做过邮件发送,趁此机会了解了一下 SMTP 协议。
使用 SMTP 协议发送邮件分为几个步骤:

  • 与 SMTP 服务器建立连接
  • 身份认证(用户名和密码需通过 base64 进行编码,一般不是邮箱密码,而是一个一次性密码)
  • 指定收件人(没有抄送人选项,抄送通过多次指定收件人实现)
  • 邮件内容
  • 退出

基于命令行

# 使用 telnet 连接到 smtp 服务器
> telnet smtp.163.com 25

Trying 240e:938:a07:6:0:14:203:45...
Connected to smtp163.mail.ntes53.netease.com.
Escape character is '^]'.
220 163.com Anti-spam GT for Coremail System (163com[20141201])
# HELO 命令
> HELO stmp.163.com

250 OK
# 认证 (dXNlcm5hbWU6 即为 username:、UGFzc3dvcmQ6 即为 password:)
> AUTH LOGIN

334 dXNlcm5hbWU6

# 输入 base64 编码后的用户名
> emh1bG9uZ2t1bjIwQDE2My5jb

334 UGFzc3dvcmQ6

# 输入 base64 编码的密码
> WU54UktjYVJLZFhLWmV

235 Authentication successful

到这里已经成功登录到服务器了。

# 发件人
> MAIL FROM:<[email protected]>

250 Mail OK

# 收件人
> RCPT TO:<[email protected]>

250 Mail OK

# 抄送人
> RCPT TO:<[email protected]>

250 Mail OK

邮件内容命令为 DATA,然后以 . 作为结束。

> DATA

354 End data with <CR><LF>.<CR><LF>

> Subject: Greet Email
> This is a greet email from 163!
> .

250 Mail OK queued as gzsmtp2,PSgvCgBnBIxyor+sPBA--.4614S2 1751466502 # 已经进入发送队列

# 使用 QUIT 命令退出
> QUIT

221 Bye
Connection closed by foreign host.

以上就是使用命令行发送邮件的全过程,其中要注意,短时间内发送多封邮件,可能会被反垃圾邮件程序拦截掉,邮件标题、内容最好不要太随意。

纯 Java 实现

基于 socket 和 IO 流实现邮件发送,代码如下:

import java.io.*;  
import java.net.InetSocketAddress;  
import java.net.Socket;  
import java.nio.charset.StandardCharsets;  
import java.util.Base64;  
  
public class MyMail {  
    public static void sendMail(String host, int port, String username, String password,  
                                String receiver, String ccReceiver, String subject, String body) throws IOException {  
        Socket socket = new Socket();  
        InetSocketAddress inetSocketAddress = new InetSocketAddress(host, port);  
        socket.connect(inetSocketAddress);  
        OutputStream outputStream = socket.getOutputStream();  
        InputStream inputStream = socket.getInputStream();  
        readInputStream(inputStream);  
  
        // 发送 HELO 命令  
        sendCommand(outputStream, "HELO " + host);  
        readInputStream(inputStream);  
  
        // 发送 AUTH LOGIN 命令  
        sendCommand(outputStream, "AUTH LOGIN");  
        readInputStream(inputStream);  
  
        sendCommand(outputStream, base64Encode(username));  
        readInputStream(inputStream);  
  
        sendCommand(outputStream, base64Encode(password));  
        readInputStream(inputStream);  
  
        sendCommand(outputStream, "MAIL FROM:<" + username + ">");  
        readInputStream(inputStream);  
  
        sendCommand(outputStream, "RCPT TO:<" + receiver + ">");  
        readInputStream(inputStream);  
  
        sendCommand(outputStream, "RCPT TO:<" + ccReceiver + ">");  
        readInputStream(inputStream);  
  
        sendCommand(outputStream, "DATA");  
        readInputStream(inputStream);  
  
        sendCommand(outputStream, "Subject: " + subject);  
        sendCommand(outputStream, body);  
        sendCommand(outputStream, ".");  
        readInputStream(inputStream);  
  
        sendCommand(outputStream, "QUIT");  
        readInputStream(inputStream);  
    }  
  
    public static String base64Encode(String text) {  
        return Base64.getEncoder().encodeToString(text.getBytes());  
    }  
  
    private static void readInputStream(InputStream inputStream) throws IOException {  
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));  
        StringBuilder response = new StringBuilder();  
        String line;  
        while ((line = bufferedReader.readLine()) != null) {  
            response.append(line).append("\n");  
            if (line.length() >= 3 && line.charAt(3) == ' ') {  
                break;  
            }  
        }  
        System.out.println(response.toString().trim());  
    }  
  
    private static void sendCommand(OutputStream outputStream, String command) throws IOException {  
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));  
        bufferedWriter.write(command + "\r\n");  
        bufferedWriter.flush();  
    }  
  
    public static void main(String[] args) {  
        try {  
            sendMail("smtp.163.com", 25,  
                    "[email protected]", "YNxRKcadfsdXsdfa",  
                    "[email protected]", "[email protected]",  
                    "Greet day", "Today is a great day!");  
        } catch (Exception e) {  
            e.printStackTrace();  
            System.out.println("邮件发送失败");  
        }
    }  
}

注意,认证的密码不是邮箱的密码,而是一次性授权码,在邮箱设置里开启“POP3/SMTP服务”可以获取到。