本文介紹了如何利用Java的網(wǎng)絡API來實現(xiàn)一個電子郵件工具程序。通常Email工具都是使用SMTP(簡單郵件傳輸協(xié)議, Simple Mail Transfer Protocol)來發(fā)送郵件,使用POP3協(xié)議來接受電子郵件。在本文中只對這兩個協(xié)議作簡單介紹。如果有興趣的讀者可以參考以下站點:
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下開發(fā)的。
電子郵件的格式
在開發(fā)Email軟件之前,你需要了解電子郵件的格式。根據(jù)RFC 2882(http://www.faqs.org/rfcs/rfc2822.html)的規(guī)定,電子郵件由很多行組成,每行由(ASCII代碼13和ASCII代碼10)結(jié)束。每行的最大長度為998個字符。其中有些行提供了收發(fā)電子郵件所必需的信息,這些行被稱為頭(Header),所有的頭構(gòu)成了頭域(Header Field)。其他的行用于保存郵件的具體內(nèi)容。
頭域提供了很多信息,其中包括郵件的來源;郵件的目的地和郵件的主題等。每個頭由名稱和冒號加上相應的值構(gòu)成。例如From:、Send:和Reply-To:中記錄了郵件的來源。在From:中記錄的是郵件的作者;在Sender:中指定了發(fā)送郵件的代理(可以是郵件地址,也可以是機器名稱);Reply-To:中指定了接受回信的郵箱地址。
一封郵件可能有多個作者,因此From:中可以指定一個或多個郵箱地址。下面給出了一個個From:的例子:
From: Ray Feng , bogus@yahoo.com.cn
在一封電子郵件中只能有一個Sender。因此Sender:的值只能包含一個郵箱地址。如果在From:中只有一個作者,而且Sender:的值和From:的值相同,則Sender:就不會出現(xiàn)在電子郵件中,否則會出現(xiàn)信息冗余;反之Sender:則應該出現(xiàn)在郵件中。下面是一個Sender:的例子:
Sender: Ray Feng rayfeng@yahoo.com.cn
在電子郵件中可以指定將回信發(fā)送到多個郵箱地址中。因此Reply-To:中可以包含一個或多個郵箱地址,每個地址之間用逗號隔開。如果郵件中有Reply-To:,回信會被發(fā)送到羅列在Reply-To:中的所有地址;如果郵件中沒有Reply-To:,則回信會被發(fā)送到羅列在From:中的地址。那么誰會收到郵件呢?To:和Cc:中保存了接受郵件的郵箱地址。兩者的值都可以包含多個郵箱地址。
除了郵件的來源和接受者,RFC 2882中還定義了其他一些頭,例如Subject:中包含了電子郵件的主題。下面是一個電子郵件頭域的例子:
From: Ray Feng
To: bogus
Cc: John
Subject: Test Email
附件
在MIME中允許在電子郵件中添加二進制文件,被添加的文件叫做附件。附件的內(nèi)容可以作為郵件的一部分進行傳輸。MIME是如果實現(xiàn)這個功能的呢?在MIME中引入了很多頭,其中和附件相關的最重要的就是Content-Type:和Content-Tracnsfer-Encoding:。為了在一封電子郵件中區(qū)分不同的部分,MIME要求在Content-Type: multipart/mixed頭中包含一個邊界參數(shù)。邊界參數(shù)的值是一個在雙引號中的字符串。通過這個字符串,程序就可以區(qū)分電子郵件的不同部分。在傳輸電子郵件的內(nèi)容前,程序先傳輸一個,兩個連字符和邊界參數(shù)。當完成Email內(nèi)容的傳輸后,程序會在最后傳輸邊界參數(shù)和兩個連字符。
下面的電子郵件中包含了兩個部分,一個部分是由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.
--***--
發(fā)送電子郵件
基于互聯(lián)網(wǎng)的電子郵件通常是利用SMTP網(wǎng)絡協(xié)議進行傳輸?shù)?。根?jù)SMTP,當電子郵件程序需要發(fā)送電子郵件時,該程序首先同一個SMTP服務程序建立起雙向的通訊通道(通常是通過套接字建立這種通道的)。這個基本的SMTP服務程序或許是這份電子郵件的最終目的地,也可能只是通向另一個SMTP服務程序的跳板??偠灾?,當電子郵件程序同SMTP服務程序建立起雙相的傳輸通道后,電子郵件程序會向SMTP服務程序發(fā)送一系列基于ASCII字符的命令,而SMTP服務程序會對這些命令產(chǎn)生相應的回應來表明相應的操作是成功還是失敗了。
讓我們假設所有的操作都成功了,那么電子郵件程序?qū)燕]件發(fā)送到SMTP服務程序,如果電子郵件的接收地址正好是該SMTP服務程序運行的服務器,那么SMTP服務程序就會將郵件加入郵件
數(shù)據(jù)庫中,否則SMTP服務程序?qū)燕]件轉(zhuǎn)發(fā)到在其他SMTP服務器上的SMTP服務程序,直到到達目的地為止。圖二通過圖示說明了這一點。
SMTP可以識別很多電子郵件用來與SMTP服務程序通訊的命令。某些命令需要參數(shù),某些命令則不需要。但是每個命令后必須跟一個。最常用的六個命令是HELO,MAIL,RCPT,DATA,RSET和QUIT。
按照上面的順序給出這六個命令并非偶然。除了RSET外,其他的命令必須按照特定的順序發(fā)送,這是因為SMTP服務程序是基于狀態(tài)的。對于每一個建立了雙向通訊通道的電子郵件程序,SMTP服務程序都會保存當前的通訊狀態(tài)。
當一個電子郵件程序和SMTP服務程序建立聯(lián)系后,SMTP服務程序?qū)⑾螂娮余]件程序發(fā)送初始化消息。該消息包含了一個三位回應碼,這個回應碼是用來標識SMTP服務程序的。除此之外,在SMTP服務程序發(fā)送給電子郵件程序的消息的頭部也帶有回應碼,它們被用來表示操作成功或者失敗。電子郵件程序接收到這些回應碼后,可以根據(jù)其中包含的信息完成相應的工作。而消息的文本部分是給人看的,電子郵件程序可以忽略文本部分。
在收到初始化消息后,電子郵件程序通過發(fā)送HELO命令來開始傳輸郵件。HELO命令有一個參數(shù),該參數(shù)標志了SMTP服務程序所在服務器的域名。它將在SMTP服務程序中標識出SMTP服務程序。作為回應,SMTP服務程序進行一些初始化工作,將自己設定到初始狀態(tài)以接收電子郵件。當這些工作成功完成后,它發(fā)送回一條成功的回應消息給電子郵件程序,該回應消息以回應碼250開頭。
在HELO命令之后,電子郵件程序會發(fā)送MAIL命令。MAIL命令將在SMTP服務程序中標識出發(fā)送者,它有兩個參數(shù):FROM:和一個電子郵件地址。如果SMTP服務程序能夠成功地解析電子郵件地址的話,通常它將返回以250開頭的回應消息;否者將發(fā)送回表示操作失敗的回應消息。
在MAIL之后是RCPT命令。RCPT命令在SMTP服務程序中標識出一個郵件的接收者,它也有兩個參數(shù):TO:和一個電子郵件地址。如果郵件由多個接收者,則程序需要多次發(fā)送RCPT命令。
RCPT命令之后,程序需要發(fā)送電子郵件本身了。程序先發(fā)送一個DATA命令,當接收到表示成功的回應消息后,將電子郵件逐行發(fā)送給SMTP服務程序,當所有的行都發(fā)送完畢后,程序發(fā)送一行由句號組成的行。在此之后,電子郵件程序等待SMTP服務程序的回應消息,以確定郵件被SMTP服務程序正常接收了。這一切都成功后,程序可以發(fā)送RSET命令來退出郵件傳輸過程。最后,當要斷開和SMPT服務程序建立的連接時,程序發(fā)送QUIT命令。主要提醒的一點是,雖然上面的命令都是大寫的,但是在實際的協(xié)議對大小寫不敏感。
現(xiàn)在也許你關心的問題是回應碼的格式是怎樣的。最左邊的一位數(shù)字代表操作是否成功,1代表收到命令,2代表操作成功完成,3代表等待后續(xù)命令,4代表操作臨時未能完成(電子郵件程序可以在當前的郵件傳輸過程中重新發(fā)送命令),5代表操作不能完成(電子郵件程序不能在當前的郵件傳輸過程中重新發(fā)送命令)。第二位數(shù)字代表回應的領域,0代表語法錯誤,1代表消息請求,2代表傳輸通道,3和4沒有指定,5代表與郵件系統(tǒng)相關。最有一位數(shù)字對第二位數(shù)字做補充說明,這里就不再詳述。根據(jù)上面的信息,我們可以看出250代表請求的命令已經(jīng)成功完成;220代表SMTP服務程序正在等待HELO命令;而503代表命令順序錯誤。有興趣的朋友可以參見RFC 2821。
下面提供了一個基于命令行的例子SMTPDemo,這個例子可以幫助你理解基于SMTP的郵件傳輸機制。這個程序?qū)⒗脴藴识丝?5連接到一個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);
// 創(chuàng)建一個BufferedReader對象,以便從命令行讀取用戶輸入。
BufferedReader stdin;
stdin = new BufferedReader (new InputStreamReader (System.in));
// 創(chuàng)建一個BufferedReader對象,以便從套接字讀取輸出。
InputStream is = client.getInputStream ();
BufferedReader sockin;
sockin = new BufferedReader (new InputStreamReader (is));
// 創(chuàng)建一個PrintWriter對象,以便向套接字寫入內(nèi)容。
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 ();
// 將用戶輸入的命令發(fā)送到SMTP服務程序。
sockout.println (cmd);
// 從套接字讀取SMTP服務程序的回應消息并顯示在屏幕上。
String reply = sockin.readLine ();
System.out.println ("S:" + reply);
// 如果發(fā)送了DATA命令并且獲得成功的回應消息,從輸入設備讀取行,
// 直到讀取到完全由句號組成的行時停止, 這些行構(gòu)成了電子郵件。
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]
通過發(fā)送helo digital.com命令開始郵件傳輸過程。digital.com是郵件服務器所在域的域名。然后郵件服務程序返回了以250開頭的歡迎信息。
C:mail from: rayfeng@digital.com
S:250 2.1.0 rayfeng@digital.com....Sender OK
接下來是輸入郵件發(fā)送者的信息mail from:。郵件服務程序返回了成功信息。
C:rcpt to: rayfeng@digital.com
S:250 2.1.5 rayfeng@digital.com
然后是通過rcpt to:指定郵件的接收者。
C:data
S:354 Start mail input; end with .
Subject: Test Email
This is the test Email.
.
S:250 2.6.0 HOMEOulkEZ00VNuHKDy00000002@home.digital.com Queued mail for delivery
接下來是輸入郵件的內(nèi)容。發(fā)送DATA命令后,等待服務器發(fā)送回命令被成功接收的回應消息。當接收到以354開頭的回應消息時,就可以輸入電子郵件的內(nèi)容了。完成后以.結(jié)束。
C:quit
S:221 2.0.0 home.digital.com closing connection
最后退出發(fā)送電子郵件的過程。請注意回應碼221,最左邊的2代表操作成功,中間的2表示傳輸通道,1表示連接關閉。
前面我曾討論過關于附件的問題。通過SMTPDemo也可以發(fā)送附件。通過向郵件服務程序發(fā)送下面的命令,就可以在郵件中加入file.txt作為附件。
helo digital.net
mail from: rayfeng@digital.com
rcpt to: rayfeng@digital.com
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實現(xiàn)Email工具的發(fā)送功能,并從地層分析了郵件發(fā)送的機制,不知您是否已經(jīng)掌握了這些內(nèi)容。在下一篇文章中,我們將一起來研究Email工具的接收功能。