企业级邮件服务器Apache James介绍(1)

12/3/2006来源:其它邮件服务器软件人气:10392

英文原文:http://www-106.ibm.com/developerworks/java/library/j-james1.html
学习这个开源项目的基础知识

级别: 中级
Claude Duguay ([email protected])
首席架构师, Arcessa, Inc.
6,10, 2003

Java Apache企业级邮件服务器 -- 通常被称为James --是Apache组织构建的一个可移植的,安全的,100% 纯 Java 实现的企业级邮件服务器。但James有潜力成为功能更强的应用服务器,这得益于它的组件式体系结构和mailet基础设施。mailet对e-mail所起的作用与 servlets对Web服务器的作用相似。E-mail 服务器在最终发展成为Internet的DARPA出现的早期就已经很普遍了。但 James 为这个经常被称为Internet最关键的应用提供了新的可能性。
这是探索James的两篇文章中的第一篇. 这篇文章提供了James的概述和探讨它的可能性的指导性方向。在第二篇文章中,我们会实现一个mailet应用程序来管理不能发送(unavailability)的消息。你将会发现,为James写应用程序简直简单的令人难以置信。在这个世界上每天都有几百万人在用 E-mail ,因此其应用的可能性也远远超出了这个系列所介绍的处理方法。无论如何,我都希望这篇文章可以作为一个基础为你服务,帮你开始想像各种可能性。
E-mail 如何工作
原则上来讲,E-mail是简单的。你可以用一个邮件用户代理(mail user agent-MUA)创建带有一个或几个接收者地址的消息。有很多种形式的 MUAs 可供选择,包括基于文本的、基于Web的、还有GUI应用程序。Microsoft Outlook 和 Netscape Messenger 属于最后一种。每个e-mail 客户端都被配置为向一个邮件传输代理(mail transfer agent --MTA)发送邮件和从一个MTA获取发给某个用户地址的e-mail消息。要想这样做, 你需要在邮件服务器(技术上讲,是MTA)上有一个e-mail 账号 ,并且你能够使用标准的Ineternet协议,无论是脱线处理 e-mail (用POP3)还是把 e-mail 留在服务器上(用IMAP)。在客户端和MTA之间以及MTA和MTA之间发送邮件的协议都是简单邮件传输协议(Simple Mail Transfer PRotocol-SMTP)。

Building a James application
这个系列的第二部分在你了解James基础设施的基础上实现一个实际的应用程序.

