はじめに

Java言語は、強力なネットワーク/オブジェクト指向言語としてすでに充分に広い認知を得るようになり、現在は拡張APIの充実や性能向上に関する研究に重点が置かれるようになっています。商用アプリケーションの開発にJavaが選択されることも珍しくなくなり、すでに趣味の言語の域を脱していることは皆さんのまわりを見ても実感できることがよくあるのではないかと思います(もちろんJava言語は最初からビジネスで利用されることを前提に作られていますが)。

本書はJavaMailというAPIに関する本でJava言語そのものを解説するものではありません。Java言語で簡単なプログラムを作ったことがあるというレベルの人が理解できるように書いています。ネットワークに関する知識についても、前提知識が多少はあった方が良いと思います。また、JavaMail自体はどちらかと言うとクライアントサイド向けに設計されたAPIで、メイラ作成支援機能といってよいものがいくつも含まれていますが、本書ではどちらかと言えばビジネスで多用される送信機能や、正しいメッセージの生成方法について詳しく記述しています。
ただし比較的低レベルな言語機能そのものについてのコラムも(筆者の趣味から)結構入っています。

本書で紹介するJavaMailは、Javaの標準拡張クラスライブラリとして、1997年暮れにEarly Accessが登場し、98年に入ってVersion 1.0がリリースされました。
2000年9月にはVersion 1.2の Early Accessが公開されており、本書が発行されるころには1.2の正式版も配布されていることでしょう。

Version 1.0からVersion 1.2に至るまでのAPIの大幅な変動はMessageContextに関連することくらいで、その他はほとんどがBugFixと簡便のためのメソッド(*)の追加です。これはつまり、JavaMail APIが柔軟な設計であり、十分なAPIが用意されていたため、変更する必要性があまりなかったということです。インターネットの約束ごと(RFC)としては新たなメッセージヘッダの定義がされるなどの拡張が随時なされていますが、それらを設定/参照する方法はもちろん最初から用意されていますし、新しいMIMEタイプを解釈する方法についてもJava Activation Frameworkという独立したパッケージに対して独自のMIMEタイプを解釈する部分のみを作成することで、JavaMailに手を加えることなく機能拡張が行えるようになっています。

JavaMail APIはJavaでプログラミングをしていこうという方にとって、絶対に知っておいて損がないAPIです。電子メイルが使われなくなることは現在は考えられませんし、JavaMail API仕様も安定しています。また、後に示しますが、Mailの送受信自体は単純なsocketプログラミングで作成できても、さまざまなRFCに準拠した実装にして、テキスト以外のメッセージも処理できるようにするとなると大変なことですので、そのような部分をライブラリに任せられるのは大きいでしょう。また、その設計思想を理解することは、他のオブジェクト指向プログラムの設計を行う際にも大変参考になるはずです。

本書ではJavaMail1.2 + JAF1.0.1という組み合わせを、JDK1.3(Java(TM) 2 SDK, Standard Edition Version 1.3)上で動作させています。サンプルプログラムの動作確認等はこの環境で行っていますが、新たなバージョンでも問題が発生することは少ないでしょう。もし、より新しいバージョンを用いて、deprecated(推奨されない)という旨のコンパイル時warningが発生した場合は、API Documentの該当クラスやメソッドの項を確認して、修正を行っていただく必要があるかもしれません。逆に、JDK1.1環境で実行しようとする場合については、JDK1.2で追加されたCollection APIなどを使用している箇所についてはJDK1.1のAPIに書き換える必要があります(*)。

*:主に他の公開APIを呼び出して複雑なパラメタの指定を省略できるようにしたメソッドを指します。本書ではコンビニエンスメソッドで統一します。

*2: JavaMail API自体はJDK1.1環境でも動作可能であり、それが売りの一つでもあります。ただし、本書でのサンプルプログラムとしては、コレクションライブラリ関連等がJava 2を用いた方がすっきり記述できるため、Java 2を対象としました。また、現在のJavaMailはProviderを検索する手段としてJava 2の機能を用いており、JDK1.1では僅かな機能制限もあります。

