在J2ME平台上构建你的邮件程序
Jacky Pan
 
Table of Contents
1.       教程的介绍和程序的安装
2.       程序的结构
3.       界面的设计
4.       账户的管理
5.       MIDlet和Servlet的网络连接
6.       Servlet和JavaMail
7.       简单的XML 
8.       小结
1.教程的介绍和程序的安装
 
本教程讲述了如何在J2ME平台上编写一个简单的邮件应用程序,包括界面的设计,邮件的发送/接受,邮件账户的创建/修改/删除,后台Servlet的编写。
 
为了运行本教程所带的演示程序,您需要安装下列软件:
1.       WTK2.0 (java.sun.com)
2.       Apache Tomcat (www.apache.org)
 
安装和运行示例程序的步骤:
1.       从http://groups.yahoo.com/group/OriTec/files/下载MicroMail Beta.zip(包括了源代码和二进制文件)
2.       解压MicroMail Beta.zip至$TMP
3.       在$WTK/apps下建一新目录MicroMail
4.       拷贝$TMP/src/client/* 至 $WTK/apps/MicroMail/src/
5.       拷贝$/TMP/bin/server/mail.war至$TOMCAT/webapps/
6.       运行Tomcat
7.       运行WTK2.0, “Open Project” 并选中MicroMail
8.       设置URL为MailAgent的地址http://server/mail/MailAgent
2. 程序的结构
采用Client-Web Server-Mail Server三层架构,如图1。
                                         Figure 1
 
Cell Phone将请求(接受/发送邮件)传给Web Server,Web Server将这些http请求转换成对POP3或 SMTP Server的请求。POP3/SMTP Server执行相应的请求,并将相应通过Web Server返回给Cell Phone.
 
客户端(PDA/手机)为J2ME平台上的程序。MIDP2提供了一些基本网络连接的API。利用这些API可以使得J2ME程序可以向远端发出Http请求,并接受响应,传递数据流。
 
MailAgent 为Servlet,用来接收来自客户端的请求,并调用Java Mail API,将这些请求转变成对远端Mail Server 的请求,同时将Mail Server的响应传给客户端。
 
那么为什么要采用这样的架构了?这是因为MIDP2.0只支持HTTP协议, 而不支持POP3 和 SMTP等其它应用层协议,而J2EE提供了完整的Java Mail的API,所以考虑通过一个Servlet将Http请求转换成POP3或SMTP请求。另一个原因是,很多运营商可能只提供有限的网络访问的能力,而通过一个agent则提供了程序部署的灵活性。
 
下面简单介绍一下源码的结构,在客户端,ui包中的类定义程序的用户界面,utility包中的类定义了数据库的操作,网络的连接,XML的解析等,mail包中的类定义了邮件账户,邮件的头。
ui package:
AccountForm.java
AccountsList.java
Confirm.java
ConfirmListener.java
MailMIDlet.java
MessageList.java
ProgressForm.java
SendMessage.java
WriteContent.java
 
utility package:
DBOperator.java
HeadParser.java
NetAdapter.java
Networker.java
ParserListener.java
 
mail package:
MailAccount.java
MessageHead.java
 
服务器端只有一个文件MailAgent.java,包含了一个servlet,用来做midlet和邮件服务器的桥梁。
 
3. 界面的设计
MIDP2.0提供了大量的API供开发者创建和控制用户界面。在包javax.microedition.lcdui中提供了List, Form, TextBox, Alert等Screen组件,在Form中可以包含一系列Item,如ChoiceGroup, TextField, StringItem等。包中还提供了Command组件,以及相应的Listener。
 
源文件ui/MailMIDlet.java定义了程序的入口以及MicroMail的主页面。如图2主页面是一个List,显示了几个功能模块,包括添加账户,修改/删除账户,接受邮件,发送邮件。
Figure 2
public MailMIDlet()
{
    display = Display.getDisplay(this);
       /* 创建List */
    mainList = new List("MicroMail0.1", Choice.IMPLICIT);
       /* 创建两个控制按钮OK和Exit */
    cmOK = new Command("OK", Command.OK, 1);
    cmExit = new Command("Exit", Command.EXIT, 1);
       /* 向mainlist中添加内容 */
    mainList.append("Add Account", null);
    mainList.append("Edit Account", null);
    mainList.append("Receive Message", null);
    mainList.append("Send Message", null);
       /* 为mainlist添加Command */
    mainList.addCommand(cmOK);
    mainList.addCommand(cmExit);
       /* MailMIDlet实现了CommandListener接口,可以作为监听器 */
    mainList.setCommandListener(this);
    
       ……
}
 