在MTA之间究竟发生了什么事情仅仅稍微有趣一点。 E-mail服务器在很大程度上依赖于DNS 和被称为邮件传输( mail transfer 或 MX)记录的e-mail-specific 记录。MX记录与用来解析URL的DNS记录稍有不同, 它还包含了一些额外的优先级信息来更高效的路由邮件。我不在这里深入研究这些细节 ,但明白DNS是成功有效的路由e-mail的关键很重要。 James 是一个 MTA, 同时JavaMail API 提供了一个 MUA的框架。在这篇文章里,我们将用JavaMail 来为我们的James 安装做一次测试。在这个系列的第二篇文章里,我们将用James Mailet API来说明如何开发你自己的James应用程序。
James 的设计目标
James的设计要满足特定的目标。例如,它完全用Java语言编码以满足最大化可移植能力的目标。它即提供了保护自身服务器环境的一些安全特性也提供了安全以实现安全的目标。 James 所具有的多线程应用程序的功能利用了很多Avalon框架提供的好处。 (Avalon 是一个Apache Jakarta 项目, 体现了Phoenix高性能服务器基础设施的一些特性。了解 Avalon对于开发James应用程序是有益但不必要的。请看 资源 部分深入学习Avalon.)
James提供了一组全面的服务,包括一些仅仅在高端的或很好的邮件服务器中才能得到的。这些服务主要是用Matcher 和 Mailet APIs实现的,它们合作提供e-mail的检测和处理能力。James支持标准的e-mail协议(SMTP, POP3, IMAP),和一些其他的协议。James采用松散耦合的嵌入式设计以保证消息框架与协议无关。这是一个很好的主意,将来可以让James像大多数消息服务器一样或者支持诸如即时消息那样的消息协议。
设计组最后提出的也是最有趣的目标是mailets的概念,它为开发e-mail应用程序提供了一个基于组件生命周期和容器的解决方案。毋庸置疑,总是有可能会用到其他的MTA,比如 Sendmail。如果你想调用其他的MTA,那么你应该能够调用任何程序,并且可以操作通过的数据。 但James提供了一个通用的、简单的API实现这些目标,并且使任务更容易完成。感谢James提供的那些我们可以操作的对象,我们会在这篇文章中深入研究Matcher 和 Mailet APIs。
安装并配置James
James可以从它在Apache基金会站点中的主页(参见资源 提供的链接).上获取,你应该下载最新发布的产品开始工作;在写这篇文章的时候是版本2.1.2。你可以在James主页左边的导航栏里选择 Downloads > Binaries 进入下载区。在下载区,找到Release Builds 部分并且选择James 2.1链接,根据你个人的喜好选择下载james-2.1.2.tar.gz 或者james-2.1.2.zip文件。
我们还要用JavaMail API测试我们的应用,所以你还需要下载它 (参见 资源)。现在能得到是1.3版本,文件名是 javamail-1_3.zip。当你到JavaMail的主页上时,你会注意到有到JavaBeans Activation Framework (JAF)的链接,它是JavaMail API 需要的。 (参见 资源 直接链接到JAF.) JAF 现在的发布版本是 1.0.2 ,文件名是jaf-1_0_2.zip。一旦你得到了所有这些文件,你就能够架起与James一起工作的系统了。
We'll set up a directory structure with all the elements we need for development.我们会为开发用到的所有元素设立一个目录结构。 在产品中,这种设立必须有所不同,文件的安排要综合考虑安全性和功能性两方面的问题。比如说,为了实现我们的目标,可以在本地主机上工作,但对于一个真正的e-mail服务器的部署工作来说,本地主机并不是一个可行的备选方案。如果你要为商业应用把James服务器部署为一个主MTA或与Sedmail一起工作,这里有大量的配置文档和提供帮助的邮件列表。
当所有文件都解压到James目录下后, 我们的目录层次结构将如列表1所示。我已经移走了 javadoc, src,和JavaMail webapp demo 目录下的一些子目录,以便我们的目录更紧凑和易于想像。
列表1. James, JavaMail, 和 JAF 目录
================================================================================
James
+---jaf-1.0.2
| +---demo
| \---docs
| \---javadocs
+---james-2.1.2
| +---apps
| +---bin
| | \---lib
| +---conf
| +---docs
| | +---images
| | \---stylesheets
| +---ext
| +---lib
| +---logs
\---javamail-1.3
+---demo
| +---client
| +---servlet
| \---webapp
+---docs
| \---javadocs
\---lib
===============================================================================
我假设你已经安装了独立于James文件的Java 平台的1.4版本。James配置文件声称在使用Java 1.3.0时已经发现了一些问题,所以你应该使用1.3.1或更高的版本。原则上来说,James应该可以在与Java 1.4 VM兼容的任何平台上很好的运转。
我们的第一步是启动James, 因为只有服务器被运行过一次以后配置文件才会被释放出来。你会在 james-2.1.2/bin目录下发现一个运行脚本 (根据你的操作系统使用run.bat 或 run.sh) 。当你运行这个脚本后,输出应该和列表 2相似 (这是在Windows系统下的输出样本):
列表2. James运行时的控制台输出

================================================================================
Using PHOENIX_HOME: D:\James\james-2.1.2
Using PHOENIX_TMPDIR: D:\James\james-2.1.2\temp
Using JAVA_HOME: c:\programming\java14

Phoenix 4.0.1