deprecatedなAPIは、ソースコード上のJavaDoc用コメント内に@deprecated 代替手段と記述することで実現されます。これはアプリケーションプログラムであっても同じです。コンパイラがコメント欄を見てくれるわけですね。

謝辞

すぐに現実逃避してしまい時間を有効に使えない癖に、執筆活動だからと称して生まれたばかりの娘の世話を任せっきりにしてしまった妻に感謝します。また、そんな親父に対していつも笑顔をふりまいてくれた娘に感謝します(筆者の顔が面白いだけらしいという話もありますが)。また、忙しい中時間を裂いて本書を査読いただき、有益な助言/励ましをいただいた、西本 圭佑氏、安島 克憲氏、野中 愛氏、佐藤 治氏に感謝いたします。また、大変有益な技術資料の宝庫であり、筆者の現在の知識の土台ともなったJavaHouseメイリングリストと、そのメンバの多大なる貢献に感謝いたします。

1章 JavaMailの概要

お決まりの「JavaMailとは?」。

JavaMailは標準拡張API

JavaMailは、Java言語上からメッセージ送受信を行うためのクラスライブラリで、Standard Extention(標準拡張ライブラリ)という妙な位置づけになっています。Standard Extentionは、オープンで標準的なJavaAPI群とされています。要するに、コアライブラリが肥大化してしまっているので、拡張ライブラリという位置付けにするわけですが、同じようなクラスライブラリが乱立しても困るので、標準化プロセスを経てオープンにされたものと解釈すればよいと思います。Standard Extentionライブラリはそれぞれが独立したjarファイルにまとめられていますが、別のライブラリへの依存関係等をmanifestファイルに記述しておくことで、そのライブラリがインストールされていなくてもネットワーク越しに参照したりできるようになっています(*1)。

Standard Extentionとなっているクラスライブラリは基本的にjavaxパッケージに属しており、以下のようなものが提供されています(全てではありません)。

javax.naming JNDIと呼ばれる、ディレクトリサービスへのアクセスを提供するためのAPI群です。
javax.servlet こちらも割とおなじみのServletパッケージ。サーバ上で呼び出される部品であるServletのAPI群です。これについては後で使用例が出てきます。
この中(サブパッケージ)にはJSP(Java Server Pages:javax.servlet.jsp)パッケージも含まれています。
javax.activation MIMEタイプに応じた(ビューアなどの)オブジェクトを構築するクラスライブラリです。
JavaMailはこのライブラリの機能を用いてマルチパートメッセージの解釈を行いますので、JavaMailにとって必須のパッケージとなっています。
javax.mail メッセージの送受信プロトコルをカプセル化したAPIです。本書ではこのパッケージとjavax.activationパッケージが主役です。
javax.datasync モバイルコンピュータとのデータ同期などをサポートするパッケージのようです。
javax.telephony JTAPIと呼ばれる、Javaから電話をかけるためのAPI群です。
javax.media マルチメディアを扱う拡張パッケージ群です。複雑なイメージ処理をサポートするjavax.media.jaiや、サウンド処理機能を提供するjavax.media.sound、3Dレンダリング等を行うAPIがまとめられたjavax.media.j3d/javax.vecmath等があります。
javax.security 拡張セキュリティ関連パッケージ群です。SSLや暗号化/複合化機能をサポートします。
javax.net
javax.crypto
javax.transaction 分散オブジェクト環境におけるトランザクション管理機能を提供するAPI群です。
javax.comm シリアルポートを扱うプログラミングのための標準API群です。
javax.xml.parsers JAXPパッケージ(Java API for XML Parsing Optional Package)です。SAX/DOMといったXMLを扱う標準APIにXML文書をparseするAPIが規定されていなかったために、さまざまなXML Parserでparseの方法がまちまちになってしまっています。
JAXPを使うことで共通のParser APIにより、さまざまなXML Parserを利用できるようになっています。
javax.ejb Enterprise JavaBeansフレームワークのコアAPI群です。