通过AccountForm(源文件ui/AccountForm.java),用户可以添加和修改邮件账户,如图3。
            
     Figure 3a                                                               Figure 3b
 
AccountForm包含了6个TextField,分别显示邮件账户的6个属性。account为账户名,也是账户的唯一标识,不可重名。address为邮件的地址,如[email protected]。 user, password是在邮件服务商注册的用户名和密码。pop3, smtp是pop3服务器和smtp服务器的名字或地址。
private void setContent(MailAccount macc)
{
    account = new TextField("Account: ", "", 20, TextField.ANY);
    address = new TextField("Address; ", "", 40, TextField.EMAILADDR);
    user = new TextField("User Name: ", "", 20, TextField.ANY);
    password = new TextField("Password: ", "", 20, TextField.PASSWORD);
    pop3 = new TextField("POP3 Server: ", "", 20, TextField.ANY);
    smtp = new TextField("SMTP Server: ", "", 20, TextField.ANY);
 
    if (macc != null)
    {
      account.setString(macc.accountName);
      address.setString(macc.address);
      user.setString(macc.username);
      password.setString(macc.password);
      pop3.setString(macc.POP3Server);
      smtp.setString(macc.SMTPServer);
    }
 
    append(account);
    append(address);
    append(user);
    append(password);
    append(pop3);
    append(smtp);
 
}
 其它页面与AccountForm类似,在此不再赘述,请大家参照源代码和MIDP API的文档。
4. 账户的管理 
邮件账户的创建,修改,删除都涉及到对数据记录的存取。MIDP提供了一种叫做记录管理系统(Record Management System)的机制来存储和访问数据。
 
javax.microedition.rms.RecordStore提供了一些API来操作这个系统。静态方法openRecordStore用来打开或创建一个RecordStore对象。方法addRecord, getRecord, deleteRecord, setRecord分别用来添加,访问,删除或修改RecordStore对象中的记录。
 
utility/DBOperator.java中封装了这些方法,从而实现添加,修改,删除邮件账户的操作。以添加帐户为例,下面的addRecord方法用来向RecordStore中添加一个记录,同时把这个记录所表示的邮件账户加到一个accounts中,accounts是一个Vecotr,用来存储系统中当前的账户。参数str包含了账户的所有信息,如账户名,地址,用户名,密码,pop3,smtp等,它们之间用空格隔开。
 
public void addRecord(String str)
{
    int id;
    byte[] rec = str.getBytes();
    String record = str;
 
    try
    {
       /* 向RecordStore中添加记录 */
      id = rs.addRecord(rec, 0 , rec.length);
 
       /* 同时把帐户添加到accounts向量中 */
      MailAccount mailacc = MailAccount.createMailAccountFromString(id, str);
      accounts.addElement(mailacc);
    }
    catch(RecordStoreException rse)
    {
      rse.printStackTrace();
    }
 
}
5. MIDlet和Servlet的网络连接
MIDP的网络API在包javax.microedition.io中定义,其中HttpConnection提供了对HTTP协议的支持。
 
在文件utility/Networker.java中通过调用这些网络API实现了接收当前信箱中邮件列表setMessageList,接受某一邮件的内容receiveMessage,发送邮件sendMessage等功能。以sendMessage为例。
 
