メッセージを受信/解釈することに主眼を置いたサーバサイドアプリケーションというのはなかなか難しいものがあります。
例えば、サーバ上でメッセージを自動的に受信して、その内容に応じてさまざまな処理を行うというアプローチは、現在Webベースで行われているブラウザからサーバへの要求送信と同様に利用できれば便利そうですが、これを実現したアプリケーションはまだほとんど見かけることはありません。電子メイルの本文にはWebのFORMのようなフォーマットが存在しないため、受信側で統一的な解釈を行うのが非常に難解だからです。現状は本文の内容を解釈するものとしては、メイリングリストにおけるコマンドメイルなどがありますが、送信者が正しくコマンドの構文に従ったメッセージを送信しないと弾かれてしまいます。このような現状では商品の受注などの複雑な要求をメイルベースで人手を介さずに受け付けるのは、ユーザの負担を考えても現実的ではありません。
現状でメッセージを受信して処理するアプリケーションとしては、先に挙げたコマンドメイルのように本文の内容を細かく解釈するようなものではなく、本文全体をHTML等に変換するものが多いです。メイリングリストのHTMLアーカイブ生成等が典型的ですね。ここでもそのタイプのサンプルとして、WebMailシステムをJavaMailとServletを用いて作成してみます。
[メイルでの注文!?]
電子メイルはWebのFORMと違って、インターネットに接続していない状態でゆっくり作成でき、途中で入力を中断/保存したり、送信を後で行うといったことが可能になるので、現在WebのFORMから行われていることが電子メイルベースでも可能になれば便利な場合もあると思います。現状はユーザが送信した電子メイルの内容を機械的に解釈するのは、ユーザに所定のフォーマットを守らせる手段が存在しないため難しいです。本文は操作しないでどこかに転送したり、人手を使って内容を解釈するというのが主流ですね。
最近のXML技術によって、クライアントサイドに特定のスキーマ(文書構造を規定するもの)に従って入力フォームが生成されて、それがXML化されて送信されるような機構が普及すれば、以下のようなシナリオも実現できるようになるかもしれません。
これならサーバが受信するメッセージのフォーマットが固定的になるので解釈ミスも少なくなり、ユーザもなにを記述すればよいか解らないと言った現象がなくなるでしょう。
全てのユーザのMUAがこういった機能に対応するようになるのはなかなか難しいですが、近い将来これに近い機能が普及する可能性はあります。
なお、ユーザ側の立場で言えば、機械が生成した返事を見るより人間に読んでもらって個別の返事が来るほうがいいと思うところですが、人が処理する場合は送ったメッセージを読んでくれるまでに結構タイムラグが生じてしまいます。機械処理なら、ほぼ即座に要求が受け付けられます。
このシステムのサポートする機能は以下のようなものにします。取り敢えずセッションを複雑にしない単純なものとして、POP3でメッセージを受信/閲覧する機能とSMTPでメッセージ送信を行えるだけのものにします。

HTMLメッセージや添付ファイルの表示は取り敢えずサポートしません。先頭のテキストパートを表示するだけにします(*)。また、接続先を固定とするというのは、このようなシステムはあまりオープンにしてしまうと予想外のトラフィックが発生したり、不正中継に利用されかねないためです。
*:HTMLメッセージを表示する場合、メッセージボディが独立したHTML文書となるため、表示時に細工が必要になります(HTML文書中に別のHTML文書が入るような形になります)。
だいたい以下のような画面と処理の構成で良いでしょう。

送信フォームは単なるHTMLでもよかったのですが、返信機能から呼びだせるようにして共通化を図ることと、将来添付ファイル送信機能を付加しやすいようにするため、Servletにしました。また、受信のための認証を受けた後でないと遷移できないという仕様にしました。無条件に送信できてしまうのはよくありませんからね。
では、まずは受信機能のログイン画面を用意しましょう。
[index.html]
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<META http-equiv="Content-Style-Type" content="text/css">
<TITLE>WebMail Servlet Sample</TITLE>
</HEAD>
<BODY>
<DIV style="text-align:center">
<H1>WebMail Servlet</H1>
<HR>
<FORM method="post" action="/servlets/com.sk_jp.servlet.webmail_sample.MessageList">
<TABLE style="font-size:18pt" border="1">
<TBODY>
<TR>
<TD>UserName</TD>
<TD><INPUT size="20" type="text" name="user" value></TD>
</TR>
<TR>
<TD>Password</TD>
<TD><INPUT size="20" type="password" name="pass"></TD>
</TR>
</TBODY>
</TABLE>
<P><INPUT type="submit" value=" login "></P>
</FORM>
</DIV>
</BODY>
</HTML>
以下のような画面になります。

