使用STMP协议发送邮件

前言

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

基本步骤

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

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

基于命令行

1
2
3
4
5
6
7
# 使用 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])
1
2
3
4
# HELO 命令
> HELO stmp.163.com

250 OK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 认证 (dXNlcm5hbWU6 即为 username:、UGFzc3dvcmQ6 即为 password:)
> AUTH LOGIN

334 dXNlcm5hbWU6

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

334 UGFzc3dvcmQ6

# 输入 base64 编码的密码
> WU54UktjYVJLZFhLWmV

235 Authentication successful

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 发件人
> MAIL FROM:<wuyifan2@163.com>

250 Mail OK

# 收件人
> RCPT TO:<liyifeng29@163.com>

250 Mail OK

# 抄送人
> RCPT TO:<76612151@qq.com>

250 Mail OK

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> 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 流实现邮件发送,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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,
"wuyifan2@163.com", "YNxRKcadfsdXsdfa",
"liyifeng@163.com", "123766119@qq.com",
"Greet day", "Today is a great day!");
} catch (Exception e) {
e.printStackTrace();
System.out.println("邮件发送失败");
}
}
}

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