需要自行完成一个研究,正好研究一下电子邮件相关协议。
一、电子邮件系统架构
图片来源:易科博客
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 ForouzanTo: Sophia FeganDate: 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 (126 com[20140526 ]) EHLO HI // 需要用户验证,使用EHLO250 -mail250 -PIPELINING250 -AUTH LOGIN PLAIN 250 -AUTH=LOGIN PLAIN250 -coremail 1 Uxr2xKj7kG0xkI17xGrU7I0s8FY2U3Uj8Cz28x1UUUUU7Ic2I0Y2UFvkzJ5UCa0xDrUUUUj250 -STARTTLS250 8 BITMIME 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--.47203 S5 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--.47203S 5; Wed, 09 Sep 2020 16 :38 :40 +0800 (CST)From: <leoxyw@126 .com>To: <leoxyw@126 .com>Subject: IDNETworkingDate: Wed, 9 Sep 2020 16 :48 :31 +0800 X-CM-TRANSID:DcmowACHUwClklhfTW4eJg--.47203S 5 Message-Id:<5 F5896EC.1825 CC.07706 @m15113.mail.126 .com> X-Coremail-Antispam: 1 Uf129KBjDUn29KB7ZKAUJUUUUU529EdanIXcx71UUUUU7v73 VFW2AGmfu7bjvjm3AaLaJ3UbIYCTnIWIevJa73UjIFyTuYvjxUbfO7UUUUU X-Originating-IP: [113.140 .11.120 ] X-CM-SenderInfo: xohr55bz6rjloofrz/1 tbiKQyaXlpEBL8pHgAAsz This is my message try smtp plzlet it go plz
有关SMTP的进一步信息可以参阅 RFC 821 和 RFC 2821
四、POP3 地址(以126邮箱为例):pop.126.com
端口号:110
命令 参数 状态 描述 USER username AUTHORIZATION 输入邮箱地址 PASS password AUTHORIZATION APOP Name Digest AUTHORIZATION Digest是MD5消息摘要 STAT None TRANSACTION 请求服务器发回关于邮箱的统计资料,如邮件总数和总字节数 UIDL [Msg#] TRANSACTION 返回邮件的唯一标识符,POP3会话的每个标识符都将是唯一的 LIST [Msg#] TRANSACTION 返回邮件数量和每个邮件的大小 RETR [Msg#] TRANSACTION 返回由参数标识的邮件的全部文本 DELE [Msg#] TRANSACTION 服务器将由参数标识的邮件标记为删除,由quit命令执行 RSET None TRANSACTION 服务器将重置所有标记为删除的邮件,用于撤消DELE命令 TOP msg n TRANSACTION 服务器将返回由参数标识的邮件前n行内容,n必须是正整数 NOOP None TRANSACTION 服务器返回一个肯定的响应 QUIT None UPDATE
五、实验结果与代码
赶工完成,有些简陋,而且被吐槽 “你这个东西有什么难度?” 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++) { 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(); 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) { e.printStackTrace(); } } public static void writeemail (BufferedReader sockin,PrintWriter sockout,String username,String password) { Socket smptsocket = null ; sockout.println("NOOP" ); try { 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) { 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) { 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(); 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="" ; }