SMTP协议原理 + Java实现

11.1 电子邮件系统工作原理
电子邮件是因特网上最为流行的应用之一,它主要包括如下组成部分:用户代理、邮件服务器、简单邮件传输协议(simple Mail Transfer Protocol,简称SMTP)、邮件消息格式、邮件访问协议。图11.1展示了因特网电子邮件系统的概貌。在下文对电子邮件系统各组成部分的说明中,以发信人A1ice给收信人Bob发送电子邮件消息作为例子。


111.png

图11.1 因特网电子邮件系统概貌

1.用户代理

用户代理提供用户阅读、保存、回复、转发、编写、发送和删除邮件消息等功能。Alice写完电子邮件消息后,其用户代理把这个消息发送给Alice邮箱所在的邮件服务器,再由该邮件服务器把这个消息放入外出消息队列中并负责发送到Bob邮箱所在的邮件服务器。当Bob想阅读电子邮件消息时,其用户代理将从其邮件服务器上的邮箱中取得邮件。20世纪90年代后期,图形用户界面(GUI)的电子邮件用户代理变得流行起来,允许用户阅读和编写多媒体消息。当前流行的用户代理包括Outlook、Foxmail等。

2.邮件服务器

邮件服务器构成了电子邮件系统的核心。每个收信人都有一个位于某个邮件服务器上的邮箱(mailbox)。Bob的邮箱用于管理和维护已经发送给他的邮件消息。一个邮件消息的典型流程是从发信人的用户代理开始,经发信人邮箱所在的邮件服务器,中转到收信人邮箱所在的邮件服务器,然后投递到收信人的邮箱中。当Bob想查看自己的邮箱中的邮件消息时,其邮箱所在的邮件服务器将以他提供的用户名和口令认证他。Alice邮箱所在邮件服务器还必须处理Bob邮箱所在邮件服务器出故障的情况。如果Alice方的邮件服务器无法把邮件消息立即递送到Bob方的邮件服务器,则A1ice方的服务器就把它们存放在消息队列(message queue)中,以后再尝试递送。这种尝试通常每30分钟左右执行一次:要是过了若干天仍未尝试成功,该服务器就把这个消息从消息队列中删除,同时以另一个邮件消息通知发信人(即Alice)。

3.简单邮件传输协议SMTP

SMTP是因特网电子邮件系统重要的应用层协议。它使用由TCP提供的可靠的数据传输服务把邮件消息从发信人邮箱所在邮件服务器传送到收信人邮箱所在邮件服务器。SMTP是客户-服务器应用模式,由发信人的邮件服务器执行的客户端和收信人的邮件服务器执行的服务器端组成。SMTP的客户端和服务器端同时运行在每个邮件服务器上。当一个邮件服务器向其它邮件服务器发送邮件消息时,它是作为SMTP客户端。当一个邮件服务器从其它邮件服务器接收邮件消息时,它是作为SMTP服务器端。
SMTP规范定义在RFC 821中,它的作用是把邮件消息从发信人的邮件服务器传送到收信人的邮件服务器。SMTP限制所有邮件消息的信体必须是简单的7位ASCII字符格式。这个限制使得二进制多媒体数据在由SMTP传送之前必须编码成7位ASCII文本;SMTP传送完毕之后,再把相应的7位ASCII文本邮件消息解码成二进制数据。
下面通过假设Alice给Bob发送一个简单的ASCII文本邮件消息的情形来说明SMTP的基本!
操作:
(1) Alice调用自己的电子邮件用户代理,给出Bob的电子邮件地址(例如bob@csu.edu),写好邮件内容,然后让用户代理发送本邮件消息。
(2) Alice的用户代理把该邮件消息发送到其邮件服务器中,由邮件服务器把该消息放入某个消息队列中。
(3) 运行在A1ice的邮件服务器上的SMTP客户端看到消息队列中的这个邮件消息后,打开一个到运行在Bob的邮件服务器主机上的SMTP服务器端的TCP连接。
(4) 经过最初的一些SMTP握手之后,SMTP客户把A1ice的邮件消息发送到TCP连接上。
(5) 在Bob的邮件服务器主机上,SMTP服务器收到这个邮件消息后,把这个消息投递到Bob的邮箱中。
(6) Bob在方便的时候调用自己的电子邮件用户代理阅读该邮件消息。图11.2展示了上述情形。

22.png

图11.2 A1ice的邮件服务器把邮件消息传送到Bob的邮件服务器

SMTP通常不使用中间的邮件服务器主机中转邮件。如果Bob的邮件服务器不工作了,那么A1ice发给Bob的邮件消息将存留在Alice的邮件服务器中等待新的尝试,而不会存放到某个中间的邮件服务器中。
SMTP协议与现实社会人们面对面交互的礼仪之间有许多相似之处。首先,运行在发送端邮件服务器主机上的SMTP客户,发起建立一个到运行在接收端邮件服务器主机上的SMTP服务器端口号25之间的TCP连接。如果接收邮件服务器当前不工作,SMTP客户就等待一段时间后再尝试建立该连接。这个连接建立之后,SMTP客户和服务器先执行一些应用层握手操作。就像人们在转手东西之前往往先自我介绍那样,SMTP客户和服务器也在传送信息之前先自我介绍一下。在这个SMTP握手阶段,SMTP客户向服务器分别指出发信人和收信人的电子邮件地址。彼此自我介绍完毕之后,客户发出邮件消息。SMTP使用传输层提供的可靠数据传输服务(TCP服务)把该消息无差错地传送到服务器。如果客户还有其它邮件消息需发送到同一个服务器,它就在同一个TCP连接上重复上述过程;否则,它就指示TCP关闭该连接。