このフォームからユーザ名/パスワードを受け取って、メッセージ一覧するServletを作成します。まず、先程のShoppingCartServletと同様に、複数のServletから構成されるため、共通の初期パラメタを読み込む処理を記述したServletを用意しておいて、これを継承して各Servletを作成しておくことにします。
// [WebMailBase.java]
package com.sk_jp.servlet.webmail_sample;
import java.io.IOException;
import java.util.Properties;
import com.sk_jp.servlet.BaseServlet;
public abstract class WebMailBase extends BaseServlet {
protected Properties properties;
protected String inputEncoding;
protected String outputEncoding;
protected String host;
// BaseServletのinit(ServletConfig)メソッドから呼び出されます。
protected final void init() throws IOException {
properties = loadProperties(
"/com/sk_jp/resources/webmail_sample/webmail.properties");
inputEncoding = properties.getProperty(
"inputEncoding", "ISO-8859-1");
outputEncoding = properties.getProperty(
"outputEncoding", "ISO-8859-1");
host = properties.getProperty(
"host", "localhost");
initSub();
}
protected void initSub() throws IOException {
}
}
*:このクラスのスーパークラスであるBaseServletについてはShoppingCartServletと同様にWebの方で公開しています。
ここで作成するServlet群に共通のパラメタとしてはtemplateファイルのエンコーディング(inputEncoding)、Servletの出力エンコーディング(outputEncoding)および、POP3/SMTPサーバの稼働するホストの3つとしました。任意のPOP3サーバに接続できるようにするのは簡単ですが、セキュリティホールの元になりかねないため制限しています。POP3サーバとSMTPサーバが異なる場合もこのサンプルでは想定していません。
では、メッセージ一覧表示Servletの作成です。
// [MessageList.java]
package com.sk_jp.servlet.webmail_sample;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.mail.NoSuchProviderException;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import com.sk_jp.text.Formatter;
import com.sk_jp.text.TextFormatter;
import com.sk_jp.mail.SimpleRetriever;
import com.sk_jp.mail.MapByMessage;
public class MessageList extends WebMailBase {
private Formatter formatter;
protected void initSub() throws IOException {
formatter = new TextFormatter(
getClass().getResourceAsStream(
"/com/sk_jp/resources/webmail_sample/messagelist.template"),
inputEncoding);
}
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
String user = getParameter(request, "user", null);
String pass = getParameter(request, "pass", null);
SimpleRetriever retriever;
HttpSession session = request.getSession(true);
if (user != null && pass != null) {
try {
retriever = new SimpleRetriever("pop3");
} catch (NoSuchProviderException e) {
getServletContext().log(e, "Got exception:");
// Servlet API 2.1以降
// log("Get Exception:", e);
error(response, "POP3Provider not found.",
"プロバイダが見つかりません。<BR>" +
"サーバ側の設定に問題があります。");
return;
}
session.putValue("retriever", retriever);
session.putValue("user", user);
session.putValue("pass", pass);
}
user = (String)session.getValue("user");
pass = (String)session.getValue("pass");
retriever = (SimpleRetriever)session.getValue("retriever");
if (retriever == null) {
errorSessionInvalid(response);
return;
}
try {
retriever.connect(host, user, pass);
MimeMessage[] messages = retriever.get();
if (messages == null) {
error(response, "メッセージはありません",
"メイルボックスにメイルはありませんでした。");
return;
}
Map[] list = new Map[messages.length];
for (int i = 0; i < messages.length; i++) {
list[i] = new MapByMessage(messages[i], true);
list[i].put("num", String.valueOf(i + 1));
}
Map args = new HashMap();
args.put("user", user);
args.put("count", new Integer(messages.length));
args.put("list", list);
print(response, formatter, args, outputEncoding);
} catch (MessagingException e) {
getServletContext().log(e, "Got exception:");
error(response, "メイルの取得に失敗",
"以下の例外によりメッセージの取得に失敗しました。<BR>" + e);
return;
} finally {
retriever.disconnect();
}
}
}
doPost()で、user/passパラメタを獲得し、3章で作成したSimpleRetrieverを用いてPOP3サーバに接続します。user/passおよびSimpleRetriever(3章で作成したもの)はHttpSessionに記憶しておき、以降パラメタ無しでMessageList Servletが呼び出された場合はその情報を用いて接続を行います。
SimpleRetriever#connect()でPOP3サーバに接続し、get()メソッドでメイルボックス上の全てのメッセージ数分のMimeMessageオブジェクトを得ます(この段階ではまだMimeMessageオブジェクト上にメッセージはダウンロードされていません)。
次に、このメッセージ群に対応するMapの配列を生成しています。これはTextFormatterに渡して出力フォーマットを得るためです。各メッセージに対応するMapには、TextFormatter上で参照するキーに対応する値を格納していく必要があるのですが、これを単純化するために筆者はMapByMessageというクラスを作成しました。これはMessageオブジェクトをラップしてMap
interfaceでアクセスできるようにするものです(Mapの実装クラスです)。MapByHttpSessionというのも紹介していましたね。このように様々なものをMapでアクセスできるようなラッパクラスを用意しておけば、何も考えずにTextFormatterに渡せるようになります。もちろんTextFormatterのために限定せず、このようなインターフェイスの共通化を行うことは様々な場面で有用でしょう(*)。いずれもhttp://www.sk-jp.com/java/library/からソースコードを取得できます。
なお、紙面の都合上ソースコードの掲載を見送っていますが、MapByMessageクラスはget()で与えるキーワードに応じてMessageオブジェクトから取得した内容を加工して返す機能を持っています。
*:セキュリティに注意。TextFormatterではテンプレートに記述されたキー以外のエントリが参照されることはないのでこのようなアプローチで問題ありませんが、他の用途でこのようなラッパクラスを用いる場合は、参照されたくない情報を隠す必要性もあるかもしれません。
さて、Mapの配列を作った後、実際にTextFormatterのパラメタとなる別のMapを作成しています(args変数)。これらのMapに適切な値を格納してprint()メソッド(TextFormatterを用いてフォーマット出力を行うメソッド)を呼び出しています。このとき渡されるargsの構造は以下のような感じです。
| Mapのキー | 値の内容 | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| user | ユーザ名 | ||||||||||||||||||
| count | メイルボックス内のメッセージ数 | ||||||||||||||||||
| list | Mapの配列 それぞれのMapは以下のような内容 (MapByMessageオブジェクトが提供するエントリです)
× count |
このMapを以下のようなテンプレートを使うTextFormatterに渡す事によってフォーマットを行います。
[messagelist.template]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<TITLE>WebMail messages</TITLE>
</HEAD>
<BODY>
<H1>@##count@ messages in your mailbox</H1>
<TABLE width="100%">
<TBODY>
<TR>
<TD width="100%">User : <VAR>@##user@</VAR></TD>
<TD align="right" valign="middle">
<FORM method="post" action="com.sk_jp.servlet.webmail_sample.MessageList">
<INPUT type="submit" value=" メイルチェック ">
</FORM>
</TD>
<TD align="right" valign="middle">
<FORM method="get" action="com.sk_jp.servlet.webmail_sample.SendForm">
<INPUT type="submit" name="create" value="新規メッセージ作成">
</FORM>
</TD>
</TR>
</TBODY>
</TABLE>
<HR>
@#exist list@
@#while list@
<P>
No: @##num@<BR>
From: @##htmlfrom@ To: @##htmlto@ Date: @##japanizeddate@ @##size@ bytes<BR>
Subject: <STRONG>@#html subject@</STRONG>
</P>
<FORM method="post" action="com.sk_jp.servlet.webmail_sample.MessageView">
<INPUT type="hidden" name="num" value="@##num@">
<INPUT type="submit" value="show">
</FORM>
@#endwhile@
@#endexist@
</BODY>
</HTML>
以下が出力される画面イメージです。