なお、javaxというパッケージ名が用いられているものが全て標準拡張APIというわけではなく、コアAPIに吸収されているものもあります。もちろんjavax以外のパッケージでも標準拡張APIというカテゴリに分類される場合もあります。

javax.swing おそらくおなじみのswingパッケージ。GUIコンポーネント群です。
javax.accessibility ユーザインターフェイスコンポーネントが実装すべきinterfaceとそれにアクセスするためのユーティリティクラス群です。
javax.rmi CORBAサポートを含むRMIの拡張です。

*1:拡張機能機構と呼ばれています。拡張機能機構にはJDKおよびJREインストールディレクトリ配下のlib/extディレクトリにjarファイルを配置するインストール型と、jarファイル内のmanifestファイルの情報によって依存するライブラリを指し示すダウンロード型があります。

jarファイルとは

jarファイルはJavaにおけるアーカイブ(書庫)の標準フォーマットです。クラスライブラリや、それらが参照するリソース(画像やサウンド/テキストなど)を一つのファイルにまとめます。まとめるだけでなく圧縮する機能も持っており、これはZIPフォーマットとコンパチブルです。圧縮するか否かはjarコマンドの0(ゼロ)オプションで指定します。

manifestファイルとは

jarファイルには、プログラマが作成したクラスやリソースファイル群の他に、META-INF/manifest.mfというファイルも格納されます。このファイルにはjarファイル内にまとめられたファイルがどのようなものであるかを示す情報をテキストで示すことができます。jarコマンドでmanifestファイルを指定しないと、デフォルトのmanifestファイルが生成されてjarファイルに追加されます。

>jar cf foo.jar *.class
とした後、
>jar xf foo.jar

として、jarファイルを展開して見るとデフォルトのmanifestファイルが見れます。

[META-INF/MANIFEST.MF]

Manifest-Version: 1.0
Created-By: 1.3.0 (Sun Microsystems Inc.)

明示的にmanifestファイルを指定する例を示します。以下の例では、executable jar fileを作成しています。

>jar -cmf foo.mf foo.jar com

[foo.mf]

Manifest-Version: 1.0
Main-Class: com.sk_jp.mail.formatter.Main
Class-Path: mail.jar activation.jar
Created-By: 1.3.0 (Sun Microsystems Inc.)

Name: com/sk_jp/mail/formatter/Main.class

これで、生成されたjarファイルに対して、

>java -jar foo.jar

とするだけで、

>java -classpath "foo.jar;mail.jar;activation.jar" com.sk_jp.mail.formatter.Main

相当の結果が得られます。Windowsであれば、foo.jarファイルをダブルクリックするだけで同じ結果となります。これは配布する場合に便利ですね。manifestファイルで指定できる情報はもちろんこれだけではありません。詳細はJDKドキュメントの「機能ガイド」の「Java Archive (JAR) の機能」を参照してください。

JavaMailの仕組み

JavaMailの機能は当然のことながら、メッセージの送信/受信です。ただし、その方法については柔軟性を持たせています。メッセージの送信を行うための役割はjavax.mail.Transport、受信を行うための役割はjavax.mail.Storeというクラスに抽象化されています。JavaMailのAPIのコア部分には、特定のプロトコルに依存したクラス/メソッドはありません。特定のプロトコルに依存した部分はTransport/Storeおよびいくつかのクラスのサブクラスを提供して、propertiesファイルによって実行時にバインディングすることによって処理されます。これらの特定のプロトコルに依存した部分をProvider(プロバイダ)と呼びます。

JavaMail1.2にはIMAP4/POP3/SMTPの3種類のプロトコルを実装したプロバイダが同梱(*2)されています。これらのプロトコルについては2章で、各プロバイダの使用方法については3章で説明します。ちなみにJavaMail1.1.3まではPOP3プロバイダは別配布でした。

また、JavaMail本体がAPI仕様だけを規定したおかげで、サードパーティのProviderも多く提供されています。この中にはNNTP(NetNews転送プロトコル)をサポートしたProviderもありますし、mboxプロバイダというものもあったりします。

JavaMailAPIとProviderの関係は以下のようになります。