假设客户所在主机名为sina.com,服务器所在主机名为csu.edu。前面标以“C:”的ASCII文本行是客户发送到它的TCP套接字中的完整文本行,前面标以“S:”的ASCII文本行是服务器发送到它的TCP套接字中的完整文本行。一个客户和服务器交互的例子如下 (以下传输脚本在TCP连接建立之后发生):

S: 220 csu.edu
C: HELO sina.com
S: 250 Hello sina.com, pleased to meet you
C: MAIL FROM: <alice@sina.com>
S: 250 Sender OK
C: RCPT TO: bob@csu.edu
S: 250 Recipient OK
C: DATA
S: 354 Enter mail, end with "." on a line by its self
C: Do you like ketchup?
C: How about pickles?
C: .
S: 250 Message accepted for delivery
C: QUIT
S: 221 csu.edu closing connection

在这个例子中,客户发送了一个从邮件服务器主机sina.com到csu.edu的邮件消息,信体内容为:“Do you like ketchup? How about pickles?”。客户总共发出了5个命令。其中HELO命令标识发信人自己的身份;MAIL FROM命令表示请求发送邮件,初始化邮件传输;RCPT TO命令标识某电子邮件的计划接收人;DATA命令表示所有的邮件接收人已标识,并初始化数据传输,以.结束;QUIT命令表示退出邮件发送过程,结束会话。
服务器给每个命令发回应答,其中每个应答都由应答码和一些英语解释(可选)构成。SMTP使用持久连接,如果发送邮件服务器有多个邮件消息需发送到同一个接收邮件服务器,那么所有这些消息可以在同一个TCP连接中发送。对于其中的每一个消息,客户以一个新的“HELO sina.com”命令开始整个消息发送过程,但是QUIT命令要等到所有消息都发送完之后才发出。一旦SMTP把Alice发给Bob的邮件消息从Alice的邮件服务器传送到Bob的邮件服务器,该邮件消息就存放在Bob的邮箱中。

4.邮件消息格式

当Alice给Bob发一封普通的邮政信件时,她把这封信装入一个信封里,在信封上写明Bob的地址和自己的回信地址,然后投入邮箱;邮政业务在递送这封信的过程中,也会把表明时间和地点的邮戳盖在信封上。类似地,当电子邮件消息从一个人传送到另一个人时,在信体之前会有一个含有这些外围信息的信头。这些信息由一系列在RFC 822中定义的邮件消息头部及其值构成。邮件消息中构成信头的各个头部和信体之间以一个空行(即CRLF)分割。RFC 822详细说明了各个邮件消息头部的格式和含义。邮件消息的每个头部都是直观可读的文本,由一个后跟冒号的关键字和相应的值构成。这些头部有些是必须的,有些则是可选的。每个信头必须有一个From:头部和一个To:头部。还可以有一个Subject:头部和其他头部。这些头部和前面讨论的SMTP命令不是一回事:SMTP命令是SMTP握手协议的一部分,邮件消息头部则属于邮件消息的一部分。
下面是一个典型的电子邮件信头:
From: alice@sina.com
To: bob@csu.edu
Subject: this is a letter
信头之后空一行就是信体。
SMTP RFC822要求每个邮件消息的信体以单个点号构成的一行做结束标记,换用ASCII字符形式,就是每个邮件消息的信体必须以“CRLF.CRLF”结尾,其中CR和LF分别代表回车符和换行符。这种方式下,当从同一个SMTP客户接收一系列邮件消息时,SMTP服务器可以通过在字节流中搜索“CRLF.CRLF”来分割每个消息。
RFC 822中说明的邮件消息头部尽管足以满足发送普通ASCII文本邮件的要求,但是在多媒体消息(例如,包含图像、音频或视频数据的消息)的描述和非ASCII文本格式(例如,非英语国家使用的文字)的承载上,却显然不够,例如,若信体是JPEG图像的二进制数据,那么这些二进制数据字节流中可能出现“CRLF.CRLF”模式。这将导致SMTP服务器误认为当前邮件消息已结束。为避免这样的问题,二进制数据应以某种方式编码成ASCII文本,保证其中不存在特定的ASCII字符(包括点号)。要发送非ASCII文本的邮件消息,必须由发送者的用户代理在其中增添额外的头部。RFC 2045和RFC 2046定义了这些额外的头部,它们是针对RFC 822的多用途因特网邮件扩展(Multipurpose Internet Mail Extensions, 简称MIME)。
支持多媒体的两个关键MIME头部是Content-Type:和Content-Tansfer-Encoding:。Content-Type:头部允许接收用户代理对邮件消息采取合适的行动。例如,通过指出信体内容为一个JPEG图像,接收用户代理可以把信件定向到某个JPEG解压缩例程。为确保SMTP正常工作,非ASCII文本消息必须预先编码成ASCII文本格式。Content-Tansfer-Encoding:头部用于告知接收用户代理信体已被编码成ASCII格式,并指出具体编码方式。这样,当某个用户代理收到一个包含这两个头部的邮件消息时,它首先使用Content-Tansfer-Encoding:头部的值把信体转换成原始的非ASCII文本形式,再使用Content-Type:头部的值确定自己应该对信体采取什么行动。
假设Alice想给Bob发送一个JPEG图像,她为此调用自己的用户代理,给出Bob的电子邮件地址和邮件消息的主题,并把这个JPEG图像插入这个邮件消息的信体中(这个图像有可能是作为该邮件消息的“附件”插入的,具体取决于Alice所用的用户代理)。Alice填写完邮件消息后让用户代理把它发送出去。Alice的用户代理生成一个大体如下的MIME消息:
From: alice@sina.com
To: bob@csu.edu
Subject: picture of mine
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Type: image/jpeg
{...base64编码数据...
...base64编码数据...}
从这个例子看到,Alice的用户代理采用base64编码格式对这个JPEG图像进行编码,而base64是在MIME中标准化的用于转换成某种可接受的7位ASCII格式的编码技术之一(定义在RFC 2045中)。
Base64采用了一种很简单的编码转换:对于待编码数据,以3个字节为单位,依次取6位数据并在前面补上两个0形成新的8位编码,由于3×8=4×6,这样3个字节的输入会变成4个字节的输出,长度上增加了1/3。上面的处理还不能保证得到的字符都是可见字符,为了达到此目的,Base64制定了一个编码表,进行统一的转换,见表11.1。码表的大小为26=64,这也是Base64名称的由来。由于编码是以3个字节为单位,当剩下的字符数量不足3个字节时,则应使用0进行填充,相应地,输出字符则使用‘=’占位,因此编码后输出的文本末尾可能会出现1至2个‘=’。
表11.1 Base64编码表
值 编码 值 编码 值 编码 值 编码 值 编码 值 编码 值 编码 值 编码
0 A 8 I 16 Q 24 Y 32 g 40 o 48 w 56 4
1 B 9 J 17 R 25 Z 33 h 41 p 49 x 57 5
2 C 10 K 18 S 26 a 34 i 42 q 50 y 58 6
3 D 11 L 19 T 27 b 35 j 43 r 51 z 59 7
4 E 12 M 20 U 28 c 36 k 44 s 52 0 60 8
5 F 13 N 21 V 29 d 37 l 45 t 53 1 61 9
6 G 14 O 22 W 30 e 38 m 46 u 54 2 62 +
7 H 15 P 23 X 31 f 39 n 47 v 55 3 63 /