*:メッセージサイズが負の値になっているのは、例によって接続したPOP3サーバであるJAMES(1.2)のバグで、LISTコマンドがメッセージ全体のサイズではなく本文のサイズを返すようになっているためです。JavaMailはLISTコマンドがメッセージ全体のサイズを返すと思っていて、Message#getSize()メソッドではそれからヘッダのサイズを引いた値を返すのです。
続いて、特定のメッセージの表示を行うMessageViewクラスまで一気に示してしまいます。上記の画面の「show」ボタンから起動されるServletです。以下のソースコード、テンプレートおよび実行結果をご覧ください。
// [MessageView.java]
package com.sk_jp.servlet.webmail_sample;
import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.mail.NoSuchProviderException;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import com.sk_jp.text.Formatter;
import com.sk_jp.text.TextFormatter;
import com.sk_jp.mail.SimpleRetriever;
import com.sk_jp.mail.MapByMessage;
public class MessageView extends WebMailBase {
private Formatter formatter;
protected void initSub() throws IOException {
formatter = new TextFormatter(
getClass().getResourceAsStream(
"/com/sk_jp/resources/webmail_sample/message.template"),
inputEncoding);
}
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null) {
errorSessionInvalid(response);
return;
}
int num = 1;
try {
num = Integer.parseInt(request.getParameter("num"));
} catch (NumberFormatException e) {
// 省略
errorIllegalParam(response, "num");
return;
}
SimpleRetriever retriever =
(SimpleRetriever)session.getValue("retriever");
try {
retriever.connect(host,
(String)session.getValue("user"),
(String)session.getValue("pass"));
MimeMessage msg = retriever.get(num);
// Replyから使用するためにキャッシュしておく
session.putValue("message", msg);
Map args = new MapByMessage(msg, true);
args.put("num", String.valueOf(num));
print(response, formatter, args, outputEncoding);
} catch (MessagingException e) {
getServletContext().log(e, "Got exception:");
error(response, "メイルの取得に失敗",
"以下の例外によりメッセージの取得に失敗しました。<BR>" + e);
return;
} finally {
retriever.disconnect();
}
}
}
[message.template]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<TITLE>WebMail message Number:@##num@</TITLE>
</HEAD>
<BODY>
<H1>Message Number: @##num@</H1>
<TABLE border="1">
<TBODY>
<TR>
<TD>From</TD>
<TD>@##htmlfrom@</TD>
</TR>
<TR>
<TD>Subject</TD>
<TD>@#html subject@</TD>
</TR>
<TR>
<TD>Date</TD>
<TD>@##date@</TD>
</TR>
<TR>
<TD>To</TD>
<TD>@##htmlto@</TD>
</TR>
<TR>
<TD>cc</TD>
<TD>@##htmlcc@</TD>
</TR>
<TR>
<TD>Message-ID</TD>
<TD>@#html message_id@</TD>
</TR>
</TBODY>
</TABLE>
<HR>
<PRE>@#html content@</PRE>
<HR>
<FORM method="POST" action="com.sk_jp.servlet.webmail_sample.Reply">
<INPUT type="submit" value=" 返信 ">
</FORM>
</BODY>
</HTML>
実行結果