アプリケーションはJavaMail APIであるjava.mail.Sessionオブジェクトを取得し、SessionオブジェクトからTransportまたはStoreオブジェクトを取得してメッセージの送信または受信を行います。

Transport/Storeは抽象クラスとなっており、Sessionが返すオブジェクトはこれらのサブクラスとなります。つまり、どのような方法で送信を行うか、どのような方法で受信を行うかという点に関してはサブクラスでの実装に委ねられているというわけです。

Sessionクラスとは、メッセージ送受信時に行われるセッションを抽象化しており、メイルアプリケーション内で同じセッションを共有したり、異なるセッションを並列に用いることが可能です。メイラで言うところのマルチアカウント機能をイメージすればよいでしょう。

*2: ProviderはJavaMail APIに含まれているわけではありません。JavaMailのアーカイブに含まれているSMTP/IMAP4/POP3の各Providerがcom.sun.mailパッケージ配下になっていることからも解ると思います。と言っても通常は見えませんが。mail.jarファイルを展開すると解ります。

次にメッセージ本体について見てみましょう。当然のごとくJavaMail APIではMessageというクラスで送受信を行うメッセージの抽象化が行われています。このクラスは文字どおり、あくまで「メッセージ」であるため、現在一般的に使われているインターネットメイル(RFC822)の形式に限りません。
ただし、本書ではJavaMailの適用例として最も多いであろうインターネットメイルに焦点を絞りますので、他の形式のメッセージの使用については置いておくとして、最近のインターネットメイルではHTML形式のメイルや、ファイルを添付したり画像/音声等をメイルに埋めこんで送信することが当たり前のように行われています。
これらはマルチパートと呼ばれ、MIME(Multipurpose Internet Mail Extension:多目的インターネットメイルのための拡張[RFC2045])でフォーマットの規定が行われたものです。

JavaMailにはMessageのサブクラスとしてMimeMessageクラスが存在します。これは、インターネットメイルを表すクラスであり、JavaMailの中核をなすといってよいでしょう。このクラスには、ボディ(Content)の設定/取り出しを行うsetContent()/getContent()というメソッドが宣言されています。(*3)
このメソッドには画像オブジェクトや音声オブジェクト、HTMLを表すテキストなどを渡すことができ、取り出すときも同様にそれらのオブジェクトを取り出すことができるようになっています(*4)。
実際にメッセージを送受信する際に、JavaMailの使用者がこれらのデータタイプに応じたエンコード/デコードを意識する必要はありません。これは一体どうなっているのでしょう。

現在MIMEはメイルとは無関係な世界にも使われるようになっています。例えば、Webで配信される情報にもメタ情報としてContent-Type:と呼ばれるヘッダ情報が含まれますが、このヘッダで示されるものはMIMEタイプと呼ばれ、そのボディがどのような種類のオブジェクトであるかといったリソースの型を表しています。そして、JavaではこれらのMIMEタイプごとに実体が異なるさまざまなオブジェクトに統一的にアクセスするためのAPIとしてJAF(JavaBeansTM Activation Framework)というパッケージをリリースしています。
このパッケージは、各MIMEタイプ毎に対応するオブジェクトを適切にストリーム化したり、受信したバイト列から(MIMEタイプに従って)Javaのオブジェクトを生成したりする機能に加え、各種のオブジェクトに固有な操作を実行するためのクラスを用意して登録することで、同じインターフェイスで複数の種類のオブジェクトを処理できる優れものです。
JavaMailはメイルのボディであるContentに対してJAFの抽象クラスであるDataHandlerを設定することで、テキストに限らないオブジェクトをMimeMessageオブジェクトに設定することができることになっています(仕様上は)。送信時はJAFの機能によりオブジェクトが転送用のストリームに変換され、JavaMailがそれに転送用エンコードを施したものが送信されます。受信時も同様にJAFの機能を利用してオブジェクトの復元を行ってくれます。つまり、MIME対応の主要な部分はJavaMail本体から切り離された別パッケージに任されているというわけです。JAFについては3章でもう少し詳しく説明します。