Bob阅读邮件时,其用户代理所操作的是同一个MIME消息。该用户代理看到值为base64的Content-Transfer-Encoding:头部后,知道该邮件消息中还有一个值为image/jpeg的Content-Type:头部,它告知Bob的用户代理应该对信体进行JPEG解压缩。该邮件消息中的MIME-Version:头部指示本消息所用的MIME版本。
Content-Type:头部遵循MIME规范(定义在RFC 2046中),其格式为:Content-Type: type/subtype; parameters。其中parameters(以及其前面的分号)是可选的。通过在Content-Type:头部给出媒体类型(type)名和子类型(subtype)名来说明MIME消息信体中数据的性质。类型名和子类型名之后的其余部分是一组参数。类型名用于声明数据的一般类型,子类型名用于指明这类数据中的某种具体格式。参数是对于类型的修饰说明,取值取决于类型和子类型本身,不影响内容性质的指定。MIME是按照可扩展目标仔细地设计的,并预期媒体类型/子类型以及它们的相关参数会随时间显著增长。为确保以有秩序的文档完备公开的方式开发这些类型/子类型,MIME建立了一套注册程式,把因特网分配数值权威(Internet Asigned Numbers Authority, 简称IANA)作为MIME各个可扩展域的中心注册处。RFC 2048具体说明了这些可扩展域的注册程式。每个MIME类型关联一组子类型,其数量在逐年增长。一些主要类型如下:
(1) text: text类型用于向接收者的用户代理指出消息体为文本。该类型的一个普遍使用的类型/子类型对为text/plain。子类型plain指定不含任何格式定义信息的普通文本。plain文本不加任何解释地按照原样显示,不需要特殊的软件,但要求支持给定的字符集。在实际的邮件消息中经常能看到值对为text/plain; charset = gb2312或  text/plain; charset = “ISO-8859-1”的Content-Type:头部,其中的参数指出用于产生相应消息的字符集。另一个普遍使用的类型/子类型对是text/html。子类型html指示接收用户代理解释嵌入在消息体中的html标记,从而像Web页面那样显示信件内容,其中有可能包含各种字体的文本、超链接、Java小应用程序等等。
(2) image: image类型用于向接收用户代理指出消息体为图像。该类型较为流行的两个类型/子类型对为image/gif和image/jpeg,接收用户代理碰到这样的类型时,就知道该把消息体作为GIF图像或JPEG图像解码并显示。
(3) audio: audio类型需要音频输出设备(例如扬声器或电话)来表达内容。这类型中常见的已标准化子类型包括basic(基本8位u-law编码)和32kadpcm(RFC 1911中定义的一种32Kbps格式)。
(4) video: video类型的子类型包括mpeg和quicktime。
(5) application: application类型适用于不适合归为其它类别的数据,通常用在必须由某个应用程序预先处理才能为用户所见或所用的数据上。例如,当用户在某个电子邮件消息中附带一个微软word文档时,其用户代理一般把它的类型子类型对指定为application/msword;这将导致接收用户代理启动微软word应用程序,并把该MIME消息的信体传递给它处理。这类型的一个重要子类型是octet-stream,它用于指示信体含有任意的二进制数据。收到内容类型为application/octet-stream的邮件消息后,接收用户代理会提示用户是否把信体保存到硬盘中,以便稍后处理。
(6) MultiPart: MultiPart是MIME类型中一个相当重要的类型。当一个多媒体消息含有不止一个对象时(例如多个图像或ASCII文本与图像共存),其Content-Type:头部的值通常为multipart/mixed。这头部向接收用户代理指出本消息中含有多个对象。在多个对象共处同一个邮件消息中的情况下,通过在每个对象之间放置边界字符串,并在每个对象之前定义Content-Type:和Content-Transfer-Encoding:头部,接收用户代理可以确定:(1)每个对象的起止位置;(2)每个非ASCII文本对象的传送编码方式;(3)每个对象的内容类型。为便于理解multipart/mixed,举例如下:假设Alice想给Bob发送一个邮件消息,其内容为一些ASCII文本,后跟一个JPEG图像,再跟一些ASCII文本。Alice使用自己的用户代理编辑文本并附上图像后,该用户代理生成一个如下的邮件消息:
From: alice@sina.com
To: bob@csu.edu
MIME-Version: 1.0
Content-type: multipart/mixed; Boundary=StartOfNextPart
--StartOfNextPart
Dear bob,
Please look at the picture
--StartOfNextPart
Content-Transfer-Encoding: base64
Content-type: image/jpeg
{...base64编码的数据...
...base64编码的数据...}
--StartOfNextPart
there is some acsii letter here
从中可以看出,Content-type:头部的Boundary参数用于指定分隔各个部分的边界字符串。在邮件消息体中,该分隔字符串以两个短划线开头,以CRLF结尾。
一个电子邮件消息由多个部件构成。信体是邮件消息的核心,它是发送者发送给接收者的真正数据。对于多部分邮件消息来说,其信体本身由多个部分组成,而每个部分又有一个或多个说明其数据性质的头部。信体之前是一个空行和由多个邮件消息头部组成的信头。这些头部既包括RFC 822头部,例如From:、To:和subject:,也包括MIME头部,例如Content-type:和Content-Transfer-Encoding:。除此之外,还有由SMTP接收服务器插到每个邮件消息项端的Received:头部,它给出了发出本消息的SMTP服务器的主机名(“from”)、收取本消息的SMTP服务器的主机名(“by”)以及接收服务器收取本消息的时间。因此,作为接收者的用户看到的邮件消息大致如下:
Received: from sina.com by csu.edu; 18 Oct 2007 09:53:37 GMT
From: alice@sina.com
To: bob@csu.edu
MIME-Version: 1.0
Content-type: multipart/mixed; Boundary=StartOfNextPart
--StartOfNextPart
Dear bob,
Please look at the picture
--StartOfNextPart
Content-Transfer-Encoding: base64
Content-type: image/jpeg
{...base64编码的数据...
...base64编码的数据...}
--StartOfNextPart
there is some acsii letter here
有时候,单个邮件消息会有多个Received:头部,有的还会有一个较复杂的Return-path:头部。这是因为邮件消息在从发送者的主机到接收者的主机的传送过程中,可能会被转发到不止一个SMTP服务器。例如,如果Bob指示他在主机csu.edu上的邮件服务器把他的所有邮件转发到主机sohu.com,那么他通过其用户代理看到的邮件消息可能以大体如下的两行开头:
Received: from csu.edu by sohu.com; 18 Oct 2007 09:55:37 GMT
Received: from sina.com by csu.edu; 18 Oct 2007 09:53:37 GMT
这些头部给接收用户代理提供了相应邮件消息访问过的SMTP服务器及访问时间的踪迹。SMTP规范所在的RFC 822详细定义丁Received:头部的语法。