おそらく他の手法と比べてもかなり単純なプログラムであるという印象をもたれると思います(筆者の独り善がりではないはず^^)。
ソースコードの方ですが、doPost()メソッドでは、まずHttpSessionオブジェクトを獲得し、パラメタである"num"の値を獲得します。
次にHttpSessionに保持しておいたSimpleRetrieverオブジェクト(*)に対して、やはりHttpSessionに保持していたuser/pass(word)を使ってconnect()
→ get(int)を呼び出し、目的のMimeMessageオブジェクトを得ています。ここで得たMimeMessageオブジェクトは返信時に取得し直さなくてすむようにHttpSessionオブジェクト中に保存しておきます。
*:SimpleRetrieverオブジェクトは毎回newしても同じで、単に使いまわすためだけの理由でHttpSessionに格納しています。
後は、MessageList.javaと同様にMapByMessageオブジェクトでラップして、今回はそのMapByMessageオブジェクトをそのままTextFormatterに渡す形で出力処理を行っています。MimeMessageオブジェクトをMapでラップする事でそのままTextFormatterに渡すことができるという利点が活かされています。
ノーマライズ
Webベースのアプリケーションでミスを犯しやすいのは、外部から取り込んだ文字列をそのままHTMLとして出力してしまうというものです。例えば、メイルの本文にHTMLタグとおもわしきものが記述されている時、これを実体参照(<や>など)に置き換えずに出力してしまうと、本文の表示が予期しないものになるだけでなく、それが<SCRIPT>タグに囲まれた悪意のあるコードであった場合などを考えると立派なセキュリティホールとなります。
本書で紹介しているTextFormatterではキーワードに対応する値のいくつかの文字を実体参照に変換して出力する構文@#html keyword@というものを用意して、適宜これを使うようにしています。
さらに、注意するのは、このような変換が必要なのはユーザが自由に入力し得る全ての文字列に対してであるということです。
例えばWebMailであればFrom:やTo:といったメイルアドレスを記述するヘッダであってもHTMLタグを記述する事ができますので当然これらも変換が必要になります。
最近騒がれた例として、サーチエンジンの検索キーワードが検索結果画面にそのまま出力されるものがあり、このキーワードにHTMLタグを含めることでCross-Site
Scriptingという攻撃が可能になるセキュリティホールがありました。また、処理を簡略化しがちな入力エラー画面にユーザの入力文字列をそのまま出力してしまうケースも多く発見されています。
Webアプリケーションの作成者は十分注意しましょう。
ここまでで作ったものについて整理しますと、ログイン画面、受信メッセージ一覧画面、受信メッセージ表示画面ですね。
残るは送信フォームと送信結果画面、そして送信フォームに対して特定メッセージに対する「返信」用の初期値を生成する処理の三つです。
先に返信処理を記述します。送信フォーム生成処理はHttpSessionオブジェクトにフォームの初期値が存在すればそれを表示し、存在しない場合は空の送信フォームを生成するものとして、返信処理はHttpSessionオブジェクトに返信用のデータを格納する部分とします。
// [Reply.java]
package com.sk_jp.servlet.webmail_sample;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.InternetAddress;
import com.sk_jp.mail.MailUtility;
public class Reply extends WebMailBase {
private String quoteHeading;
private String quoteSymbol;
protected void initSub() throws IOException {
// 引用時のヘッダと引用符
// ユーザ毎の設定としてサーバに保存できるようにするのがよいでしょう。
quoteHeading = properties.getProperty(
"quoteHeading",
"In article @##message_id@\r\n@##fromname@ wrote:\r\n");
quoteSymbol = properties.getProperty(
"quoteSymbol",
">");
}
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false); // (1)
if (session == null) {
errorSessionInvalid(response);
return;
}
MimeMessage msg = (MimeMessage)session.getValue("message");
if (msg == null) {
error(response, "メッセージの返信が行えません",
"メッセージが表示されていないためメッセージ" +
"の返信が行えません。");
return;
}
try { // (2)
session.putValue("to",
MailUtility.decodeText(
InternetAddress.toString(msg.getReplyTo())));
String msgID = msg.getMessageID();
if (msgID != null) {
session.putValue("in_reply_to", msgID);
String references =
MailUtility.unfold(
msg.getHeader("References", " "));
if (references != null) {
references += ' ' + msgID;
} else {
references = msgID;
}
session.putValue("references", references);
}
session.putValue("subject",
MailUtility.createReplySubject(
MailUtility.decodeText(
MailUtility.unfold(
msg.getHeader("Subject", null)))));
} catch (MessagingException e) {
getServletContext().log(e, "Got exception:");
error(response, "メッセージの返信が行えません",
"返信メッセージの生成に失敗しました。");
return;
}
session.putValue("content",
MailUtility.createReplyContent(
msg, quoteHeading, quoteSymbol, -1));
response.sendRedirect("com.sk_jp.servlet.webmail_sample.SendForm");
}
}
まず、MessageViewでHttpSessionに保存された返信元のMessageオブジェクトを取り出します(1)。
その後、「返信」フォームに表示すべきデータを作成しています(2)。この部分は、Message#reply()、メソッドで得たMimeMessageオブジェクトからデータを取り出すように実装すればよいのではないかと思えますが、4章でも説明したとおり、Message#reply()は最低限の処理しか行わない上、日本で流通するメイラの「返信」機能における挙動とは異なる部分がありますので、Message#reply()の内容に近い処理を独自に実装しています。
返信用フォームの作成手順を一つずつ見ていきます。なお、この処理は筆者の判断で実装したコードであり、全てに通用する汎用的なものというわけではないことを予めお断りしておきます。
まずはTo:の生成です。
session.putValue("to",
MailUtility.decodeText(
InternetAddress.toString(msg.getReplyTo())));
返信元MessageのgetReplyTo()メソッドを呼び出して返信アドレスを取り出します。このメソッドは「Reply-To:の内容を返す」ではなく、「返信先とおぼしきアドレスを返す」というもので、Reply-To:またはFrom:の値をAddress[]型で返します。これに対してInternetAddress#toString(Address[])を呼び出す事で、複数のAddressオブジェクトをカンマ区切りの文字列に変換します。このメソッドはメッセージ送信時に使用される事が想定されているため、これによって得られるメイルアドレス文字列は、個人名の部分がMIMEエンコードされた状態になってしまいますので、decodeText()メソッドにより、Unicode文字列に変換したものをHttpSessionに格納しています。
MailUtility#decodeText()は4章の「日本語を扱う場合の注意点」で紹介したもので、MimeUtility#decodeText()に代わるものです。
String msgID = msg.getMessageID();
if (msgID != null) {
session.putValue("in_reply_to", msgID);
String references = // (1)
MailUtility.unfold(
msg.getHeader("References", " "));
if (references != null) {
references += ' ' + msgID;
} else {
references = msgID;
}
session.putValue("references", references);
}
次はIn-Reply-To:とReferences:を生成しています。これらは2章で説明していますが、返信元メッセージのMessage-ID:を利用して生成します。
In-Reply-To:は返信元のMessage-ID:の値をそのまま設定します。References:は、返信元メッセージにReferences:が存在しない場合はIn-Reply-To:と同様にMessage-ID:の値を設定します。既に存在した場合は、そのReferences:の末尾にMessage-ID:の値を空白区切りで追加します。
これらをそれぞれ、HttpSessionの"in_reply_to"と"references"に格納しています。
(1)で示した部分では、返信元メッセージのReferences:を取り出した後、MailUtility#unfold()に通した結果を使用しています。これは、4章の「folding/unfoldingについて」で説明しているように、Part#getHeader()等のメソッドは、ヘッダが折り返されている(folding)場合にそれを元の一行に戻す(unfolding)ことを行わず、<CRLF>を含んだままの文字列を返します。従って、getHeader()等で得た文字列をFORMの初期値やGUIのTextField等に設定する場合はunfoldingをアプリケーション側で行う必要があるのです。
session.putValue("subject",
MailUtility.createReplySubject(
MailUtility.decodeText(
MailUtility.unfold(
msg.getHeader("Subject", null)))));
次は返信メッセージのSubject:の初期値を生成しています。多段にメソッドを呼び出していて複雑ですが、ここで行っている事は以下のような事です。
まず、Part#getHeader()により返信元のSubject:を得ています。Message#getSubject()を用いていないのは、このメソッドでは一部のメイラが送出する間違ったエンコードを施されたSubject:を正しく取り出せないためです。次に、先程References:の箇所で説明した通り、取り出したSubject:は<CRLF>が取り除かれていない可能性があるため、MailUtility#unfold()で(もしfoldingが行われていれば)unfoldingを行い、その結果をMailUtility#decodeText()でデコードしています。
これにより、返信元メッセージのSubject:を一行の正しいUnicode文字列に変換できます。
後はこの返信元メッセージのSubject:をMailUtility#createReplySubject()メソッドに渡す事により、"Re:
"が付加された文字列を得て、それをHttpSessionに格納しているというわけです。
MailUtility#createReplySubject()は筆者の作成したメソッドで、上記の通り、先頭に"Re:
"を付加するものです。既に"Re: "がついている場合は付加しない等の処理を行っています(Message#reply()の中で行われている事に似ていますが多少機能拡張しています)。
このメソッドの実装については本書では触れていませんが、http://www.sk-jp.com/java/library/から、他に紹介したメソッド群とともにソースコードを取得できます。
session.putValue("content",
MailUtility.createReplyContent(
msg, quoteHeading, quoteSymbol, -1));
最後に返信メッセージの本文を生成しています。これについてもここでは実装をご紹介していないMailUtility#createReplyContent()というメソッドに任せています。
このメソッドの中で行われていることは、返信元メッセージの情報を元にquoteHeadingで与えられたテンプレートに従って返信メッセージ先頭の見出し部分を生成し、返信元メッセージの本文(最初のテキストパート)の各行の先頭にquoteSymbolで与えられた引用符を付加したものを生成するという二点です。
これで返信メッセージの初期値として必要な情報を返信元メッセージから生成することができました(*)。
後は、sendRedirect()によって、送信フォーム生成Servletに処理を任せています。
*:返信元メッセージの自分以外のTo:やcc:のアドレスも返信先に含めたいような場合は、もう少し処理を書き足す必要があります。Message#reply()をtrueを渡して呼び出す事でもそのような処理が行われますが、この処理はjavax.mail.Sessionオブジェクトに渡すPropertiesの"mail.from"を設定するなどによりInternetAddress.getLocalAddress()が正しい自分のメイルアドレスを返すことができる状態でないと意図しない結果になる可能性があります。
次に、送信に関する処理を作成します。このServletはメッセージ一覧画面から「メッセージ新規作成」にて、また、メッセージ画面から「返信」を押下する事によるReply Servletから呼び出される事を想定しています。基本的にHttpSession中の所定のキーに対する値が送信フォームの初期値として表示されるようにします。そのようにすると、一度「返信」等によりそれらのキーに値が設定されると、次に「新規作成」で呼び出された時も前回の「返信」時のデータが表示されてしまう問題が出ますので、どこかでHttpSessionの中身をクリアする必要があります。これは、このServletを呼び出す時に"create"というパラメタが付加されている場合にそこでHttpSessionのクリアを行うようにしました。GETとPOSTで切り替えるといった方法も考えられます。
// [SendForm.java]
package com.sk_jp.servlet.webmail_sample;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.sk_jp.text.Formatter;
import com.sk_jp.text.TextFormatter;
import com.sk_jp.servlet.MapByHttpSession;
public class SendForm extends WebMailBase {
// 新規メッセージ作成時にクリアするHttpSessionのキー
private static final String[] REMOVE_KEYS = {
"from", "subject", "to", "cc", "bcc", "reply_to", "content",
"in_reply_to", "references",
};
private Formatter formatter;
public void initSub() throws IOException {
formatter = new TextFormatter(
getClass().getResourceAsStream(
"/com/sk_jp/resources/webmail_sample/send.template"),
inputEncoding);
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null) {
errorSessionInvalid(response);
return;
}
if (existParameter(request, "create")) {
for (int i = 0; i < REMOVE_KEYS.length; i++) {
session.removeValue(REMOVE_KEYS[i]);
}
}
String from = (String)session.getValue("from");
if (from == null || from.length() == 0) {
session.putValue("from", session.getValue("user") + "@" + host);
}
print(response,
formatter,
new MapByHttpSession(session),
outputEncoding);
}
}
ソースコードはこれだけで済んでいます。まあ、実際送信フォームを表示するだけのServletですので大した処理は必要ないんですね。
このServletはdoGet()を定義しています。これはHttpServletResponse#sendRedirect()を経由して呼び出される場合があるため必然的にGETメソッドを使うようになるためです(本書で紹介しているServletはそのケース以外のものは全てdoPost()メソッドを用いています)。
HttpSessionを得るところは良いですね。その後、スーパークラスのexistParameter()というメソッドで"create"というパラメタが存在するか否かをチェックしています。存在した場合はHttpSessionから、REMOVE_KEYSとして定義される配列内の全てのキーを削除しています。これが先程書いた「新規作成」の場合のゴミ掃除処理です。
次の処理は若干おまけ的な機能なのですが、「新規作成」「返信」に関らずFrom:の初期値として、ログイン時のアカウント情報を元に生成したメイルアドレスを設定しています。
最後にHttpSessionをMapでラップしたものをパラメタとして、TextFormatterによるフォーマット出力を行います。
送信フォーム用のテンプレートは以下のようなものを使います。
[send.template]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<META http-equiv="Content-Style-Type" content="text/css">
<TITLE>WebMail send</TITLE>
</HEAD>
<BODY>
<div style="text-align:center">
<H1>Send Message</H1>
<FORM method="POST" action="com.sk_jp.servlet.webmail_sample.Send">
<TABLE border="1" cellspacing="0">
<TBODY>
<TR>
<TD>From</TD>
<TD><INPUT size="60" type="text" name="from" value="@#html from@"></TD>
</TR>
<TR>
<TD>Subject</TD>
<TD><INPUT size="60" type="text" name="subject" value="@#html subject@"></TD>
</TR>
<TR>
<TD>To</TD>
<TD><INPUT size="60" type="text" name="to" value="@#html to@"></TD>
</TR>
<TR>
<TD>cc</TD>
<TD><INPUT size="60" type="text" name="cc" value="@#html cc@"></TD>
</TR>
<TR>
<TD>bcc</TD>
<TD><INPUT size="60" type="text" name="bcc" value="@#html bcc@"></TD>
</TR>
<TR>
<TD>Reply-To</TD>
<TD><INPUT size="60" type="text" name="reply_to" value="@#html reply_to@"></TD>
</TR>
<TR>
<TD colspan="2">
<TEXTAREA rows="24" cols="80" name="content">@#html content@</TEXTAREA>
</TD>
</TR>
</TBODY>
</TABLE>
<INPUT type="hidden" name="in_reply_to" value="@#html in_reply_to@">
<INPUT type="hidden" name="references" value="@#html references@">
<INPUT type="submit" name="send" value=" 送信 "> <BR>
</FORM>
</DIV>
</BODY>
</HTML>
このテンプレートを元にフォーマットされた結果の画面として、「返信」ボタンを押下した場合に出力される画面を以下に示します。

