Java 实现命令行收发邮件

Java 实现命令行收发邮件

需要自行完成一个研究,正好研究一下电子邮件相关协议。

一、电子邮件系统架构

图片来源:易科博客

MUA (Mail User Agent) 客户端,显示邮件内容,处理用户操作

MTA (Mail Transfer Agent) 报文传输代理,MTA客户机与MTA服务器间建立 SMTP 协议

MDA (Mail Deliver Agent) 服务器保存接收到的邮件

MRA (Mail Receive Agent) 为客户端从服务器拉取邮件,有POP3 IMAP4等协议

二、格式

电子邮件分信封和报文两部分,信封包含发信人的地址与收信人的地址。

报文有头部和主题,头部包括发件人,收件人,主题,其他信息(如编码),标识与内容以:分隔,例如:

1
2
3
4
From: Behrouz Forouzan
To: Sophia Fegan
Date: 1/5/05
Subject: Network

正文部分以 . 结束

MIME

电子邮件原本只支持 NVT 7位 ASCII 格式的报文,多用途因特网邮件扩充协议(MIME)允许发送方将非 ASCII 数据转化为 NVT ASCII 数据发送,并在接收方解码为原来的数据。

三、SMTP

每次投递,SMTP在发送方与邮件服务器间使用一次,在邮件服务器间使用一次。它使用命令与响应在MTA客户与MTA服务器之间传输报文。每一条命令与响应都以 回车和换行 <CRLF>(\r\n)结束。

SMTP的地址(以126邮箱为例) smtp.126.com.

端口号:25

命令

客户发给服务器,包含关键词及变量,用冒号分隔,变量视情况可以是任意数量的。

关键词作用
HELO+发送方主机名(域名),使用标准的SMTP
EHLO扩展的HELO,支持用户认证,参见 RFC 2821
MAIL FROM+发件人地址,标明发件人
RCPT TO+收件人地址,标明收件人,可以有多个
DATA+邮件内容,以<CRLF>.<CRLF>结束
QUIT无变量,结束会话
VRFY+地址,验证指定邮箱是否存在;由于安全方面的原因,服务器常禁止此命令
EXPN+地址列表,验证给定的邮箱列表是否存在,扩充邮箱列表,也常被禁用
HELP(+命令),查询服务器支持什么命令
NOOP无变量,服务器响应 250 OK
RSET无变量,重置会话,当前传输被取消,服务器响应 250 OK
STARTTLS启用TLS

状态码

状态码说明状态码说明
211系统状态或帮助回答500语法错误,命令无法识别
214帮助信息501语法错误,参数或变量出错
220服务就绪502命令未执行
221服务关闭503命令序列不正确
250操作完成504命令暂时未执行
251用户非本地,将转发报文550命令未执行,邮箱不可用
354开始邮件内容输入551用户非本地
421服务不可用552异常终止,超出存储位置
450邮箱不可用553请求未发生,邮箱名不可用
451命令异常终止,本地错误554操作失败
452命令异常终止,存储空间不足

示例

以126邮箱为例,首先进入邮箱打开SMTP服务,获得授权码。找到base64加密网站,将登录邮箱地址和授权码分别加密。

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
telnet smtp.126.com 25 //smtp端口为25
Trying 220.181.15.113...
Connected to smtp.126.com.
Escape character is '^]'.
220 126.com Anti-spam GT for Coremail System (126com[20140526])
EHLO HI //需要用户验证,使用EHLO
250-mail
250-PIPELINING
250-AUTH LOGIN PLAIN
250-AUTH=LOGIN PLAIN
250-coremail 1Uxr2xKj7kG0xkI17xGrU7I0s8FY2U3Uj8Cz28x1UUUUU7Ic2I0Y2UFvkzJ5UCa0xDrUUUUj
250-STARTTLS
250 8BITMIME
auth login //认证登录
334 dXNlcm5hbWU6 //base64的Username:
**** //登录邮箱地址转化为base64,输入
334 UGFzc3dvcmQ6 //base64的Password:
**** //邮箱给你的授权码转化为base64,输入
235 Authentication successful
MAIL FROM: <leoxyw@126.com> //发件人地址
250 Mail OK
RCPT TO: <leoxyw@126.com> //收件人地址
250 Mail OK
DATA //邮件主体
354 End data with <CR><LF>.<CR><LF>
//邮件头字段,这里输入不规范会被当做垃圾邮件退回。
From: <leoxyw@126.com>
To: <leoxyw@126.com>
Subject: IDNETworking
Date: Wed, 9 Sep 2020 16:48:31 +0800
//正文
This is my message try smtp plz
let it go
plz
. //回车换行.回车换行 结束输入
250 Mail OK queued as smtp3,DcmowACHUwClklhfTW4eJg--.47203S5 1599641324
QUIT

进入邮箱,可以看到刚刚收到的邮件。