また、メッセージのボディとして設定できるオブジェクトの一つとして、マルチパートという種別が存在します。マルチパートは複数のパートを格納できるコンテナとしての役割を持ちます。そして、そのそれぞれのパートがメッセージと同じようにボディを持てるようになっており、これによって複合コンテンツを表現できるようになっています。詳しくは2章で説明します。

*3: 余談ですがJava言語仕様書では「定義」という用語は用いず、全て「宣言」と呼ぶようです。なぜかは解りませんが。本書では、上記の箇所以外では適宜「定義」という用語を用います。
*4:Javaオブジェクトを直接setContent()する場合は、JAFにそのオブジェクトに対応するDataContentHandlerがインストールされている必要があります。Javaオブジェクトではなくファイル(バイト列)としてであればどのような形式のものでも問題ありません。

interfaceとインターフェイス

本書では「interface」と「インターフェイス」という用語を使い分けています。「interface」は言わずと知れたJava言語仕様のinterfaceキーワードによって宣言されるものを表し、「インターフェイス」は「特定のプログラムが持っている公開されている機能」(メソッドという用語をより大域的に表すもの)といった意味になります。「インターフェイス」はJava言語に限らず使われます。
例えばC言語で「メモリ操作インターフェイスとしてmalloc/free関数などがあります」とか、Javaでは「java.io.Readerクラスには行単位の読み込みインターフェイスを持っていない」とか、「ユーザインターフェイス」なんて言葉もありますしね。

ところで「class」と「クラス」はそこまで異なる意味としての使い分けは特にされていませんね。しかし、こちらも基本的に「class」は言語機能上のclassキーワードにて宣言されるもので、「クラス」はもう少し大域的にオブジェクト指向設計における「役割を表すもの」位の感覚と思っていただければ良いのではないでしょうか。

しかしinterfaceが役割そのものを表すのに、クラスも役割を表すものなどといってしまうと混乱してしまいそうですね。これは、「interfaceはclassではないがクラスの一種である」ということになるのですが、解るでしょうか…。
言語機能としてはinterfaceとclassは別のものですが、「クラス」の機能のうち、そのクラスの「外部仕様」のみを表現するための言語機能がinterfaceなのですね。

JavaMailのパッケージ

JavaMail1.2のパッケージは以下のように分類されています。

javax.mail

メッセージ送受信機能を抽象化したAPI群。次のjavax.mail.internetに記すように、このパッケージのレベルではターゲットがInternetMailに限りません。以下のようなinterface/classが提供されます。

メッセージを構成する部品群
Part/BodyPart/Header/Address/MultiPart/Message・・・

メイルセッション管理
Session/Service/Provider/Transport/Store/Folder/Authenticater/PasswordAuthenticater・・・

javax.mail.internet

インターネットにおける標準的な電子メイル転送プロトコルを念頭に置いたAPI群です。javax.mailのinterfaceを継承してInternetMailメッセージに特有の機能が追加されています。以下のようなinterface/classが提供されます。

メッセージを構成する部品群
MimePart/MimeBodyPart/MimeMultiPart/MimeMessage/InternetHeaders/InternetAddress/NewsAddress/ContentType/ContentDisposition・・・

javax.mail.event

JavaMailは、メッセージの内容の変更や送信/受信時等に独自の処理を組み入れるために、さまざまな操作に対してイベントを発生させる仕組みを持っています。JDK1.1から採用されたイベントモデルのおかげで、元のクラスをサブクラス化しなくても、フック処理のインストールができるようになっています。以下のイベントに対応するクラスセットが提供されます。

ConnectionEvent
FolderEvent
MessageChangedEvent
MessageCountEvent
StoreEvent
TransportEvent

javax.mail.search

IMAP4プロトコルでは、MUA(2章参照)が行うようなメッセージの検索などの機能もサーバが提供しています。これは、IMAP4プロトコルがサポートする検索コマンドをAPI化したものです。従って、POP3プロトコルにおいては意味のないものとなってしまいますが、そこは、実装しだいで、それこそアプリケーションはIMAP4かPOP3かの意識を全く行わなくてすむように、この検索機能がPOP3プロトコル上でも使えるようにPOP3Provider(後述)の実装を行うこともできます(ただし、SunのPOP3Providerはそのような実装にはなっていません)。