5.邮件访问协议

在前面的讨论中,都是假设Bob通过直接登录到自己的邮件服务器主机启动用户代理来阅读邮件消息。直到20世纪90年代早期,这仍然是标准的做法。然而,当今的用户一般使用在本地PC机(或Mac机)上执行的用户代理来阅读邮件,而不管是办公室PC机、家庭PC机还是便携机。用户在本地PC机上执行用户代理可享受诸多好处,包括方便查看多媒体邮件消息和附件。
邮件消息的接收者在本地PC机上执行用户代理时,很自然的一个想法是在本地PC机上也运行邮件服务器,然而这种方法存在一个问题。邮件服务器是管理邮箱并运行SMTP的客户端和服务器端的,这意味着如果收信人把自己的邮件服务器驻留在本地PC上,那么他不得不始终开着这台PC机并连接在因特网上,以便接收可能在任意时刻到达的新邮件。对于大多数因特网用户来说,这显然是不现实或不经济的做法。相反,用户一般只在本地PC机上运行一个用户代理,由它远程访问存放在某台共享的邮件服务器主机上的邮箱,而该邮件服务器主机总是连接在因特网上并为多个用户所共享。该主机及其上的邮件服务器—般由该用户的ISP(例如大学或公司)维护。
用户代理运行在各个用户的本地PC机上,而邮件服务器运行在ISP或机构内部网络中的某台服务器主机上,用户代理和邮件服务器之间需要有一个互相通信的协议。先查看一下出自从Alice的本地PC机的某个邮件消息如何设法到达Bob邮箱所在的SMTP邮件服务器。这个任务可简单地由Alice的用户代理使用SMTP直接与Bob邮箱所在的邮件服务器进行通信来完成。具体地说,从Alice的用户代理发起建立一个到Bob邮箱所在的邮件服务器的TCP连接,并通过该连接发出SMTP握手命令,再用DATA命令上传邮件消息,最后关闭连接。这种方法尽管切实可行,却很少被采用,因为它没有给Alice的用户代理提供任何资源来应对目标邮件服务器临时崩溃的情况。相反,通常采用的方法是先由Alice的用户代理发起与自己邮箱所在的邮件服务器的一个SMTP会话,把邮件消息上传到该邮件服务器;再由Alice方的邮件服务器发起与Bob方的邮件服务器的一次SMTP会话,把邮件消息中转给Bob方的邮件服务器。如果Bob方的邮件服务器暂时不可用,Alice方的邮件服务器就暂存该邮件消息,以后继续尝试。
现在的问题是,像Bob这样在本地PC机上运行用户代理的收信人该如何获取已到达自己的邮件服务器的邮件消息(该邮件服务器运行在Bob的ISP中的某台主机上)。通过引入用于从自己的邮件服务器到本地PC机上的用户代理传送邮件消息的邮件访问协议,可以解决这个问题。目前流行的邮件访问协议有两个:邮局协议版本3(Post Office Protocol Version 3, 简称POP3)和因特网邮件访问协议(Internet Mail Access Protocol, 简称IMAP)。这两个协议使用内拉操作获取邮件消息,而SMTP是一个外推协议。图11.3汇总了因特网电子邮件系统中所用的协议:SMTP用于从发送者的邮件服务器到接收者的邮件服务器传送邮件消息,也用于从发送者的用户代理到发送者的邮件服务器传送邮件消息;POP3或IMAP用于从接收者的邮件服务器到接收者的用户代理传送邮件消息。