筆者の手軽に利用できる環境がWindows98であり、テスト用のメイルサーバとしてローカルホスト上でJAMESというサーバを使用している関係上、メイルアドレスおよびMessage-ID:のドメインパートが'localhost'等となっていますが、これはグローバルなメイルサーバを用いる場合は正しい文字列になります。
どうやら最低限の機能は実現できたようですね。
最後に送信フォームで入力された情報を元にメッセージを生成して送信を行うServletを作成してこのシステムの一応の完成となります。
メッセージ送信ServletではFORMで入力された情報を元にMessageオブジェクトを生成してSimpleSenderにより送信を行います。送信結果としては送信したMessageオブジェクトをMapByMessageでラップしたものを用いて出力画面を生成する事にします。
// [Send.java]
package com.sk_jp.servlet.webmail_sample;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.sk_jp.io.UnicodeCorrector;
import com.sk_jp.mail.MailUtility;
import com.sk_jp.mail.MapByMessage;
import com.sk_jp.mail.SimpleSender;
import com.sk_jp.text.Formatter;
import com.sk_jp.text.TextFormatter;
public class Send extends WebMailBase {
// ISO-2022-JPへの変換に必要
private static UnicodeCorrector corrector; // (1)
static {
try {
corrector = UnicodeCorrector.getInstance("ISO-2022-JP");
} catch (UnsupportedEncodingException e) {}
}
private Formatter formatter;
protected void initSub() throws IOException {
formatter = new TextFormatter(
getClass().getResourceAsStream(
"/com/sk_jp/resources/webmail_sample/sendcomplete.template"),
inputEncoding);
}
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
if (request.getSession(false) == null) {
errorSessionInvalid(response);
return;
}
String subject = getParameter(request, "subject", outputEncoding);
String from = getParameter(request, "from", outputEncoding);
String to = getParameter(request, "to", outputEncoding);
String cc = getParameter(request, "cc", outputEncoding);
String bcc = getParameter(request, "bcc", outputEncoding);
String replyTo = getParameter(request, "reply_to", outputEncoding);
String inReplyTo =
getParameter(request, "in_reply_to", outputEncoding);
String references =
getParameter(request, "references", outputEncoding);
String content = getParameter(request, "content", outputEncoding);
if (from == null) {
// Fromの到達性チェックまではまだやってないけど
error(response, "Fromが記述されていません",
"Fromは必ず記述してください。<br>" +
"Fromがメイルが届くアドレスでなければメイルの送信は" +
"行われませんのでご注意ください。");
return;
}
if (to == null) {
error(response, "Toが記述されていません",
"Toは必ず記述してください。<br>" +
"Toがメイルが届くアドレスでなければメイルの送信は" +
"行われませんのでご注意ください。");
return;
}
if (subject == null) {
error(response, "Subjectが記述されていません",
"Subjectのないメイルを送信すべきではありません。");
return;
}
try {
SimpleSender sender = new SimpleSender();
MimeMessage message = sender.createMessage();
// Unicodeの一部の文字はISO-2022-JPで出力するために補正が必要
message.setSubject(
MailUtility.encodeText(
corrector.correct(subject), "ISO-2022-JP", "B"));
message.addFrom(
MailUtility.parseAddresses(corrector.correct(from)));
message.setRecipients(Message.RecipientType.TO,
MailUtility.parseAddresses(corrector.correct(to)));
if (cc != null) {
message.setRecipients(Message.RecipientType.CC,
MailUtility.parseAddresses(
corrector.correct(cc)));
}
if (bcc != null) {
// これはメッセージから取り除かれるものなので
// 2バイト文字の補正は不要
message.setRecipients(Message.RecipientType.BCC,
InternetAddress.parse(bcc));
}
if (replyTo != null) {
message.setReplyTo(
MailUtility.parseAddresses(
corrector.correct(replyTo)));
}
message.setHeader("In-Reply-To", inReplyTo);
// setHeader()はfoldingを行わないため、
// 長くなる事が分かっているヘッダは自分でfoldingを
// 行う必要があります
message.setHeader("References", MailUtility.fold(references, 12));
message.setText(corrector.correct(content), "ISO-2022-JP");
sender.connect(host);
sender.send(message);
sender.disconnect();
print(response,
formatter,
new MapByMessage(message),
outputEncoding);
} catch (AddressException e) {
getServletContext().log(e, "Got exception:");
error(response, "メイルアドレスの指定に誤りがあります。",
"以下のメイルアドレスは不正です。<BR>[<STRONG>" +
e.getRef() +
"</STRONG>]<br>" +
"正しいメイルアドレスか確認してください。");
return;
} catch (MessagingException e) {
getServletContext().log(e, "Got exception:");
error(response, "メイルの送信に失敗",
"以下の例外によりメッセージの送信に失敗しました。<br>" + e);
return;
}
}
}
(1)の部分についてさきに説明しておきます。これは4.5.8で示した「一部の文字が化ける場合の対応」を行っています。詳細は4.5.8の方をご覧いただきたいのですが、入力エンコーディングが"Shift_JIS"になる可能性がある場合で、送信するメッセージは"ISO-2022-JP"とするような場合は、Unicodeの補正処理が必要になるということです。入力エンコーディング(この場合は入力フォームのHTMLのエンコーディング)がISO-2022-JPであれば、この補正は不要なのですが、本書のサンプルではServletの初期パラメータでエンコーディングを指定できるようにしているため、どのようなエンコーディングであっても動作するようにするために必要です。
さて、doPost()の処理についてですが、まずはHttpSessionを獲得しています。が、それはnullであるか否かのチェックのみです。Send.javaではHttpSessionの内容を参照することがないのですが、セッション継続中でなければ送信を行えないようにしています。この理由は、誰にでも送信できるようにはしたくないためです。
つまり、ログインに成功した人のみに送信を許可しているわけですね。ここではこれだけの処理にしていますが、この方法では実は機能的に万全ではありません。HttpSessionはタイムアウトが存在して、一定時間で自動的に消失してしまうため、送信するメッセージを書くのに長い時間がかかったりするとセッションが破棄されて送信に失敗してしまうのです。これは、HttpSessionを用いたセッション管理の問題点の一つであり、これに対応したい場合はCookieを直接利用する必要があります。
では、次に進みます。次はFORMの入力データを取り出してそれぞれ変数に格納しています。これらのパラメタ名は、send.templateファイルに記述されているものです。その後、To/From/Subjectについてのみ未入力のチェックを行っています。ここも本来はもっといろいろチェックすべきでしょうけれど、サンプルですので最低限のものにしています。
次は送信メッセージの生成です。まず、SMTP用のSimpleSenderオブジェクトを生成し、このオブジェクトからMimeMessageオブジェクトを獲得します。このあたりは3章のSimpleSenderの項で説明していましたね。獲得した空のMimeMessageオブジェクトに対して、各ヘッダや本文の設定を行っていきます。ここもReply.javaの時のように一つずつ見ていく事にしましょう。
// Unicodeの一部の文字はISO-2022-JPで出力するために補正が必要
message.setSubject(
MailUtility.encodeText(
corrector.correct(subject), "ISO-2022-JP", "B"));
まず、Subject:を設定しています。Message#setSubject()を用いています。MimeMessage#setSubject(subject,
charset)の方を用いていないのは、4章の「JavaMailにおけるSubjectのエンコーディングスキームについて」に記した理由からです。JavaMailのsetSubject()での自動エンコードに頼らず、明示的にエンコードを行ってASCIIのみに変換された文字列を設定しているのです。Subject:のエンコードにはMailUtility#encodeText()という筆者のライブラリのメソッドを用いています。こちらは4.5.3の「JavaMailでのヘッダのエンコードにおける問題」で示したメソッドですが、javax.mail.internet.MimeUtility#encodeText()を用いても大きな問題はありません。このメソッドのパラメタとして、corrector.correct(subject)と、"ISO-2022-JP",
"B"を渡しています。corrector.correct(subject)は先程書いた通り、4.5.8の「一部の文字が化ける場合の対応」に示した、Unicodeの特定のコードの置換を行うというものです。
つまり、FORMのパラメタの文字列に対して、それを構成するUnicodeをISO-2022-JP向けに補正し、それを'B'エンコーディングした文字列を得てSubjectに設定しているわけです。
message.addFrom(
MailUtility.parseAddresses(corrector.correct(from)));
message.setRecipients(Message.RecipientType.TO,
MailUtility.parseAddresses(corrector.correct(to)));
if (cc != null) {
message.setRecipients(Message.RecipientType.CC,
MailUtility.parseAddresses(
corrector.correct(cc)));
}
if (bcc != null) {
// これはメッセージから取り除かれるものなので
// 2バイト文字の補正は不要
message.setRecipients(Message.RecipientType.BCC,
InternetAddress.parse(bcc));
}
if (replyTo != null) {
message.setReplyTo(
MailUtility.parseAddresses(
corrector.correct(replyTo)));
}
次は各種アドレスを表すヘッダの設定です。addFrom()を見るとFrom:には、FORMから取得した文字列のUnicodeの補正を行ったものに対して、(またしても)筆者のライブラリメソッドであるMailUtility#parseAddresses()を呼び出した結果を設定しています。このメソッドについて説明する前に、機能仕様についてもう一度確認しておきましょう。
Reply.javaのところではそれを前提のように書いていましたが、このWebMailの送信フォームのメイルアドレス入力欄は、「木下<shin@sk-jp.com>」のように個人名の記述を可能としています。本来なら、個人名とメイルアドレスは別個に入力欄を設けた方がよいのですが、何れのアドレス欄についても任意このアドレスを記述することができるため、この方法を実現しようと思うとユーザインターフェイスの作成がかなり難しくなります。従って、「木下<shin@sk-jp.com>」のような表記を許し、これを'カンマ区切りで入力する事で複数のメイルアドレスの指定を可能としたわけです(これは一般的なMUAのユーザインターフェイスと同じといえます)。
ただ、JavaMailのInternetAddressクラスには、「木下<shin@sk-jp.com>」という文字列を正しくInternetAddressオブジェクトに設定する手段が提供されていません。
InternetAddress#parse()というメソッドは元々受信したメッセージのヘッダを解析するためのものですので、"木下"の部分がエンコード済みであることを前提としており、このメソッドを用いるとInternetAddressオブジェクトの内部情報が不正になって個人名部分が正しく出力されないのです(その原因の説明は省略します)。また、コンストラクタは個人名とメイルアドレスを別々に渡す必要があります。
MailUtility#parseAddresses()というメソッドは個人名部分に非ASCII文字がそのまま使われている文字列を適切にInternetAddressオブジェクトの配列に変換するというもので、以下のような実装になっています。
public static InternetAddress[] parseAddresses(String addressesString)
throws AddressException {
if (addressesString == null) return null;
try {
InternetAddress[] addresses =
InternetAddress.parse(addressesString, true);
// correct personals
for (int i = 0; i < addresses.length; i++) {
addresses[i].setPersonal(
addresses[i].getPersonal(), "ISO-2022-JP");
}
return addresses;
} catch (UnsupportedEncodingException e) {
throw new InternalError(e.toString());
}
}
InternetAddress#parse()で得たInternetAddressオブジェクトの配列の各要素に対して、address.setPersonal(address.getPersonal(), "ISO-2022-JP")とする事で個人名部分の補正が行われます。取り出して再設定を行っているというわけです。なぜこれで正しくなるかの説明はやはり省略させていただきます。
さて、話を戻しまして、以降の処理についてですが、これらは同様の手順でTo: cc: bcc: Reply-To: の設定を行っているというわけです。
message.setHeader("In-Reply-To", inReplyTo);
// setHeader()はfoldingを行わないため、
// 長くなる事が分かっているヘッダは自分でfoldingを
// 行う必要があります
message.setHeader("References", MailUtility.fold(references, 12));
次はIn-Reply-To:とReferences:が指定されている場合はそれらを設定するという処理です。In-Reply-To:の方は単にPart#setHeader()を呼び出しているだけです。
References:の設定についてはコメントにも記した通り、foldingを行うためにMailUtility.fold()というメソッドを呼び出した結果を設定しています。
このメソッドについては、4章のコラム「folding/unfoldingについて」に記した通りで、文字列が長い場合に適宜<CRLF>と空白を挿入するというものです。
message.setText(corrector.correct(content), "ISO-2022-JP");
メッセージ生成の最後は本文の設定です。ここでも本文中の特定の文字に対する補正を行ってからMimePart#setText()に渡しています。
Messageオブジェクトの設定が完了したら、SimpleSenderを使ってconnect/send/disconnectでそのメッセージを送信しています。
これが成功すれば、送信したメッセージをMapでラップしてTextFormatterにより結果画面の出力を行っています。送信結果画面のテンプレートは以下のようになっています。
[sendcomplete.template]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<META http-equiv="Content-Style-Type" content="text/css">
<TITLE>Send complete</TITLE>
</HEAD>
<BODY>
<DIV style="text-align:center">
<H1>送信しました</H1>
<FORM method="POST" action="com.sk_jp.servlet.webmail_sample.MessageList">
<INPUT type="submit" value=" メッセージ一覧へ ">
</FORM>
<TABLE border="1">
<CAPTION>送信内容</CAPTION>
<TBODY>
<TR><TD>From</TD><TD>@##htmlfrom@</TD></TR>
<TR><TD>Subject</TD><TD>@#html subject@</TD></TR>
<TR><TD>To</TD><TD>@##htmlto@</TD></TR>
<TR><TD>cc</TD><TD>@##htmlcc@</TD></TR>
<TR><TD>bcc</TD><TD>@##htmlbcc@</TD></TR>
<TR>
<TD colspan="2">
<PRE>@#html content@</PRE>
</TD>
</TR>
</TBODY>
</TABLE>
</DIV>
</BODY>
</HTML>
以下が送信成功時の画面です。