このクラス群については4章で触れますが、メイラを作成する場合以外で使用する頻度は少ないと考えられるので、本書では詳しくは掘り下げないことにします。

JavaMailを使う利点

2章で述べるように、メッセージ送受信のセッションは単純なコマンドのやりとりですので、JavaMailを使わなくても比較的簡単に実装できるといってよいでしょう。となると、なぜJavaMailなのでしょうか?

以下にJavaMailを用いずにプレインテキストメッセージを送信するプログラムを示してみましょう。

// [SMTPTransportSample.java]
import java.io.*;
import java.util.Date;
import java.net.Socket;

/**
 * 5つのパラメタ(host, from, to, subject, contentFile)を指定して
 * ファイル内容を送信します。
 */
public class SMTPTransportSample {
    public static void main(String[] args) throws Exception {
        sendMessage(args[0], 25, // <- SMTP port
                    args[1], new String[] {args[2]},
                    args[3], args[4]);
    }

    public static void sendMessage(String host, int port,
                                   String from, String[] to,
                                   String subject, String contentFile)
                throws IOException {
        SMTPTransportSample smtp =
                new SMTPTransportSample(host, port);

        smtp.sendCommand("HELO " + from.substring(from.indexOf('@') + 1));
        smtp.sendCommand("MAIL FROM:<" + from + '>');

        for (int i = 0; i < to.length; i++) {
            smtp.sendCommand("RCPT TO:<" + to[i] + '>');
        }

        smtp.sendCommand("DATA");
        // contents
        StringBuffer toBuf = new StringBuffer(to[0]);
        for (int i = 1; i < to.length; i++) {
            toBuf.append(", ").append(to[i]);
        }
        smtp.send("To: " + encode(toBuf.toString()));
        smtp.send("From: " + encode(from));
        smtp.send("Subject: " + encode(subject));
        smtp.send("Date: " + getRFC822Date());
        smtp.send("MIME-Version: 1.0");
        smtp.send("Content-Type: text/plain; charset=ISO-2022-JP");
        smtp.send("");

        // read on default encoding
        BufferedReader dataIn =
                new BufferedReader(new FileReader(contentFile));
        String line;
        while ((line = dataIn.readLine()) != null) {
            smtp.send(line);
        }
        smtp.send(".");
        smtp.checkResponse();

        smtp.sendCommand("QUIT");

        System.out.println("Send complete");
    }


    private Socket         socket;
    private BufferedReader in;
    private PrintWriter    out;
    public SMTPTransportSample(String host, int port) throws IOException {
        try {
            socket = new Socket(host, port);
            in = new BufferedReader(
                    new InputStreamReader(
                            socket.getInputStream()));
            out = new PrintWriter(
                    new OutputStreamWriter(
                            socket.getOutputStream(), "ISO-2022-JP"));

            // skip welcome message
            checkResponse();
        } catch (IOException e) {
            close();
            throw e;
        }
    }

    public void sendCommand(String command) throws IOException {
        send(command);
        checkResponse();
    }

    public void send(String message) {
        System.out.println("S: " + message);
        out.print(message);
        out.print("\r\n");
        out.flush();
    }

    public void checkResponse() throws IOException {
        String response = in.readLine();
        System.out.println("R: " + response);

        if (response.charAt(0) != '2' &&
            response.charAt(0) != '3') {
            throw new IOException(response);
        }
    }

    public void close() {
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {}
        }
        if (out != null) {
            out.close();
        }
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException e) {}
        }
    }

    // ヘッダのエンコード処理が入っていませんので、ヘッダに日本語を用いると
    // いわゆる「生JIS」になってしまいます。
    private static String encode(String src) {
        // not implement
        return src;
    }
    // これは正しい形式ではありません。
    private static String getRFC822Date() {
        // not implement
        return new Date().toString();
    }
}

プログラムが行っているのはSMTPサーバに接続して決まった順番にコマンドを送信しているだけです。コマンドの意味などは2章で説明します。

