本文先容了如何利用Java的网络API来实现一个电子邮件工具程序。通常Email工具都是应用SMTP(简略邮件传输协议, Simple Mail Transfer Protocol)来发送邮件,应用POP3协议来吸收电子邮件。在本文中只对这两个协议作简略先容。假如有爱好的读者可以参考以下站点:
POP3: ftp://ftp.isi.edu/in-notes/rfc1939.txt
SMTP: ftp://ftp.isi.edu/in-notes/rfc2821.txt
Java中固然供给了JavaMail API,但是由于在这篇文章中我将从底层来探讨电子邮件软件是如何工作的,因此不会应用JavaMail API。本文中的例子是在J2SE 1.4下开发的。
电子邮件的格式
在开发Email软件之前,你需要懂得电子邮件的格式。根据RFC 2882(http://www.faqs.org/rfcs/rfc2822.html)的规定,电子邮件由很多行组成,每行由〈CRLF〉(ASCII代码13和ASCII代码10)结束。每行的最大长度为998个字符。其中有些行供给了收发电子邮件所必须的信息,这些行被称为头(Header),所有的头构成了头域(Header Field)。其他的行用于保存邮件的具体内容。
头域供给了很多信息,其中包含邮件的起源;邮件的目标地和邮件的主题等。每个头由名称和冒号加上相应的值构成。例如From:、Send:和Reply-To:中记载了邮件的起源。在From:中记载的是邮件的作者;在Sender:中指定了发送邮件的代理(可以是邮件地址,也可以是机器名称);Reply-To:中指定了吸收回信的邮箱地址。
一封邮件可能有多个作者,因此From:中可以指定一个或多个邮箱地址。下面给出了一个个From:的例子:
From: Ray Feng 〈〉,
在一封电子邮件中只能有一个Sender。因此Sender:的值只能包含一个邮箱地址。假如在From:中只有一个作者,而且Sender:的值和From:的值雷同,则Sender:就不会涌现在电子邮件中,否则会涌现信息冗余;反之Sender:则应当涌现在邮件中。下面是一个Sender:的例子:
Sender: Ray Feng
在电子邮件中可以指定将回信发送到多个邮箱地址中。因此Reply-To:中可以包含一个或多个邮箱地址,每个地址之间用逗号隔开。假如邮件中有Reply-To:,回信会被发送到罗列在Reply-To:中的所有地址;假如邮件中没有Reply-To:,则回信会被发送到罗列在From:中的地址。那么谁会收到邮件呢?To:和Cc:中保存了吸收邮件的邮箱地址。两者的值都可以包含多个邮箱地址。
除了邮件的起源和吸收者,RFC 2882中还定义了其他一些头,例如Subject:中包含了电子邮件的主题。下面是一个电子邮件头域的例子:
From: Ray Feng 〈〉
To: bogus 〈〉
Cc: John 〈〉
Subject: Test Email
附件
在MIME中答应在电子邮件中添加二进制文件,被添加的文件叫做附件。附件的内容可以作为邮件的一部分进行传输。MIME是假如实现这个功效的呢?在MIME中引进了很多头,其中和附件相干的最重要的就是Content-Type:和Content-Tracnsfer-Encoding:。为了在一封电子邮件中区分不同的部分,MIME请求在Content-Type: multipart/mixed头中包含一个边界参数。边界参数的值是一个在双引号中的字符串。通过这个字符串,程序就可以区分电子邮件的不同部分。在传输电子邮件的内容前,程序先传输一个〈CRLF〉,两个连字符和边界参数。当完成Email内容的传输后,程序会在最后传输边界参数和两个连字符。
下面的电子邮件中包含了两个部分,一个部分是由iso-8859-1字符组成的文本,一部分是名为file.txt的附件。这里没有包含Content-Transfer-Encoding:头,表明应用缺省的7位ASCII字符。
Content-Type: multipart/mixed; boundary="***"
--***
Content-Type: text/plain; charset="iso-8859-1"
This message has an attachment.
--***
Content-Type: text/plain; name="file.txt"
Attachment text.
--***--
发送电子邮件
screen.width-333)this.width=screen.width-333;">
基于互联网的电子邮件通常是利用SMTP网络协议进行传输的。根据SMTP,当电子邮件程序需要发送电子邮件时,该程序首先同一个SMTP服务程序建立起双向的通信通道(通常是通过套接字建立这种通道的)。这个基础的SMTP服务程序或许是这份电子邮件的终极目标地,也可能只是通向另一个SMTP服务程序的跳板。总而言之,当电子邮件程序同SMTP服务程序建立起双相的传输通道后,电子邮件程序会向SMTP服务程序发送一系列基于ASCII字符的命令,而SMTP服务程序会对这些命令产生相应的回应来表明相应的把持是成功还是失败了。
让我们假设所有的把持都成功了,那么电子邮件程序将把邮件发送到SMTP服务程序,假如电子邮件的吸收地址正好是该SMTP服务程序运行的服务器,那么SMTP服务程序就会将邮件加进邮件数据库中,否则SMTP服务程序将把邮件转发到在其他SMTP服务器上的SMTP服务程序,直到达到目标地为止。图二通过图示阐明了这一点。
SMTP可以辨认很多电子邮件用来与SMTP服务程序通信的命令。某些命令需要参数,某些命令则不需要。但是每个命令后必须跟一个〈CRLF〉。最常用的六个命令是HELO,MAIL,RCPT,DATA,RSET和QUIT。
按照上面的次序给出这六个命令并非偶然。除了RSET外,其他的命令必须按照特定的次序发送,这是由于SMTP服务程序是基于状态的。对于每一个建立了双向通信通道的电子邮件程序,SMTP服务程序都会保存当前的通信状态。
当一个电子邮件程序和SMTP服务程序建立接洽后,SMTP服务程序将向电子邮件程序发送初始化消息。该消息包含了一个三位回应码,这个回应码是用来标识SMTP服务程序的。除此之外,在SMTP服务程序发送给电子邮件程序的消息的头部也带有回应码,它们被用来表现把持成功或者失败。电子邮件程序吸收到这些回应码后,可以根据其中包含的信息完成相应的工作。而消息的文本部分是给人看的,电子邮件程序可以疏忽文本部分。
在收到初始化消息后,电子邮件程序通过发送HELO命令来开端传输邮件。HELO命令有一个参数,该参数标记了SMTP服务程序所在服务器的域名。它将在SMTP服务程序中标识出SMTP服务程序。作为回应,SMTP服务程序进行一些初始化工作,将自己设定到初始状态以吸收电子邮件。当这些工作成功完成后,它发送回一条成功的回应消息给电子邮件程序,该回应消息以回应码250开头。
在HELO命令之后,电子邮件程序会发送MAIL命令。MAIL命令将在SMTP服务程序中标识出发送者,它有两个参数:FROM:和一个电子邮件地址。假如SMTP服务程序能够成功地解析电子邮件地址的话,通常它将返回以250开头的回应消息;否者将发送回表现把持失败的回应消息。
在MAIL之后是RCPT命令。RCPT命令在SMTP服务程序中标识出一个邮件的吸收者,它也有两个参数:TO:和一个电子邮件地址。假如邮件由多个吸收者,则程序需要多次发送RCPT命令。
RCPT命令之后,程序需要发送电子邮件本身了。程序先发送一个DATA命令,当吸收到表现成功的回应消息后,将电子邮件逐行发送给SMTP服务程序,当所有的行都发送完毕后,程序发送一行由句号组成的行。在此之后,电子邮件程序等候SMTP服务程序的回应消息,以断定邮件被SMTP服务程序正常吸收了。这一切都成功后,程序可以发送RSET命令来退出邮件传输过程。最后,当要断开和SMPT服务程序建立的连接时,程序发送QUIT命令。重要提示的一点是,固然上面的命令都是大写的,但是在实际的协议对大小写不敏感。
现在也许你关心的标题是回应码的格式是怎样的。最左边的一位数字代表把持是否成功,1代表收到命令,2代表把持成功完成,3代表等候后续命令,4代表把持临时未能完成(电子邮件程序可以在当前的邮件传输过程中重新发送命令),5代表把持不能完成(电子邮件程序不能在当前的邮件传输过程中重新发送命令)。第二位数字代表回应的领域,0代表语法毛病,1代表消息恳求,2代表传输通道,3和4没有指定,5代表与邮件系统相干。最有一位数字对第二位数字做补充阐明,这里就不再详述。根据上面的信息,我们可以看出250代表恳求的命令已经成功完成;220代表SMTP服务程序正在等候HELO命令;而503代表命令次序毛病。有爱好的朋友可以参见RFC 2821。
下面供给了一个基于命令行的例子SMTPDemo,这个例子可以赞助你懂得基于SMTP的邮件传输机制。这个程序将利用标准端口25连接到一个SMTP服务程序上。为了使程序能够运行,你需要将home更改为你应用的邮件服务器的地址。
// SMTPDemo.java
import java.io.*;
import java.net.*;
class SMTPDemo
{
public static void main (String [] args)
{
String SMTPServer = "home
int SMTPPort = 25;
Socket client = null;
try
{
// 向SMTP服务程序建立一个套接字连接。
client = new Socket (SMTPServer, SMTPPort);
// 创立一个BufferedReader对象,以便从命令行读取用户输进。
BufferedReader stdin;
stdin = new BufferedReader (new InputStreamReader (System.in));
// 创立一个BufferedReader对象,以便从套接字读取输出。
InputStream is = client.getInputStream ();
BufferedReader sockin;
sockin = new BufferedReader (new InputStreamReader (is));
// 创立一个PrintWriter对象,以便向套接字写进内容。
OutputStream os = client.getOutputStream ();
PrintWriter sockout;
sockout = new PrintWriter (os, true);
// 显示同SMTP服务程序的握手过程。
System.out.println ("S:" + sockin.readLine ());
while (true)
{
System.out.print ("C:");
// 读取用户输进。
String cmd = stdin.readLine ();
// 将用户输进的命令发送到SMTP服务程序。
sockout.println (cmd);
// 从套接字读取SMTP服务程序的回应消息并显示在屏幕上。
String reply = sockin.readLine ();
System.out.println ("S:" + reply);
// 假如发送了DATA命令并且获得成功的回应消息,从输进设备读取行,
// 直到读取到完整由句号组成的行时结束, 这些行构成了电子邮件。
if (cmd.toLowerCase ().startsWith ("data") &&
reply.substring (0, 3).equals ("354"))
{
do
{
cmd = stdin.readLine ();
if (cmd != null && cmd.length () 〉 1 &&
cmd.charAt (0) == ´.´)
cmd = ".";
sockout.println (cmd);
if (cmd.equals ("."))
break;
}
while (true);
// 从SMTP服务程序中读取回应消息并显示。
reply = sockin.readLine ();
System.out.println ("S:" + reply);
continue;
}
// 假如用户输进QUIT命令,则退出程序。
if (cmd.toLowerCase ().startsWith ("quit"))
break;
}
}
catch (IOException e)
{
System.out.println (e.toString ());
}
finally
{
try
{
if (client != null)
client.close ();
}
catch (IOException e)
{
}
}
}
}
当运行SMTPDemo时,你将会看到下面的输出。其中C:后面是用户的输进,S:后面是SMTP服务程序返回的信息。
S:220 home.digital.com Microsoft ESMTP MAIL Service, Version: 4.0.2195.2966 ready
at Fri, 13 Dec 2002 15:06:58 +0800
当运行SMTPDemo后,邮件服务程序返回了初始化信息。
C:helo digital.com
S:250 home.digital.com Hello [23.2.254.53]
通过发送helo digital.com命令开端邮件传输过程。digital.com是邮件服务器所在域的域名。然后邮件服务程序返回了以250开头的欢迎信息。
C:mail from:
S:250 2.1.0 ....Sender OK
接下来是输进邮件发送者的信息mail from:。邮件服务程序返回了成功信息。
C:rcpt to:
S:250 2.1.5
然后是通过rcpt to:指定邮件的吸收者。
C:data
S:354 Start mail input; end with 〈CRLF〉.〈CRLF〉
Subject: Test Email
This is the test Email.
.
S:250 2.6.0 Queued mail for delivery
接下来是输进邮件的内容。发送DATA命令后,等候服务器发送回命令被成功吸收的回应消息。当吸收到以354开头的回应消息时,就可以输进电子邮件的内容了。完成后以〈CRLF〉.〈CRLF〉结束。
C:quit
S:221 2.0.0 home.digital.com closing connection
最后退出发送电子邮件的过程。请留心回应码221,最左边的2代表把持成功,中间的2表现传输通道,1表现连接封闭。
前面我曾讨论过关于附件的标题。通过SMTPDemo也可以发送附件。通过向邮件服务程序发送下面的命令,就可以在邮件中加进file.txt作为附件。
helo digital.net
mail from:
rcpt to:
data
Subject: Attachment Demo
Content-Type: multipart/mixed; boundary="***"
--***
Content-Type: text/plain; charset="iso-8859-1"
This message has an attachment.
--***
Content-Type: text/plain; name="file.txt"
Attachment text.
--***--
quit
到此为止,我们先容了如何用Java实现Email工具的发送功效,并从地层分析了邮件发送的机制,不知您是否已经把握了这些内容。在下一篇文章中,我们将一起来研究Email工具的吸收功效。