以上で、最低限の機能ではありますがWebMailの完成ということになります。添付ファイルへの対応などはこのプログラムを基礎として拡張しやすいようにしたつもりです。JavaMailの機能を普通に使うだけでも比較的簡単に送受信機能は作れるわけですが、日本語に正しく対応したり、RFCに忠実なメッセージを生成するためには、やはりいくらかの知識が必要になりますね。筆者はWebページ上でこのサンプルを拡張したWebMailシステムを公開しているのですが、月に一通程度は、正常に受信できないメッセージが到着することがあります。インターネットは世界中の人がざっくばらんな規約(RFC)になるべく従ったいろいろなソフトを利用しているわけで、RFCでは「実装者の自由」とされているところで非互換が生まれていたり、RFCに一部故意に従わないようなソフトウェアもあります。
多くのユーザに利用されるソフトウェアを目指すからには、「送信は厳密に、受信は寛容に」の原則に従う他はありません。
間違った実装が含まれる多くのMUAの送出するメッセージを読めること、最低限RFCに準拠したメッセージしか送信しないことですね。そういう意味ではJavaMailは普通に使用すると「送信は厳密で受信も厳密」ですので、いいかげんな実装のMUAの送信するメッセージを正しく解釈できないわけですが、JavaMailはあくまでライブラリですので、地域固有の事情はアプリケーション側で対処する必要があります。世の中の多くのMUA作成者は、最も普及しているであろう大手ベンダのMUAが間違ったメッセージを送信していることに対して憤りを感じつつも、それぞれで対処してメッセージを正しく表示しようとしているわけです。そうなるとプログラムはどうしても回避コードの継ぎはぎのようになってしまいます。
このような手間を減らすためにもRFC準拠のメッセージが読めないMUAはどんどん指摘していくべきでしょう。また、ベンダは自社の製品のことばかり考えず、フリーソフトの制作者などと同じように、インターネットの参加者の一人であり、他の製品との相互通信性を確保することがもっとも重要であることを認識すべきです。
HTTPでのパスワードの送信について
ServletのようなWebベースのシステムでログイン機能を用いる場合、SSL等を使って通信路を暗号化していないと、どうしても生のパスワードがネットワーク上を流れる事になってしまう事に注意して下さい。WebMailの場合、APOP等を用いたとしても、まずはWebサーバに生パスワードを送信してからWebサーバとPOPサーバ間で暗号による認証が行われる事になりますので、HTTP通信路が暗号化されていなければ意味がなくなってしまいます(そもそもJavaMail添付のPOP3ProviderがAPOP対応していないことは置いておいて^^)。