たったこれだけのプログラムでメイルの送信ができてしまいました。しかし、知っている人からみれば、当然このプログラムは危険がいっぱいあります。実運用に耐えられるようなプログラムにするためには、この基本の流れは変わらずともたくさんのチェック/エンコーディング処理を挿入しないと、いろいろなパラメタを与えた時にさまざまな問題がでることでしょう(実際ヘッダのエンコーディングやDate:の処理は未実装です)。

そして、前のプログラムと同じように、特にチェックも行わずに単に送信を行うだけのJavaMailを用いたプログラムを以下に示します。

// [JavaMailTransportSample.java]
import java.io.*;
import java.util.Date;
import java.util.Properties;

import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.InternetAddress;

/**
 * 5つのパラメタ(host, from, to, subject, contentFile)を指定して
 * ファイル内容を送信します。
 */
public class JavaMailTransportSample {
    public static void main(String[] args) throws Exception {
        StringWriter content = new StringWriter();
        PrintWriter out = new PrintWriter(content);

        // read on default encoding
        BufferedReader contentIn =
                new BufferedReader(new FileReader(args[4]));
        String line;
        while ((line = contentIn.readLine()) != null) {
            out.println(line);
        }
        contentIn.close();

        sendMessage(args[0], 25,
                    args[1], new String[] {args[2]},
                    args[3], content.toString());
    }

    public static void sendMessage(String host, int port,
                                   String from, String[] to,
                                   String subject, String content)
                throws IOException, MessagingException {
        Properties props = System.getProperties();
        props.put("mail.smtp.host", host);
        Session session = Session.getDefaultInstance(props, null);

        // Messageオブジェクトを構築します。
        MimeMessage msg = new MimeMessage(session);

        // 宛て先(To:)の設定
        for (int i = 0; i < to.length; i++) {
            msg.addRecipients(MimeMessage.RecipientType.TO,
                              InternetAddress.parse(to[i], false));
        }

        // From: Date: の設定
        msg.setFrom(InternetAddress.parse(from, false)[0]);
        msg.setSentDate(new Date());

        // Subject: 本文の設定
        msg.setSubject(subject, "ISO-2022-JP");
        msg.setText(content, "ISO-2022-JP");

        msg.writeTo(System.out);

        Transport.send(msg);

        System.out.println("Send complete");
    }
}

プログラムはかなり短くなりました。これはまあ、ライブラリを呼び出すだけですむようになっているのだから当たり前として、それ以外に重要な相異を挙げていきます。

まず一つは、SMTPのコマンドに対する意識というのがなく、サーバからのレスポンスコードのチェックを書く必要がなくなっています。サーバとのやりとりで起こる失敗はMessagingExceptionといったJavaの例外によって通知されようになっています。また、メイルアドレスの書式が正しいか否かのチェックもなされます。SMTPTransportSampleに書かれたレスポンスのチェック処理は本当に最低限のものですし、サーバがちょっとレスポンスを間違えたりすると途端に問題が発生します。

次に、From:To:といったヘッダの設定は、MimeMessage(インターネットメイルの書式を抽象化)のメソッドとして定義されているので、メッセージ本体のヘッダ部分をテキスト処理で操作する必要がなくなっています(実際には多少補正は必要になりますが)。SMTPTransportSampleクラスではヘッダに対する"B"エンコード(2章参照)処理を省略していますが、この処理を書くと当然の如く書かなければならない処理の量が増えてしまうことでしょう。WindowsからWindowsに送信したりすると一見正しく日本語が表示されてしまうこともあるので気付かないことが多いんですが、これはRFC2047での定義(ヘッダにUS-ASCII以外の文字コードが含まれてはならない)に反しています(JISコードであれば文字コードとしての定義には反してはいませんが、エンコーディングされていないと相手がJISコードであることを解釈してくれなければ読めないことになります)。JavaMailTransportSampleでは、Subject:の符号化(エンコード)も行っています。JavaMailが変換機能を提供してくれているわけです。JavaMailを使うことでRFCで規定されている形式/プロトコルを守ることが容易になります。