更多->查看邮件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Received: from HI (unknown [113.140.11.120])
by smtp3 (Coremail) with SMTP id DcmowACHUwClklhfTW4eJg--.47203S5;
Wed, 09 Sep 2020 16:38:40 +0800 (CST)
From: <leoxyw@126.com>
To: <leoxyw@126.com>
Subject: IDNETworking
Date: Wed, 9 Sep 2020 16:48:31 +0800
X-CM-TRANSID:DcmowACHUwClklhfTW4eJg--.47203S5
Message-Id:<5F5896EC.1825CC.07706@m15113.mail.126.com>
X-Coremail-Antispam: 1Uf129KBjDUn29KB7ZKAUJUUUUU529EdanIXcx71UUUUU7v73
VFW2AGmfu7bjvjm3AaLaJ3UbIYCTnIWIevJa73UjIFyTuYvjxUbfO7UUUUU
X-Originating-IP: [113.140.11.120]
X-CM-SenderInfo: xohr55bz6rjloofrz/1tbiKQyaXlpEBL8pHgAAsz

This is my message try smtp plz
let it go
plz

有关SMTP的进一步信息可以参阅 RFC 821RFC 2821

四、POP3

地址(以126邮箱为例):pop.126.com

端口号:110

命令参数状态描述
USERusernameAUTHORIZATION输入邮箱地址
PASSpasswordAUTHORIZATION
APOPName DigestAUTHORIZATIONDigest是MD5消息摘要
STATNoneTRANSACTION请求服务器发回关于邮箱的统计资料,如邮件总数和总字节数
UIDL[Msg#]TRANSACTION返回邮件的唯一标识符,POP3会话的每个标识符都将是唯一的
LIST[Msg#]TRANSACTION返回邮件数量和每个邮件的大小
RETR[Msg#]TRANSACTION返回由参数标识的邮件的全部文本
DELE[Msg#]TRANSACTION服务器将由参数标识的邮件标记为删除,由quit命令执行
RSETNoneTRANSACTION服务器将重置所有标记为删除的邮件,用于撤消DELE命令
TOPmsg nTRANSACTION服务器将返回由参数标识的邮件前n行内容,n必须是正整数
NOOPNoneTRANSACTION服务器返回一个肯定的响应
QUITNoneUPDATE

五、实验结果与代码

email1

email2 

email3

赶工完成,有些简陋,而且被吐槽 “你这个东西有什么难度?” TAT
好像的确没什么难度。

Emailclient.java

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
package email;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Scanner;
import java.io.*;
import java.net.Socket;

public class Emailclient {
private static String POP3Server = "pop.126.com";
private static String SMTPServer = "smtp.126.com";

public static void recvmailbox(BufferedReader sockin,PrintWriter sockout) {
sockout.println("stat");
String temp[];
try {
temp = sockin.readLine().split(" ");
int count = Integer.parseInt(temp[1]);//得到信箱中共有多少封邮件
Maillist maillist = new Maillist();

System.out.println("信箱共有" + count + "封邮件");

for (int i = 1; i < count + 1; i++) {//依次将邮件添加至 maillist
sockout.println("retr " + i);
Mailinfo mailinfo = new Mailinfo();
int fromindex = "from:".length();
int subjectindex = "subject:".length();
int flag = 1;
while (flag==1) {
String reply = sockin.readLine();
//System.out.println(reply);
if(reply.startsWith("from:")||reply.startsWith("From:"))
mailinfo.received_from = reply.substring(fromindex);
if(reply.startsWith("subject:")||reply.startsWith("Subject:"))
mailinfo.subject = reply.substring(subjectindex);
if(reply.equals("")) {
while(true) {
reply = sockin.readLine();
mailinfo.content = mailinfo.content+reply+"\r\n" ;
if (reply.toLowerCase().equals(".")) {
flag = 0;
break;
}
}
}
}
maillist.putMail(mailinfo);
}
int flag=1;
while(flag==1) {
System.out.println("输入0查看邮件列表,输入数字查看该序号邮件,输入-1返回主菜单");
int command;
Scanner scanner3 = new Scanner(System.in);
command = scanner3.nextInt();
switch (command) {
case 0:
System.out.println("序号 发件人 主题");
for(int i=0;i<count;i++) {
Mailinfo info = new Mailinfo();
info = maillist.getMail(i+1);
System.out.println((i+1)+" "+info.received_from+" "+info.subject);
}
break;
case -1:
flag = 0;
break;
default :
Mailinfo info = new Mailinfo();
info = maillist.getMail(command);
System.out.println("主题:"+info.subject);
System.out.println("发件人:"+info.received_from);
System.out.println("正文:"+info.content);
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

public static void writeemail(BufferedReader sockin,PrintWriter sockout,String username,String password) {
Socket smptsocket = null;
sockout.println("NOOP");
try {
//建立SMTP连接
System.out.println("S:" + sockin.readLine());
String recvadd = "";
String fromadd = username;
smptsocket = new Socket(Emailclient.SMTPServer,25);
InputStream sis = smptsocket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(sis));
OutputStream sos = smptsocket.getOutputStream();
PrintWriter pw = new PrintWriter(sos, true);
System.out.println("SMPT:" + br.readLine());
int recvcount = 1;

//登录与验证
pw.println("EHLO Alice");
String str = null;


char[] data = new char[1024];
br.read(data,0,1024);
System.out.println("SMPT:" + data);
pw.println("auth login");
System.out.println("SMPT:" + br.readLine());
String base64useranme = Base64.getEncoder().encodeToString(username.getBytes("utf-8"));
pw.println(base64useranme);
System.out.println("SMPT:" + br.readLine());
Scanner scanner2 = new Scanner(System.in);
System.out.print("请输入写邮件授权码\n");
String passwordsmtp = scanner2.next();
String base64password = Base64.getEncoder().encodeToString(passwordsmtp.getBytes("utf-8"));
pw.println(base64password);
System.out.println("SMPT:" + br.readLine());

//发件人与收件人
pw.println("MAIL FROM: <"+fromadd+">");
System.out.println("SMPT:" + br.readLine());

System.out.print("请输入收件人个数\n");
recvcount = scanner2.nextInt();
List<String> names=new ArrayList<String>();

for(int i=0;i<recvcount;i++) {
System.out.print("请输入收件人 "+(i+1)+" 地址\n");
recvadd = scanner2.next();
names.add(recvadd);
pw.println("RCPT TO: <"+recvadd+">");
System.out.println("SMPT:" + br.readLine());
}
//邮件内容
pw.println("Data");
System.out.println("SMPT:" + br.readLine());
Scanner scanner3 = new Scanner(System.in);
System.out.print("请输入邮件主题\n");

String subject = scanner3.nextLine();
pw.println("From: "+fromadd);
pw.println("To: "+String.join(",",names));
pw.println("Subject: "+subject);
System.out.print("输入正文,以新一行的“.”结束\n");
while(true) {
String msg = scanner2.nextLine();
pw.println(msg);
if(msg.equals("."))break;
}
System.out.println("SMPT:" + br.readLine());
System.out.println("邮件发送完成,写邮件通道关闭");
pw.println("quit");
System.out.println("SMPT:" + br.readLine());


} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
if (smptsocket != null) {
smptsocket.close();
}
} catch (IOException e) {}
}

}

public static void quit(BufferedReader sockin,PrintWriter sockout) {
sockout.println("quit");
try {
System.out.println("S:" + sockin.readLine());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

public static void AUTHORIZATION(BufferedReader sockin,PrintWriter sockout,String USERNAME,String PASSWORD) throws IOException {
System.out.println("S:" + sockin.readLine());
sockout.println("user " + USERNAME);
System.out.println("S:" + sockin.readLine());
sockout.println("pass " + PASSWORD);
System.out.println("S:" + sockin.readLine());
}

public static void main(String[] args) {
int POP3Port = 110;
String USERNAME = "";
String PASSWORD = "";
Socket client = null;
try {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入邮箱地址\n");
USERNAME = scanner.next();
System.out.print("请输入授权码\n");
PASSWORD = scanner.next();
//创建socket
client = new Socket(Emailclient.POP3Server, POP3Port);
InputStream is = client.getInputStream();
BufferedReader sockin = new BufferedReader(new InputStreamReader(is));
OutputStream os = client.getOutputStream();
PrintWriter sockout = new PrintWriter(os, true);

AUTHORIZATION(sockin,sockout,USERNAME,PASSWORD);

while(true) {
System.out.print("请输入命令 1.查看收件箱 2.写信 3.刷新 0.退出\n");
int command;
command = scanner.nextInt();
switch (command) {
case 1:
recvmailbox(sockin,sockout);
break;
case 2:
writeemail(sockin,sockout,USERNAME,PASSWORD);
break;
case 3:
quit(sockin,sockout);
client = new Socket(Emailclient.POP3Server, POP3Port);
is = client.getInputStream();
sockin = new BufferedReader(new InputStreamReader(is));
os = client.getOutputStream();
sockout = new PrintWriter(os, true);
AUTHORIZATION(sockin,sockout,USERNAME,PASSWORD);
break;
case 0:
quit(sockin,sockout);
sockout.println("quit");
System.out.println("S:" + sockin.readLine());
if(sockout!=null)
sockout.close();
if(os!=null)
os.close();
if(sockin!=null)
sockin.close();
if(is!=null)
is.close();
if (client != null) {
client.close();
}
System.exit(0);
}
}

} catch (IOException e) {
System.out.println(e.toString());
} finally {
try {
if (client != null) {
client.close();
}
} catch (IOException e) {}
}
}
}

Maillist.java

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
package email;

import java.util.ArrayList;

public class Maillist {
ArrayList<Mailinfo> mails;
public Maillist()
{
mails=new ArrayList<Mailinfo>();
}

public void putMail(Mailinfo mail)
{
mails.add(mail);
}

public Mailinfo getMail(int num)
{
return mails.get(num-1);
}

public int mailCount()
{
return mails.size();
}
}

Mailinfo.java

1
2
3
4
5
6
7
8
package email;

public class Mailinfo {
public String received_from="";
public String subject="";
public String content="";
}