James 2.1.2
Remote Manager Service started plain:4555
POP3 Service started plain:110
SMTP Service started plain:25
NNTP Service started plain:119
Fetch POP Disabled
================================================================================
你可以按 Ctrl+C 退出程序, Phoenix 容器会发出一个消息告诉你它正在退出。严格的讲,正确的退出James的方法是使用远程管理界面。我在我的开发环境里已经用过Ctrl+C,没有发现负面的影响。但无论如何,在开发环境里总是应该使用shutdown命令。
在第一次关闭了James之后,你就会在james-2.1-2/apps/james/SAR-INF文件夹下面发现config.xml文件,你应该认真看一下这个文件。通常你应该做的第一件事是改变管理员账号,默认设置为root,密码是root。由于是进行开发,所以我们不管它,但很明显在生产系统中保留这种配置是很不明智的。接下来要改变的通常是DNS服务器的地址,如果James是完全作为一个e-mail服务器运转的话,这是非常必要的。既然我们所有的测试都是在本机上进行的,我们也不去管它。 但你应该意识到这是个很重要的设置。虽然理解配置文件很重要,但对于我们的开发目标来说,其余的默认设置都没有问题。你可以在 james-2.1.2/docs目录下得到更详细的信息。
让我们在开始工作之前先添加几个用户。首先用命令“telnet localhost 4555” telnet到本机的4555端口。可以用root这个用户名和密码登录,登录成功之后,我们就可以添加用户了。“adduser” 命令需要用户名和密码两个参数。在这个项目中我们会添加用户名为red、 green和 blue的三个用户,密码都和用户名相同。 (我相信你知道这在创建真实用户的时候不是一个好主意,但这样很容易配置我们的测试用例。) 添加用户之后,你可以用”listusers“ 命令验证输入是否正确,然后键入”quit“命令退出远程管理。整个会话如列表3所示,红色的文本表示你自己键入的命令。(可惜看不到红色了,:( )
列表3. 用远程管理添加用户
===============================================================================
JAMES Remote Administration Tool 2.1.2
Please enter your login and passWord
Login id:
root
Password:
root
Welcome root. HELP for a list of commands
adduser red red
User red added
adduser green green
User green added
adduser blue blue
User blue added
listusers
Existing accounts 3
user: blue
user: green
user: red
quit
Bye
================================================================================
现在我们和运行起来的James服务器一起大干一场了。如你所见,James的部署和用远程管理进行配置是相当简单的。很明显,如果想让邮件服务器安全的话你将需要改变几个配置参数,但那并不是一个很复杂的过程。 使用邮件服务器真正关键的是在多用户多服务环境中正确的配置DNS。这超出了本文的讨论范围,但它也不是很复杂的过程。
用JavaMail测试James
为了确保我们的安装是成功的, 我们将很快的编写能够发送消息和显示收件夹中的内容列表的两个类,用来模拟典型的e-mail客户端的基本功能。we'll write a quick pair of classes that will send messages and list inbox content, simulating the base functionality of a typical e-mail client. 我们用两个类是因为 MailClient 类,如列表4所示,能够重用以测试更复杂的行为,我们在开发自己的James应用程序时(本系列的第二篇文章)将会重用这个类。
列表4. MailClient: 模拟一个e-mail 客户端的基本功能
===========================================

import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;

public class MailClient
extends Authenticator
{
public static final int SHOW_MESSAGES = 1;
public static final int CLEAR_MESSAGES = 2;
public static final int SHOW_AND_CLEAR =
SHOW_MESSAGES + CLEAR_MESSAGES;

protected String from;
protected session session;
protected PasswordAuthentication authentication;

public MailClient(String user, String host)
{
this(user, host, false);
}

public MailClient(String user, String host, boolean debug)
{
from = user + '@' + host;
authentication = new PasswordAuthentication(user, user);
Properties props = new Properties();
props.put("mail.user", user);
props.put("mail.host", host);
props.put("mail.debug", debug ? "true" : "false");
props.put("mail.store.protocol", "pop3");
props.put("mail.transport.protocol", "smtp");
session = Session.getInstance(props, this);
}

public PasswordAuthentication getPasswordAuthentication()
{
return authentication;
}

public void sendMessage(
String to, String subject, String content)
throws MessagingException
{
System.out.println("SENDING message from " + from + " to " + to);
System.out.println();
MimeMessage msg = new MimeMessage(session);
msg.addRecipients(Message.RecipientType.TO, to);
msg.setSubject(subject);
msg.setText(content);
Transport.send(msg);
}

public void checkInbox(int mode)
throws MessagingException, IOException
{
if (mode == 0) return;
boolean show = (mode %26amp; SHOW_MESSAGES) > 0;
boolean clear = (mode %26amp; CLEAR_MESSAGES) > 0;
String action =
(show ? "Show" : "") +
(show %26amp;%26amp; clear ? " and " : "") +
(clear ? "Clear" : "");
System.out.println(action + " INBOX for " + from);
Store store = session.getStore();
store.connect();
Folder root = store.getDefaultFolder();
Folder inbox = root.getFolder("inbox");
inbox.open(Folder.READ_WRITE);
Message[] msgs = inbox.getMessages();
if (msgs.length == 0 %26amp;%26amp; show)
{
System.out.println("No messages in inbox");
}
for (int i = 0; i < msgs.length; i++)
{
MimeMessage msg = (MimeMessage)msgs[i];
if (show)
{
System.out.println(" From: " + msg.getFrom()[0]);
System.out.println(" Subject: " + msg.getSubject());
System.out.println(" Content: " + msg.getContent());
}
if (clear)
{
msg.setFlag(Flags.Flag.DELETED, true);
}
}
inbox.close(true);
store.close();
System.out.println();
}
}
===========================================
设计MailClient的主要目的是让我们发送消息,显示或者删除给定用户在服务器上的消息列表。我已经声明了一些有用的常量,让我们 显示消息SHOW_MESSAGES,清除消息 CLEAR_MESSAGES, 或者执行这两种操作。 MailClient类还实现了 Authenticator接口,以便在检索邮件时的在线处理更容易管理。
我创建了两个构造函数,其中一个显式的设置JavaMail的调试标志。在调试状态下,程序会把客户端/服务器的交互协议输出到控制台,这样就能看到发生了什么。 短的构造方法不打开调试标志,另外两个参数是用户名和主机,e-mail地址可以由用户名和主机得到。我们创建了一个在Authenticator接口中定义的 PasswordAuthentication 对象,这个对象可以由getPasswordAuthentication()方法获得。
构造函数的其余代码创建了JavaMail properties来保存所说明的用户名和主机,并且显式的说明了我们要用到的协议。一旦我们将这些值存入Properties对象,我们就能调用静态的Session 方法 getInstance() 来得到一个有效的Session 引用, 然后将它保存在局部变量中 。一旦构造函数被用于指定的用户,我们就准备好在指定的e-mail主机上发送或检索那个用户的e-mail。
sendMessage() 方法同样很简单。它用指定的接收者、主题和文本内容创建了一个 MimeMessage 对象,然后使用 JavaMail中Transport 类的静态方法 send() 发送这个消息。为了看清楚发生了什么,我们把这个过程也输出到控制台。
checkInbox() 方法做的工作比较多,因为它要列出消息列表,并可以根据选择删除它们。method does more work because it needs to list messages and, optionally, erase them. 也可以不查看直接删除消息,这取决于你在mode参数中使用的标志。It's also possible to just erase messages without looking at them, depending on the flags you use for the mode argument. 为了实际得到消息,我们需要通过session获得一个Store的引用。连接到服务器,然后打开收件箱文件夹。我们得到一个文件夹的引用之后,就可以循环显示或删除消息。
现在我们有了可重用的 MailClient 代码,我们可以为本机上的James服务器编写一个快速的测试代码了。列表5中的 JamesConfigTest 类创建了3个MailClient类的实例,分别代表我们创建的用户 (red、 green和 blue). 在你执行这段代码之前,确保那些用户在e-mail服务器上是有效的。
列表5. JamesConfigTest: James 服务器的一个快速测试
===========================================

public class JamesConfigTest
{
public static void main(String[] args)
throws Exception
{
// CREATE CLIENT INSTANCES
MailClient redClient = new MailClient("red", "localhost");
MailClient greenClient = new MailClient("green", "localhost");
MailClient blueClient = new MailClient("blue", "localhost");

// CLEAR EVERYBODY'S INBOX
redClient.checkInbox(MailClient.CLEAR_MESSAGES);
greenClient.checkInbox(MailClient.CLEAR_MESSAGES);
blueClient.checkInbox(MailClient.CLEAR_MESSAGES);
Thread.sleep(500); // Let the server catch up

// SEND A COUPLE OF MESSAGES TO BLUE (FROM RED AND GREEN)
redClient.sendMessage(
"[email protected]",
"Testing blue from red",
"This is a test message");
greenClient.sendMessage(
"[email protected]",
"Testing blue from green",
"This is a test message");
Thread.sleep(500); // Let the server catch up

// LIST MESSAGES FOR BLUE (EXPECT MESSAGES FROM RED AND GREEN)
blueClient.checkInbox(MailClient.SHOW_AND_CLEAR);
}
}
===========================================
创建了3个MailClient 实例之后, JamesConfigTest 先简单的用 checkInbox()方法以CLEAR_MESSAGES 模式清除每个邮箱,并且等待半秒以确保服务器处理完删除操作。接着分别从red和green发送一个消息到blue 。然后检查blue账号里的消息。当你运行JamesConfigTest的时候,你应该看到如列表6的输出:
列表6. JamesConfigTest运行时的输出
===========================================

Clear INBOX for [email protected]

Clear INBOX for [email protected]

Clear INBOX for [email protected]

SENDING message from [email protected] to [email protected]

SENDING message from [email protected] to [email protected]

Show and Clear INBOX for [email protected]
From: [email protected]
Subject: Testing blue from green
Content: This is a test message

From: [email protected]
Subject: Testing blue from red
Content: This is a test message
===========================================
这表明我们的James安装是成功的;在进行开发之前你需要按这种方式设置你的系统。然而,我们在这个系列的第二部分之前不会谈论到开发的问题。在这篇文章的剩余部分,我们将研究一下Matcher和Mailet API,以及随James发布版本一起提供的现成的匹配器和mailets。我们还将快速浏览一下James支持的其它特性。
匹配器
James自带了一些标准的匹配器(matchers)。它们全都实现了Matcher API,如列表7所示,并且提供了现有的MTA一般都有的功能,还提供了一些实用的扩展功能。这个接口非常简单;它包含了一对生命周期方法,init() 和 destroy() ,还有一对记录方法getMatcherInfo() 和 getMatcherConfig(),以及一个主方法,match() ,对Mail对象进行操作。Mail引用提供了容器状态的访问、邮件消息和要进行操作的元数据。
列表7. The Matcher 接口
===========================================

public interface Matcher
{
void init(MatcherConfig config);
void destroy();
String getMatcherInfo();
MatcherConfig getMatcherConfig();
Collection match(Mail mail);
}
===========================================
匹配器的任务是识别一组接收者,并返回一个代表要被mailet处理的接收者的字符串对象集合。通过结合匹配器的识别能力和mailet的处理能力,可以开发出复杂的e-mail消息处理应用程序。
随James一起发布的匹配器使你无需开发自己的匹配器就能做一些事情。在决定开始开发自己的匹配器之前最好先了解一下这些已有的匹配器。通常的情况下,你想做的工作可能已经帮你做好了。你可以在表格1中看到这些已有的匹配器:
表格 1. James自带的匹配器(matchers)
Matcher Description
All 匹配所有的e-mail并返回所有的接收者
HasHeader 匹配含有指定的头信息的消息
HasAttachment 匹配带有附件的消息
SubjectStartsWith 匹配标题以指定的文本开头的消息
SubjectIs 匹配含有指定的标题消息
HostIs 匹配来自指定的主机的消息
HostIsLocal 匹配本机产生的消息
UserIs 匹配指定的用户的消息
SenderIs 匹配指定的发送者的消息
SenderInFakeDomain 匹配发送者的主机地址不能解析的消息
SizeGreaterThan 匹配比指定的限制大的消息
Recipients 匹配接收者在指定的列表中的消息
RecipientsLocal 匹配接收者在本地的消息
IsSingleRecipient 匹配仅有一个接收者的消息
RemoteAddrInNetwork 匹配来自指定的IP地址、域等列表的消息
RemoteAddrNotInNetwork 匹配不是来自指定的IP地址、域等列表的消息
RelayLimit 匹配转发次数大于指定的服务器数的消息。
InSpammerBlackList 与mail-abuse.org提供的列表中的地址匹配
NESSpamCheck 采用得自Netscape Mail Server的方法匹配垃圾邮件
HasHabeasWarrantMark 采用Habeas Warrant匹配邮件
FetchedFrom 与FetchPOPMatches所用的 X-fetched-from 头信息匹配
CommandForListserv 匹配目录服务器的命令

正如你在表中所看到的,你不用编写任何新的代码就可以完成很多任务了,包括诸如匹配头、主题、接收者这样的原子级任务,以及像检测垃圾邮件和处理目录服务器命令这样的高层任务。
Mailets
James' 的很多特性是通过列表8中Mailet API 实现的,熟悉Servlet API的开发者可能会觉得奇怪,它看起来很眼熟。与Matcher API一样,Mailet接口支持两个生命周期方法,一个提供初始化(init() 方法),一个停止服务(destroy() 方法)。还有两个返回信息的方法,getMailetInfo(), 返回一个包含作者、版本、该mailet的版权等信息的字符串对象,getMailetConfig()很实用,它返回mailet的配置信息。init()方法有一个MailetConfig对象作为参数,虽然这个对象可能被修改,但它通常是由getMailetConfig()提供的。
列表 8. The Mailet 接口

===========================================
public interface Mailet
{
void init(MailetConfig config);
void destroy();
String getMailetInfo();
MailetConfig getMailetConfig();
void service(Mail mail);
}
===========================================
services() 方法以一个Mail 对象为参数,完成主要的处理工作。Maile对象提供了对容器状态、邮件消息和要进行处理的元数据的访问。
表格2是James已有的mailet实现的列表,它可以给你一个James支持的特性和已有的mailet应用程序的类型的概念。
表格2. James自带的 mailets
Mailet Description
Null 结束e-mail消息的处理
AddHeader 给消息内容加一个文本的头信息
AddFooter 给消息内容加一个文本的脚信息
Forward 将消息转发给列表中的接收者
Redirect 提供可配置的转发服务
ToProcessor 将e-mail处理转发给一个指定的处理器
ToRepository 将消息复制到指定的目录下
NotifySender 将消息作为附件转发给原始的发送者
NotifyPostmaster 将消息作为附件转发给postmaster
RemoteDelivery 管理SMTP主机的发送
LocalDelivery 将消息发送到本地邮箱
JDBCAlias 使用JDBC数据源进行别名翻译
JDBCVirtualUserTable 使用JDBC数据源进行更复杂的别名翻译
UseHeaderRecipient 从消息的头信息中重建邮件的接收者
ServerTime 发送一个带有服务器时间戳的消息
PostmasterAlias 将 [email protected] 的消息转发到一个个人的地址中
AddHabeasWarrantMark 给消息添加一个Habeas Warrant标记
AvalonListserv 提供一个基本的目录服务器功能
AvalonListservManager 处理目录服务器的管理命令

从这个列表可以看出,有几个James所支持的特性要归功于Mailet API,包括复杂的目录服务器支持、别名、存储和路由能力。
其它特性
除了这个系列所论述的,James还有很多其它的功能。我们会在这里简单的介绍 以使你更好的了解James的实际能力。首先是对NNTP的支持,让James可以作为一个Usenet服务器。James还实现了FetchPOP协议,能够支持基于邮件的远程管理。RemoteManager 和SpoolManager 提供了多类型存储和管理的抽象,能够支持多种类型的存储和管理。虽然James支持部分或全部的以数据库为中心的解决方案,但对于开发而言,使用基于SpoolManager的文件系统就足够了。
James提供了高效的用户管理接口和服务,并且支持邮件列表。 实际上,邮件列表功能是James提供的服务中最常用的,通常也是管理员选择James作为e-mail解决方案的原因。
下篇内容简介
James基础设施的设计目标是灵活性和易于进行应用开发。E-mail 应用的可能性是无限的。在这个系列的 follow-up installment 中,我们将开发一个简单的应用程序提供假期消息功能,允许用户向一个特定的James地址发送e-mail打开假期消息功能。用户可以起草一封e-mail作为自动答复消息,在用户向另一个特定的James地址发送取消消息关闭假期消息功能之前,James会用答复消息自动答复该用户收到的消息。e-mail客户端软件通常会提供这种机制,但客户端软件通常会受到地理位置的限制,而且如果客户端关了的话,这个功能就不起作用了。通过在e-mail服务器上实现这个功能,我们可以不受限制的在任何地方检查我们的邮件,而且我们也能够很容易根据计划的变化情况修改回复消息,以使答复更确切。
资源
下载这篇文章里用到的源代码
ftp://www6.software.ibm.com/software/developer/library/j-james1.zip

从 Apache James主页下载James包
http://james.apache.org/

从Avalon 项目主页深入学习Avalon
http://avalon.apache.org/

要执行这篇文章中的示例代码,你需要下载JavaMail API 和 JavaBeans Activation Framework.
http://java.sun.com/products/javamail
http://java.sun.com/beans/glasgow/jaf.html

在教程 "Fundamentals of JavaMail API" (developerWorks, August 2001)深入学习 JavaMail .
http://www-106.ibm.com/developerworks/edu/j-dw-javamail-i.html


About the author
Claude Duguay has developed software for more than 20 years. He is passionate about the Java platform, reads about it incessantly, and writes about it as often as possible. You can reach him at [email protected],