public void sendMessage(final String url, final String formData)//send a message
{
       /* 创建新的进程 */
  Thread t = new Thread()
  {
    public void run()
    {
      HttpConnection http = null;
 
      byte[] data = formData.getBytes();
      try
      {
              /* 打开并返回一个HTTP连接 */
        http = (HttpConnection) Connector.open(url);
      ......
              /* 设置HTTP请求头 */
        http.setRequestMethod(HttpConnection.POST);
        http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        http.setRequestProperty("User-Agent",
                                "Profile/MIDP-1.0 Configuration/CLDC-1.0");
        http.setRequestProperty("Content-Language", "en-US");
        http.setRequestProperty("Accept", "application/octet-stream");
        http.setRequestProperty("Connection", "close"); // optional
        http.setRequestProperty("Content-Length", Integer.toString(data.length));
       ...... 
              /* 打开输出流 */
        OutputStream os = http.openOutputStream();
              /* 写邮件数据 */
        os.write(data);
              /* 关闭输出流 */
        os.close();
                
      }
      catch (IOException ioe)
      {
        ......
      }
      finally
      {
        try
        {
                     /* 关闭连接 */
          if (http != null) http.close();
        }
        catch (IOException ignored) {}
      }
    }
  };
/* 启动进程 */
  t.start();
 
}
 
MIDlet通过HTTP连接向Servelet发出接受或发送邮件的请求,Servlet根据不同的请求向邮件服务器发出相应的请求,并将返回结果传给MIDlet。
6. Servlet和JavaMail
J2EE中提供了对邮件相关协议的支持,包javax.mail和包javax.mail.internet 中定义了JavaMail API。下面是MailAgent.java中Servlet处理接受邮件列表请求的代码片断。
public void doPost(HttpServletRequest request, HttpServletResponse response)
       throws ServletException, IOException 
{
       PrintWriter out = response.getWriter();
              
       String typeStr = request.getParameter("type");
       int type = Integer.parseInt(typeStr);
       
       String pop3Server;
       String username;
       String password;
       
       Properties props;
       String provider;
       
       switch (type)
       {
              case RECEIVE_LIST:
                     /* 提取参数pop3服务器,用户名,密码 */
                     pop3Server = request.getParameter(paramPOP3);
                     username = request.getParameter(paramName);
                     password = request.getParameter(paramPass);
              
                     if (pop3Server == null || username == null || password == null)  
                     {
                            out.print(STATUS_BAD);//xml?
                            return;
                     }
              
                     props = new Properties();
                     provider = "pop3";
                     try
                     {
                            /* 以指定的用户名和密码连接pop3服务器 */
                            Session session = Session.getDefaultInstance(props, null);
                            Store store = session.getStore(provider);                          
                            store.connect(pop3Server, username, password);
                            /* 得到INBOX收件箱 */
                            Folder inbox = store.getFolder("INBOX");
                            
                            if (inbox == null)
                            {
                                   out.print(STATUS_BAD);//xml?
                                   return;
                            }
                            /* 打开收件箱 */
                            inbox.open(Folder.READ_ONLY);
                            /* 得到收件箱中的邮件列表,并写到 XML中 */
                            writeXML(inbox.getMessages(), out);
              
                            inbox.close(false);
                            store.close();
                     }
                     catch(Exception e)
                     {
                            out.println(e.getMessage());
                            e.printStackTrace();
                     }
       
                     out.close();
              
                     break;
              
              case RECEIVE_MESSAGE:
                     ......
                     break;
                     
              case SEND_MESSAGE:
                     ......
                     break;
       }
       
}
J2EE程序的编译和部署请大家参照相关的书籍或文档。http://www.tusc.com.au/tutorial/html/ 的<<Tutorial for building J2EE Applications using JBOSS and ECLIPSE>>是一篇不错的文档。
7. 简单的XML
在MIDlet接受邮件时,首先向Servlet请求传送邮箱中当前邮件的列表。列表中包括了邮件的头部信息,包括发送者的地址,邮件的主题,发送的时间等,如图4。
Figure 4
在邮件主题中可能包括任何字符,所以没有办法用某一特殊字符分隔这些信息,而XML正好适合传输这种具有特定格式的信息。在Servlet端,把有用的邮件头部信息作为XML的元素写到输出流中。
private void writeXML(Message[] messages, PrintWriter out)
{
       out.println("<?xml version=\"1.0\"?>");
       out.println("<mail>");
       
       if (messages == null)
       {
              out.println("<error>No Mail</error>");
       }
       
       try
       {
              int j = 0;
              for (int i = messages.length -1; i >= 0; i--)
              {
                     out.println("<message>");
                     /* 写邮件头 */
                     out.println("<from><![CDATA[" + InternetAddress.toString(messages[i].getFrom()) + "]]></from>");
                     out.println("<subject><![CDATA[" + messages[i].getSubject() + "]]></subject>");
                     out.println("<date>" + messages[i].getSentDate().toLocaleString() + "</date>");
                     out.println("<index>" + i + "</index>");
                     out.println("</message>");
                     
                     j++;
                     if (j > 9)
                     {
                            /* 一次只看10个邮件 */
                            break;
                     }
              }
       }
       catch(MessagingException me)
       {
              out.println("<error>" + me.toString() + "</error>");
       }
       
       out.println("</mail>");
}
 