图11.3 电子邮件协议及它们的通信实体
(1) POP3
RFC 1939中定义的POP3是一个简单的邮件访问协议,其功能也相当有限。POP3开始于用户代理打开一个到POP3服务器端口号110的TCP连接。POP3服务器与邮件服务器运行在相同的服务器主机上,前者从用户的邮箱中读取并可能删除邮件消息,后者往用户的邮箱中写入邮件消息。TCP连接建立好之后,POP3依次经历授权认证、处理和更新3个阶段。在授权阶段,用户代理分别发出一个用户名和一个口令以认证下载邮件消息的用户。在处理阶段,用户代理获取邮件消息,并可以标记待删除的邮件消息或去掉这些标记,获取邮件统计信息。更新阶段发生在用户代理发出quit命令以结束当前POP3会话之后,期间POP3服务器删除己加过删除标记的邮件消息。在POP3会话期间,用户代理发出命令,POP3服务器则对每个命令响应以一个应答。可能的应答有两个:指出刚才的命令执行成功的+OK(有时后跟一个解释性消息)和指出刚才的命令执行有误的-ERR。
a) 授权阶段:授权阶段共有两个基本命令:user <用户名>和pass<口令>。可以使用telnet工具指定使用POP端口号110直接登录到某台POP3服务器主机,并发出这些命令来展示它们的用法。假设mailserver是邮件服务器主机名,这个过程大致如下:
telnet mailserver 110
+OK POP3 server ready
user alice
+OK
pass password
+OK user successfully logged on
若写错了某个命令,POP3服务器将返回一个-ERR应答。
b) 处理阶段:POP3的用户代理可配置成“下载并删除”或“下载并保留”两种模式之一。POP3用户代理发出的一系列命令取决于其运行在哪种模式。在下载并删除模式中,用户代理会发出list、retr和dele命令。假设用户的邮箱中已存有两个邮件消息,其POP3处理阶段大致如下(其中前面标以“C:”的是用户代理发出的命令,而标以“S:”的是POP3服务器返回的应答):
C: list
S: 1 498
S: 2 912
S: .
C: retr 1
S: (abcd ......
S: ............
S: ......)
S: .
C: dele 1
C: retr 2
S: (... ...
S: ...
S: ......)
S: .
C: dele 2
C: quit
S: +OK POP3 server signing off
用户代理首先要求POP3服务器列出存放在自己的邮箱中的每个邮件消息的大小,接着依次取回并删除每个邮件消息。授权阶段结束之后,用户代理只能发出4个命令:list、retr、dele、quit。这些命令的具体语法定义在RFC 1939中。
c) 更新阶段:处理完quit命令后,POP3服务器进入更新阶段,把邮件消息1和2从相应的邮箱中删除。收信人可能希望从不止一台主机访问自已的邮箱,如既能从办公室PC机访问,也能从家庭PC机访问,还能从便携机访问。下载并删除模式不能满足这种需求,例如,这种模式将导致同一用户的邮件消息散布到他的多台主机上,若他先在家里的PC机上阅读了某个邮件消息,则以后就不能在便携机上阅读同一个邮件消息了。下载并保留模式则恰好相反,用户代理把己从POP3服务器下载的邮件消息继续保留在邮件服务器中。在这种模式下,用户可以在不同的时间从不同的主机访问同样的邮件消息。在用户代理和邮件服务器之间的POP3会话期间,POP3服务器维护一定的状态信息,即它跟踪哪些邮件消息己被标记成待删除状态。但POP3服务器不会跨会话保存状态信息,例如,每次会话开始之时没有任何邮件消息被标记成待删除状态。
(2) IMAP
收信人使用POP3把邮件消息下载到本地机之后,就可以把它们移入现行的或新创建的邮件夹中,然后可以进行删除、跨邮件夹转移、按发信人名字或消息主题搜索邮件消息等操作。但是这种邮件夹和邮件消息都存放在本地机上,不能满足游动用户的需求。游动用户更愿意在远程邮件服务器主机上维护邮件夹,这样从任何主机都可以访问它。
RFC 2060中定义的IMAP协议解决了上述问题。IMAP允许用户像对待本地邮箱那样操纵远程邮箱,使得收信人能够在自己的邮件服务器主机中创建并维护多个存放邮件消息的邮件夹。用户可以把邮件消息存入邮件夹,也可以从一个邮件夹到另一个邮件夹转移邮件,还可以在这些远程邮件夹中搜索匹配特定准则的邮件消息。IMAP服务器必须为每个用户维护一个邮件夹层次结构,因此IMAP的实现比POP3的实现复杂得多。某个用户相继访问自己的IMAP服务器时,这个IMAP服务器为该用户维护的状态信息跨这些相继的访问保持一致。POP3服务器则相反,一旦用户退出当前的POP3会话,就不再为其维护状态信息。
IMAP有一些允许用户代理获取邮件消息部件的命令。例如,用户代理可以只获取邮件消息的信头,或者只获取多部分MIME消息的某个部分。这个特征在用户代理和邮件服务器主机之间为低带宽连接(例如无线连接或低速调制解调器拨号连接)时非常有用。通过低带宽连接访问邮件时,用户很可能不希望下载自己的邮箱中的所有邮件消息,特别是可能含有音频或视频剪辑的长消息。
在IMAP会话过程中,首先,用户代理发起建立一个到IMAP服务器端口号143的TCP连接;然后,服务器返回初始问候消息;接着,客户与服务器就可以交互了。客户和服务器的交互由客户发出的命令、服务器返回的数据或命令完成结果响应构成。IMAP服务器在会话期间总是处于以下4个状态之一:未认证(nonauthenticated)、已认证(authenticated)、已选择(selected)和注销(1ogout)。未认证状态是连接刚建立时的初始状态,这种状态下,用户必须提供一个用户名和口令对才能继续下面的操作,即发出更多的命令。在已认证状态下,用户必须选择一个邮件夹才能发出作用于邮件消息的命令。在已选择状态下,用户可以发出作用于邮件消息的任何命令(获取、转移、删除、获取多部分消息的某个部分等等)。最后的注销状态是会话即将终止时的状态。
(3) HTTP邮件
使用提供这种服务的服务器时,用户代理是普通的web浏览器,用户与邮件服务器主机上的邮箱之间的交互由HTTP协议完成。当收信人(例如Bob)想要查看其邮箱中的邮件消息时,这些消息通过HTTP协议(而不是POP3或IMAP协议)从邮件服务器主机传送到他的浏览器。当发信人(例如Alice)想要发送电子邮件消息时,这些消息也是通过HTTP协议(而不是SMTP协议)从她的浏览器传送到她邮箱所在的邮件服务器主机的。但邮件消息在邮件服务器主机之间的中转仍然通过SMTP协议。与IMAP一样,用户可以在远程服务器主机中使用一个邮件夹层次结构组织邮件消息。基于Web的电子邮件给移动用户带来了极大方便,但其主要不足之处在于速度比较慢,因为其服务器主机往往远离客户主机,而且用户的浏览器是通过CGI脚本与邮件服务器间接交互的。当前,越来越多的用户使用基于浏览器的电子邮件服务,例如Hotmail和Yahoo!Mall,在未来,它将有可能形成与POP3或IMAP访问办法共存的局面。
(4) 持续媒体电子邮件
持续媒体(continuous-media, 简称CM)电子邮件是包含音频或视频数据的电子邮件系统,它对于朋友之间和家庭成员之间的异步交流很有吸引力。例如,学龄前儿童更愿意使用CM电子邮件给祖父母发送邮件消息。CM电子邮件在公司也可能受欢迎,因为办公室工作人员录制CM邮件消息的速度要比输入文本消息的速度快许多(使用英语每分钟可以说180个单词,但是普通办公室工作人员每分钟只能输入20-40个单词)。CM电子邮件在某些方面类似电话语音留言,不过功能要强大得多。它不仅给用户提供一个访问邮箱的图形界面,而且允许用户评注并回复CM邮件消息,或者把CM邮件消息转发给多个收信人。
CM电子邮件与传统文本电子邮件在许多方面存在差异。CM电子邮件消息比文本电子邮件消息大得多,对于端到端延迟有更严格的要求,对于收信人千差万别的因特网访问速率和本地存储容量也更为敏感。当前的电子邮件设施也存在一些妨碍CM电于邮件推广的不足之处。首先,许多现有的邮件服务器没有存放大邮件消息的容量;它们一般拒绝接收或中转CM邮件消息,因此不可能给依附它们的收信人发送这样的消息。其次,收信人的用户代理只在取得完整的邮件消息后才表达其内容,对于CM电子邮件,这会导致网络带宽和本地主机存储容量的过度浪费。事实上,仓储的CM邮件消息往往不是完整地表达的,因此接收未作表达的数据显然浪费了网络带宽和本地存储容量(例如,当某人收到来自相当唠叨的同事的长篇语音邮件消息后,可能只听上前15秒就决定不再听,要删除还剩20分钟内容的整个消息)。再次,当前使用的邮件访问协议(POP3、IMAP、HTTP)不适合为收信人流播放CM邮件消息。这些邮件访问协议没有提供这样的功能:允许用户暂停/恢复播放邮件消息内容,或者在邮件消息内重新定位播放点;另外,在TCP上实现流播放往往导致糟糕的接收效果。这些不足之处有望在未来的几年内得到解决。不过近来超大邮箱开始流行起来,如GMAIL等,邮箱容量的限制问题正在得到解决。