さて、まとめますと、要するに完全なプログラムにするために自分で書かなければいけない部分が大幅に減った、というのが利点ということになるわけですが、これは楽になるからという意味だけではありません。より重要なのは、多くのコードを書かなければならないということは、それだけテストにかかる時間が増加するということです。メイルを扱うプログラマが同じように意識することはほとんどJavaMail内部でやってくれて、その詳細処理部分のテストは終わっているわけです(いや、もちろん今も続けられていますが我々がやる必要はなくなっていますね)。

そして、このサンプルでは書かれていませんが、JavaMailではMIMEで定義されるマルチパートメッセージのサポートもしっかりしています。テキスト以外のメッセージの送受信も上記サンプルと同じような感覚で行うことができてしまいます。SMTPTransportSampleの方式でマルチパートに対応するのは大変でしょう。

さて、便利便利と叫ぶのはこの辺にして、次章から実際にJavaMailで可能なことを具体的に見ていきましょう。

Tips
このコラムではサンプルプログラム中で使われている手法の中で知っておくとちょっと便利なTipsやイディオムについて触れていきます。
main(String[] args) throws Exceptionと例外処理の基本

mainメソッドでthrows Exception の記述を行っておくことで、try 〜 catchの記述を省略できるようになります。これはサンプルプログラムやデバッグ中のプログラムでは大変便利です。中には、try 〜 catch (Exception e) {}などと書いてしまう人もいるのですが、これでは例外が発生してもそれがどのような例外かを知ることができなくなってしまい、何のための例外機構か解らなくなってしまいます。このように書いてしまうということは、Javaの例外機構を煩わしいものと思ってしまっている可能性があります。注意しましょう。

もちろん、

try {
  :
} catch (Exception e) {
    e.printStackTrace();
}

と書くのが常套手段なのですが、この方法にも弊害があります。このように書くのに慣れてしまうと、製品版としてリリースする段階になってもcatch (Exception e)という記述を改めることを忘れがちになります。本来、例外はthrowsで宣言されている具体的な例外毎にエラー処理を分けるべきなのに、この記述では全ての例外がこのcatch節に集まってしまいます。この上、

} catch (Exception e) {
    if (e instanceof FileNotFoundException) {
      :
    }
    if (e instanceof IOException) {
      :
    }
      :
}

等という記述をしてしまったらおまぬけ以外の何者でもありません。

} catch (FileNotFoundException e) {
  :
} catch (IOException e) {
  :
} catch (Exception e) {
  :
} finally {
  :
}

ですね。catchの順番は、より具体的なクラスを先に記述します。また、try 〜 catchの範囲を修正することを忘れる可能性も高くなります。例外の処理は厳密に行われるべきです。よく、

public void foo() {
    try {
      :
    } catch (Exception e) {
        e.printStackTrace();
    }
}

のように書かれる場合があります。throwsに記述するわけにはいかないプログラムのプロトタイプではよく行うと思います。本来は、例外を派生させるメソッド呼び出しを含み、そのメソッドでの例外によってスキップされる範囲を絞り込んでtry 〜 catchで囲むべきです。
上記のように書いていると、最終的にそのように変更するのが億劫になりがちです。なるべくなら、try 〜 catchの範囲はプロトタイプの時点で決めておくべきで、そうでない場合は上記のように書くよりは、throws Exceptionと記述しておくほうが後で修正しやすいと思います。

メソッド全体をtry 〜 catchで囲むケースは別の意図があります。そのメソッドでは起こり得ない例外をcatchせざるを得ない場合(*)です。それでも例外を発生させるメソッド呼び出し部だけを囲むのが常套ですが、その呼び出し箇所が多い場合は面倒ですから。
*:これは、例えば文字列を解釈する(ParseExceptionがthrows宣言されてるような)メソッドに対して安全な文字列リテラルを渡すような使い方を指します。

public void foo() {
    try {
      :
    } catch (HogeException e) {  // ←呼び出すメソッドからthrowsされてはいるが起こり得ない例外
        throw new InternalError(e.getMessage());
    }
}