在MIDlet端再解析这个XML,在J2ME平台上有许多免费的XML Parser,kxml就是其中的一个。可以从 http://kxml.enhydra.org/ 下载kXML 1.21的源代码,jar文件以及API文档。
 
下面是utility/HeadParser.java中处理XML的代码片断
public void parse(InputStream in) throws IOException
{
  Reader reader = new InputStreamReader(in);
  XmlParser parser = new XmlParser(reader);
  ParseEvent pe = null;
 
  parser.skip();
  /* 读一个名字为mail的event */
  parser.read(Xml.START_TAG, null, "mail");
 
  boolean trucking = true;
  boolean first = true;
 
  while (trucking)
  {
    /* 读取下一个event */
    pe = parser.read();
 
    if (pe.getType() == Xml.START_TAG)
    {
      /* 得到event的名字 */
      String name = pe.getName();
 
      if (name.equals("message"))
      {
        String from = null;
        String subject = null;
        String date = null;
        int index = -1;
 
        while ((pe.getType() != Xml.END_TAG) ||
            (pe.getName().equals(name) == false))
        {
          pe = parser.read();
          if (pe.getType() == Xml.START_TAG &&
              pe.getName().equals("subject"))
          {
            pe = parser.read();
            /* 得到event的内容 */
            subject = pe.getText();
          }
          else if (pe.getType() == Xml.START_TAG &&
              pe.getName().equals("from"))
          {
            pe = parser.read();
            from = pe.getText();
          }
          else if (pe.getType() == Xml.START_TAG &&
              pe.getName().equals("date"))
          {
            pe = parser.read();
            date = pe.getText();
          }
          else if (pe.getType() == Xml.START_TAG &&
              pe.getName().equals("index"))
          {
            pe = parser.read();
            index = Integer.parseInt(pe.getText());
          }
 
        }
        
        /* 把邮件头交给监听器处理 */
        headListener.itemParsed(from, subject, date, index);
      }
      else //Non Message block
      {
        while ((pe.getType() != Xml.END_TAG) ||
            (pe.getName().equals(name) == false))
          pe = parser.read();
      }
    }
    if (pe.getType() == Xml.END_TAG &&
          pe.getName().equals("mail"))
    {
      trucking = false;
    }
 
  }
 
具体的API的用法请大家参照kxml的文档。JSR172 (J2ME Web Services Specification)提出了在J2ME平台上处理XML的规范,有兴趣的朋友可以到jcp的网站 (http://www.jcp.org) 上看看,但目前可能还没有厂商或组织的实现。
8. 小结
本文介绍了J2ME平台上邮件程序的编写,涉及的知识点有:
1.  J2ME的UI
2.  Record Store
3.  J2ME的网络连接 / J2ME 和J2EE之间数据的传递
4.  Parsing XML in J2ME
5.  简单的Servlet
6.  Java Mail APIs
 
 
参考资料有:
1.         MIDP2.0 Spec, http://www.jcp.org 
2.         <<Core J2ME>>, http://www.corej2me.com/
3.         <<J2ME in Nutshell>>, http://www.oreilly.com 
4.         Tutorial for building J2EE Applications using JBOSS and ECLIPSE, http://www.tusc.com.au/tutorial/html/