6.关键源代码示例

(1) 邮件发送端源代码
package mailoper;

import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

public class MailSender{
  private JFrame frame;
  private Scanner in;
  private PrintWriter out;
  private JTextField from;
  private JTextField to;
  private JTextField smtpServer;
  private JTextArea message;
  private JTextArea comm;
  private Logger logger;
  private PrintWriter ps;
  
  public static final int DEFAULT_WHIDTH = 300;
  public static final int DEFAULT_HEIGHT = 300;
  
  public MailSender(){
    try {
      ps = new PrintWriter(new FileOutputStream("WebRoot\\ClientLog.txt",true),true);
    } catch (FileNotFoundException e1) {
      e1.printStackTrace();
    }
    logger = new Logger(ps);
    logger.log("MailSender","Info","开始记录日志...");
  }
  
  public MailSender(boolean isFraem){
    this();
    initFrame();  
  }
  
  public void initFrame(){
    frame = new JFrame();
    frame.setSize(300,300);
    frame.setTitle("邮件服务器");
    frame.setLayout(new GridBagLayout());
    frame.setLocation(200,200);
    frame.setVisible(true);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
    frame.add(new JLabel("From"),new GBC(0,0).setFill(GBC.HORIZONTAL));
    
    from = new JTextField(20);
    from.setText("shexu@163.com");
    frame.add(from,new GBC(1,0).setFill(GBC.HORIZONTAL));
    
    frame.add(new JLabel("To"),new GBC(0,1).setFill(GBC.HORIZONTAL));
    to = new JTextField(20);
    to.setText("liusumantian@shexu.com");
    frame.add(to,new GBC(1,1).setFill(GBC.HORIZONTAL).setWeight(100,0));
    
    frame.add(new JLabel("SMTP server:"),new GBC(0,2).setFill(GBC.HORIZONTAL));
    smtpServer = new JTextField(20);
    smtpServer.setText("localhost");
    frame.add(smtpServer,new GBC(1,2).setFill(GBC.HORIZONTAL).setWeight(100,0));
    
    message = new JTextArea();
    message.setText("你好世界!!!");
    frame.add(new JScrollPane(message),new GBC(0,3,2,1).setFill(GBC.BOTH).setWeight(100,100));
    
    comm = new JTextArea();
    frame.add(new JScrollPane(comm),new GBC(0,4,2,1).setFill(GBC.BOTH).setWeight(100,100));
    
    JPanel buttonPanel = new JPanel();
    frame.add(buttonPanel,new GBC(0,5,2,1));
                                    
    JButton sendButton = new JButton("发送");
    buttonPanel.add(sendButton);
    sendButton.addActionListener(new ActionListener(){
      public void actionPerformed(ActionEvent e){
        new Thread(new Runnable(){
          public void run(){
            comm.setText("");
            sendMail();
          }
        }).start();
      }
    });
    frame.validate();
  }
  
  public void sendMail(){
    Socket server = null;
    try{
      server = new Socket(smtpServer.getText().trim(),25);
      InputStream ins = server.getInputStream();
      OutputStream outs = server.getOutputStream();
      in = new Scanner(ins);
      out = new PrintWriter(outs);
      String hostName = InetAddress.getLocalHost().getHostName();
      // 发送邮件过程
      receive();
      send("HELO " + hostName);
      receive();
      send("MAIL FROM:<" + from.getText().trim() + ">");
      receive();
      send("RCPT TO:<" + to.getText().trim() + ">");
      receive();
      send("DATA");
      receive();
      send(message.getText());
      send(".");
      receive();
      send("QUIT");
      receive();
    } catch(IOException e){
      e.printStackTrace();
      comm.append("Error:" + e.getMessage());
    } finally{
      try {
        if(server != null){
          server.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
  
  
  public void sendMail(String to,String from,String message){
    Socket server = null;
    try{
      server = new Socket("localhost",25);
      InputStream ins = server.getInputStream();
      OutputStream outs = server.getOutputStream();
      in = new Scanner(ins);
      out = new PrintWriter(outs);
      String hostName = InetAddress.getLocalHost().getHostName();
      // 发送邮件过程
      receive();
      send("HELO " + hostName);
      receive();
      send("MAIL FROM:<" + from + ">");
      receive();
      send("RCPT TO:<" + to + ">");
      receive();
      send("DATA");
      receive();
      send(message);
      send(".");
      receive();
      send("QUIT");
      receive();
    } catch(IOException e){
      e.printStackTrace();
//      comm.append("Error:" + e.getMessage());
    } finally{
      try {
        if(server != null){
          server.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
  /**
   * 向服务端发送信息
   * @param str
   */
  private void send(String str) {
    logger.log("MailSender","Info","发送信息:" + str);
//    comm.append(str);
//    comm.append("\n");
    out.print(str.replaceAll("\n","\r\n") + "\r\n");
//    out.print("\r\n");
    out.flush();
  }
  
  /**
   * 从服务端收到信息并显示
   */
  private void receive() {
    if(in.hasNextLine()){
      String line = in.nextLine();
      logger.log("MailSender","Info","服务器信息:" + line);
//      comm.append(line);
//      comm.append("\n");
    }    
  }

  public static void main(String[] args) {
    new MailSender(true);
    
  }

}
(2)邮件接收端源代码
package mailoper;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;

/**
 * 收信端服务器
 * @author shx
 *
 */
public class MailReceiver {
  private ServerSocket server;
  private Socket socket;
  private PrintWriter pw;
  private BufferedReader reader;
  private int count;
  private Logger logger;
  private PrintWriter ps;
  private  PrintWriter out;
  private SimpleDateFormat df;
  private Date date;
  
  public void receiveMail(){
    df = new SimpleDateFormat("yyyy年MM月dd日hh时mm分ss秒");
    date = new Date();
    String str = df.format(date);
    int cot = 0;
    Properties pr = null;
    try {
      FileInputStream count = new FileInputStream("letter\\count.data");
      pr = new Properties();
      pr.load(count);
      String temp = pr.getProperty("count");
      System.out.println("Befor cot++:" + temp);
      cot = Integer.parseInt(temp) + 1;
      String fileName = "letter\\letter" + cot + ".data" ;
      
      ps = new PrintWriter(new FileOutputStream("ServerLog.txt",true),true);
      out = new PrintWriter(new FileOutputStream(fileName,true),true);
      
    } catch (FileNotFoundException e1) {
      e1.printStackTrace();
    } catch(IOException e){
      e.printStackTrace();
    }
    
    logger = new Logger(ps);
    logger.log("MailReceiver","Info","开始记录日志...");
    
    
    try {
      server = new ServerSocket(25);
      // 写日志文件
      logger.log("MailReceiver","Info","服务器启动等待连接......");
      System.out.println("服务器启动等待连接......");
      // 启动服务
      socket = server.accept();
      // 写日志文件
      out.println("时间:" +str.substring(0,14) + " 收到信件");
      logger.log("MailReceiver","Info","收到主机" + socket.getInetAddress() + socket.getInetAddress().getHostName() +"的连接请求!");
      System.out.println("收到主机" + socket.getInetAddress() + socket.getInetAddress().getHostName() +"的连接请求!");
      
      pr.setProperty("count",new Integer(cot).toString());
      pr.store(new FileOutputStream("letter\\count.data"),"count");
      
      pw = new PrintWriter(socket.getOutputStream());
      reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
      
      send("220 smtp.shexu.com SMTP CSUSOFTMAIL (Postfix Rules!)" + "\t\n");
      receive();
      send("250 OK smtp.shexu.com" + "\t\n");
      receive();
      send("250 OK Sender" + "\t\n");
      receive();
      send("250 OK Receiver" + "\t\n");
      receive();
      send("354 Start mail input; end with <CRLF>.<CRLF>" + "\t\n");
      receiveData();// 接收Date
//      receive();
      send("250 OK" + "\t\n");
      receive();
      send("221 smtp.shexu.com Service closing transmission channel !GoodBye...");
    } catch (IOException e) {
      e.printStackTrace();
    } finally{
      if(server != null){
        try {
          server.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
      if(socket != null){
        try {
          socket.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }
  
  public void receive(){
    count++;
    String str = null;
    try {
      if(!(str = reader.readLine()).trim().equals("")){
        System.out.println(count + "收到客户端信息:" + str);
        logger.log("MailReceiver","Info","收到客户端信息:" + str);
      }
      
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  
  /**
   * 读取正文并保存到文件
   */
  public void receiveData(){
    char[] temp = new char[512];
    String str = null;
    int count = 0; 
    try {
      while(!(str = reader.readLine()).trim().equals(".")){
        count++;
        System.out.println("收到客户端Date信息:" + str);
        logger.log("MailReceiver","Info","收到客户端信息:" + str.trim());
        // 消除开头换行
        if(count >= 4 && count <=11){
          
        }else {
          out.println(str);
        }
        
      }
      System.out.println("收到客户端Date信息:" + str);
      logger.log("MailReceiver","Info","收到客户端信息:" + str.trim());
    } catch (IOException e) {
      e.printStackTrace();
    }
    
  }
  
  public void send(String str){
    logger.log("MailReceiver","Info","服务器发送信息:" + str.trim());
    pw.print(str);
    pw.flush();
  }
  
  public static void main(String[] args) {
    MailReceiver mr = new MailReceiver();
    while(true){
      mr.receiveMail();
    }
  }

}
(3)邮件阅读源代码
package mailoper;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;

public class ReadLetter {

  public int getCount() {
    int count = 0;
    try {
      FileInputStream in = new FileInputStream("letter\\count.data");
      Properties pr = new Properties();
      pr.load(in);
      count = Integer.parseInt(pr.getProperty("count"));
    } catch (IOException e) {
      e.printStackTrace();
    }
    return count;
  }

  public String readLetter(String file) {
    String str = "";
    try {
      int temp = getCount();
      for (int i = 1; i <= temp; i++) {
        String fileName = "MailSender\\letter\\letter" + i + ".data";
        FileInputStream in = new FileInputStream(fileName);
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        System.out.println("************************");
        String tem = null;
        str = str + "信件" + i + ":<br>";
        int count = 0;
        while ((tem = reader.readLine()) != null) {
          count++;
          str = str + tem + "<br>";
          
          System.out.println(tem.trim());
        }
        str = str + "<br>";
      }

    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return str;
  }

  public static void main(String[] args) {
    new ReadLetter().readLetter();
  }

}

11.2 实验内容
1.实验目的
学习电子邮件系统的工作原理,深入理解SMTP协议,熟悉多媒体邮件格式和Base64编码,掌握简化的SMTP邮件服务器的设计与实现方法。
2.实验工具
硬件:小型局域网(PC机不少于4台)。
软件:windows 2000操作系统、Eclipse3.1+MyEclipse4.1+tomcat5.5编程环境、Java编程语言。3.实验要求
编写简化的SMTP邮件服务器,实现邮件消息在邮件服务器之间的传输;提供用户远程登陆邮件服务器撰写、发送、阅读、回复、转发、删除邮件等功能。
4.实验指导
(1) 熟练掌握实验工具部分所列出的软件工具。
(2) 仔细阅读电子邮件系统工作原理部分,勾画出实现邮件服务器的各组成部分的总体结构图。图11.4是一个示例。

图11.4 一种电子邮件服务器组成及交互关系
(3) 熟悉动态交互页面的编写技术,例如JSP。编写基于Web的用户代理,实现邮箱注册,撰写、发送、阅读、回复、转发、删除邮件等功能。
(4) 熟悉SMTP协议规范。编写SMTP客户端,实现定期扫描待发送邮件队列、主动发起与SMTP服务器端的TCP连接、发送邮件消息等功能。
(5) 编写SMTP服务器端,实现对SMTP客户端发送邮件请求的响应、接收邮件消息、根据用户邮箱名将邮件存入用户邮箱。
(6) 可考虑将邮件系统划分为如下功能模块:a)登陆页面:实现用户从浏览器登陆个人邮箱时的身份验证,如对输入的用户名和密码进行验证;b)邮件发送页面:实现用户填写信件内容并发送的功能;c)邮件接收页面:实现用户对所有信件的接收、查询、回复、转发、删除等功能;d)邮件阅读页面:以页面形式显示指定邮件内容,实现用户对邮件内容的查看。
5.分析与思考
(1) 观察SMTP协议的通信过程。从Alice方的邮件服务器发送一封邮件给Bob,验证Bob是否正确收到邮件?
(2) 采用什么数据结构构造待发送邮件队列和用户邮箱? 并说明采用某种数据结构的理由。
(3) 若要实现对多媒体邮件传输的支持,SMTP客户端和服务器端需要增加什么功能? 如何实现这些功能?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345