MIME の基礎

IIJ技術研究所 山本和彦

今回は MIME の話をしましょう。 以下では、言語と文字コードを同一視して説明します。 (もちろん両者が違うことは重々承知していますが、 今回は同一視した方が分かりやすいと思います。)


テキスト・メールの問題点

MIME が登場した 1992 年以前の RFC822 (現 RFC 2822)で定められたメールは、 テキスト・メールと呼ばれています。 MIME はテキスト・メールの問題を解決するために考え出されました。 ここでは、その問題とは何だったのかをおさらいしておきましょう。

問題その1: テキスト・メールではヘッダが ASCII に限定されていました。 そこで、例えば日本語のような英語以外の文字列を 入れられませんでした。

問題その2: テキスト・メールでは本文も ASCII に限定されていました。 非英語圏では、これはあまりにも不便な制約でしたので、 その地域内の約束として独自の文字コードを採用しました。 例えば日本では、本文に ISO-2022-JP(旧称は JUNET コード) を使ってよいと取り決めました。 しかし、これは厳密にはテキスト・メールの規格に違反しています。 また、どんな文字コードが格納されているかを示す 明示的な情報がないので、 異なるデフォルトの文字コードを採用した地域間では、 結局英語でしかテキスト・メールを交換できませんでした。 たとえば、フランス語で書いたテキスト・メールを 日本に送っても正しくフランス語と認識できなかったわけです。

問題その3: 本文がテキストに限定されていました。 別の言い方をすると、 本文にテキスト以外のデータを入れても、 そのデータ型を示す方法がありませんでした。 そのため、メールリーダが自動処理できず、 ユーザ自身が判断してデータを取り出す必要がありました。

問題その4: 画像などのバイナリ・データを格納する場合は、 uuencode を使ってテキスト・データに変換していました。 しかし、uuencode は配送システムが誤動作を起こす危険な文字が 使われていました。 たとえば、System V 系の uuencode でバイナリ・データを変換すると、 空白文字(スペース)が現れます。 (4.3 BSD 以降の uuencode では、空白文字の代りに "`" が用いられています。) この空白文字がたまたま行末に現れたテキスト・メールを BITNET (ああ、懐かしい)に送ったとしましょう。 BITNET のファイル・システムでは、 行末には空白文字が存在できないようになっています。 結果として行末の空白文字が削られて、 受信者はデータを復元できなくなります。 また、カンマなどのようにヘッダで特殊な意味を持つ文字も使われていましたので、 ヘッダの符号化にも適していませんでした。

問題その5: たくさんの人がテキスト・メールに対するさまざまな拡張を提案していました。 たとえば、 テキスト・メールを本文に格納して転送する手段(RFC934)、 Content-Type: というフィールドによって本文のデータ型を指定する方法(RFC1049)、 PEM による本文のプライバシ強化(RFC989)など 挙げればきりがありません。 これらはバラバラの提案だったため統一性がありませんでした。


MIME の本質

上記のような問題を解決するために、 MIME が提案されました。 ヘッダに MIME-Version: 1.0 というフィールドを持つメールが MIME メールです。 正しい MIME メールリーダなら、 受け取ったメールに MIME-Version: 1.0 があれば MIME メールとして、 なければテキスト・メールとして(その地域独自のルールに従って)取り扱います。

MIME の本質は、以下の 4 点に集約されます。

データ型を示すフィールドの導入
既存の Content-Type: というフィールドを再定義して、 さまざまなデータ型を指定できるようになりました。 テキスト・データには文字コードを明示的に指定できるようになりました。
本文の再帰的な構造化
メールやマルチパートというデータ型を導入することで、 複数のデータを格納したり、入れ子構造をとれるようにしました。
安全な符号方式の定義
配送システムが誤動作を起こさないような安全な 符号方式を定義しました。
ヘッダの国際化
上記の符号方式を使って、ASCII 以外の文字コードを ASCII へ符号化し、ヘッダに挿入できるようになりました。

以下ではそれぞれについて説明します。


データ型を示すフィールドの導入

MIME では、パートという構造が基本になります。 パートは、コンテント・ヘッダとコンテント・ボディを 空行で区切った形をしています。 コンテント・ヘッダとコンテント・ボディの関係は、 メールのヘッダと本文の関係と同じように考えてもらって構いません。

分かりやすいように例を挙げてみましょう。

Content-Type: Text/Plain; charset=ISO-2022-JP ← コンテント・ヘッダ

これは日本語のテキストです。 ← コンテント・ボディ

コンテント・ヘッダは、コンテント・ボディに 「何がどのように処理されて格納されているか」という情報を示すための領域です。 コンテント・ヘッダにおいて、 コンテント・ボディのデータ型を示すフィールドが Content-Type: になります。

Content-Type: には主型と副型をスラッシュで区切って記述します。 大文字小文字は区別しません。 たとえば、通常のテキストなら Text/Plain、GIF イメージなら Image/Gif になります。 以下に MIME の 7 つの主型を列挙します。

Text
テキスト・データです。通常のテキストなら Text/Plain、 HTML なら Text/Html を使います。
Image
画像データです。GIF、JPEG、TIFF、PNG などは、 それぞれの名前がそのまま副型になります。
Audio
音声データです。8ビットの PCM には Audio/Basic を使います。
Video
ビデオ・データです。副型には MPEG があります。
Application
さまざまなアプリケーション独自のデータを示すときに使います。 たとえば、Application/PostScript は PostScript ファイルになります。 また、任意のバイナリ・ファイルという意味を示すために、 Application/Octet-Stream が用意されています。
Message
メールなどのメッセージを示します。 テキスト・メールと MIME メールの両方に対し Message/Rfc822 を使います。
Multipart
複数のパートを格納するために使います。 とりあえず基本的なマルチパートである Multipart/Mixed を知っておけば 十分でしょう。

Content-Type: には付加的な情報を示すためのパラメータを指示できます。 パラメータは「名前=値」の形式をセミコロンで区切って記述します。

Text には重要なパラメータとして、文字コードを指示するための charset パラメータがあります。 上記の例では、charset パラメータで ISO-2022-JP(日本語) であると 指定されています。 英語なら US-ASCII、ヨーロッパの主要言語なら ISO-8859-1 と記述します。 なお、charset パラメータが省略された場合は、 US-ASCII として取り扱われます。

MIME メールリーダは、知らない Content-Type: が来たら、 それぞれの主型に対して定義してあるデフォルトの型として扱います。 たとえば、Text/Enriched を知らないなら Text/Plain、 Multipart/Parallel を知らないなら Multipart/Mixed、 Application/X-MagicPoint を知らないなら Application/Octet-Stream として扱います。 これが、型が主型と副型に分けてある理由です。

Content-Type: が存在しない場合は、 Text/Plain; charset=US-ASCII として扱います。

メールのヘッダは特殊なコンテント・ヘッダ、 本文は特殊なコンテント・ボディと考えてよいでしょう。 日本人がもっともよく使う本文が日本語テキストである MIME メールは以下のような書式になります。

From: kazu@iijlab.net
To: itojun@iijlab.net
Subject: This is a MIME message
MIME-Version: 1.0
Content-Type: text/plain; charset=ISO-2022-JP

MIME メールだよーん。

--かず

優れた MIME メールリーダなら、 Content-Type: の値に応じてコンテント・ボディを適切に処理して 表示します。


本文の再帰的な構造化

「MIME とは再帰構造と見付けたり」--- 1995年 山本和彦

MIME では、前述の Multipart と Message というデータ型を使って 本文を再帰的に構造化できます。

ファイル・システムに例えるなら、 Multipart はディレクトリ、 その他のデータ型はファイルになります。 ディレクトリの中にディレクトリが作成できるように、 Multipart の中にも Multipart を作ってどんどん階層を深くして いくことができます。

Multipart で重要なパラメータは、boundary パラメータです。 このパラメータによって、 格納する複数のパートを区切ります。 例を挙げてみましょう。

Content-Type: Multipart/Mixed; boundary="simple"
Content-Transfer-Encoding: 7bit

--simple ← 境界
Content-Type: Text/Plain; charset=US-ASCII
Content-Transfer-Encoding: 7bit

Hello, world!

--simple ← 境界
Content-Type: Text/Plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit

はろ、わあるど。

--simple-- ← 最後の境界

このように、パラメータに指定された文字列の前に "--" を付けて パートを区切り、 最後の区切りには後にも "--" を連結します。 Multipart 自身もコンテント・ヘッダとコンテント・ボディ、 各パートもそれぞれコンテント・ヘッダとコンテント・ボディ から構成されていることがお分かりになると思います。

再帰的な構造を作るもう1つのデータ型が、Message/Rfc822 であり、 テキスト・メールや MIME メールを格納するために使います。 もし読者のみなさんが、 「本文を構造化したことなんてない」と思っていても、 メールを転送するだけで本文を入れ子を作成していることになります。 例を挙げてみましょう。

From: itojun@iijlab.net
To: utashiro@iijlab.net
Subject: Fw: This is a MIME message
MIME-Version: 1.0
Content-Type: Multipart/Mixed; boundary="NextPart"
Content-Transfer-Encoding: 7bit

--NextPart ← 境界
Content-Type: Text/Plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit

メールを転送します。

いとぢゅん

--NextPart ← 境界
Content-Type: Message/Rfc822
Content-Transfer-Encoding: 7bit

From: kazu@iijlab.net
To: itojun@iijlab.net
Subject: This is a MIME message
MIME-Version: 1.0
Content-Type: text/plain; charset=ISO-2022-JP

MIME メールだよーん。

--かず

--NextPart-- ← 最後の境界

Multipart と同様、 Message/Rfc822 を使っても階層をどんどん深くしていくことができます。

優れた MIME メールリーダなら、 本文が深く階層化されていても適切に解析して、 その構造を表示します。


安全な符号方式の定義

符号方式は Content-Transfer-Encoding: で指定します。 値としては、7bit、8bit、binary、base64、quoted-printable があります。 7bit、8bit、binary は無変換であり、 コンテント・ボディが 7ビットの行の集合なら 7bit、 行の集合で1 バイトでも 8 ビット文字が含まれているなら 8bit、 その他が binary になります。 Base64 と quoted-printable は実際に変換を施す符号方式で、 変換後は 7 ビットのデータになります。

Base64 は、uuencode のように 8ビット3文字(24ビット)を 6ビット4文字(24ビット)に変換する符号方式です。 しかし、uuencode とは違い 6 ビットである 64 文字は メールにとって安全であるものが選ばれています。 その 64 文字は、"A-Za-z0-9+/" です。 Base64 は、バイナリ・ファイルの符号化などに適しています。 実はこの符号方式は PEM で定義された printable-encoding を MIME が名前を変えて採用したのです。 Base64 の例を以下に示します。

Content-Type: Image/Jepg
Content-Transfer-Encoding: base64

LS0tIGRyYWZ0LW5ndHJhbnMtdHJhbnNsYXRvci5vcmlnCVN1biBOb3YgMTUgMTY6MzI6NDYg
MTk5OAorKysgaWQtdHJhbnMJU3VuIE5vdiAxNSAxNToxNDo1NCAxOTk4CkBAIC0xLDggKzEs
MTIgQEAKICNyZXYgMDAuMDIKIEludGVybmV0LURyYWZ0ICAgICAgICAgICAgICAgICAgICAg

Quoted-printable では、 8ビット文字やその他メールにとって危険な文字 1 バイトを "=" に続けて 16 進数表記にします。 当然キーワードである "=" は "=3D" のように変換する必要があります。 それから、行末に "=" が現れた場合は長い行を折り返したことになりますので、 復号化の際には "=" を取って 2 つの行を連結します。 Quoted-printable はヨーロッパ言語のテキストや PostScript データの符号化に適しています。 以下に例を示します。

Content-Type: Text/Plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable

I love M=D6tley Cl=DCe(Mo*tley Clu*e).
This is a very, very, very, very, very, very =
long line, so folded.

それぞれのデータ型にはそれに適した符号方式があります。 たとえば、Text はなるべく符号化しない方がいいので 7bit や 8bit が 選ばれることが多いです。 Application/PostScript はほとんどテキストであり、 多くの場合長い行を折り返すためだけに符号化しますから quoted-printable が適しています。 その他の多くのデータ型には、base64 が適しています。 なお、構造化するためのデータ型である Multipart や Message は 全体が無変換でないといけないので、7bit か 8bit か binary になります。 格納しているデータを符号化する必要があるなら、 Multipart や Message 全体ではなく、 それぞれのパートで符号化を施します。

Content-Transfer-Encoding: が省略された場合は、7bit として取り扱います。


ヘッダの国際化

MIME になってもヘッダには ASCII しか格納できないことには変わりません。 そこで、ヘッダに ASCII 以外の文字コードを挿入するには、 まず ASCII 文字列に符号化する必要があります。 さらにその符号方式と文字コードを指示できるべきです。 このような考えの基に、 以下のような書式が提案されました。

=?文字コード?符号方式?符号化された文字列?=

文字コードには charset パラメータと同じ値を用います。 符号方式には "B" と "Q" があり、 "B" は base64 と同じで、 "Q" は quoted-printable の亜種になります。

以下に「山本和彦」を符号化した場合の例を示します。

=?iso-2022-jp?B?GyRCOzNLXE9CSScbKEI=?=

このヘッダの符号化の優れた点について触れておきましょう。 当然、ASCII 以外の文字コードが ASCII 文字列に変換されていますので、 配送システムが誤動作を起こすことはありません。 さらに、このヘッダの符号化は、テキスト・メールであっても (つまり MIME-Version: 1.0 フィールドがなくても) 利用できることになっています。

優れた MIME メールリーダなら、 ヘッダに ASCII 以外の文字列が書かれた場合は、 送信時に自動的に符号化します。 また MIME メールリーダが受け取った場合は自動的に復号化して表示し、 返答時には引用後自動的に符号化します。

この規則を知らない テキスト・メールのメールリーダが受け取った場合は、 符号化されたまま表示します。当然ユーザには意味不明ですが、 テキスト・メールのメールリーダが誤動作を起こすことはないでしょう。 返答の際にはこの文字列をそのまま引用します。 つまり、符号化された文字列が保存されるので、 MIME メールリーダで復号化することが可能なのです。

このようにヘッダの符号化は、 テキスト・メールリーダと MIME メールリーダで 問題なくやりとりができるように設計されています。

ヘッダの符号化の規則は奥が深くとても難しいのですが、 これ以上は詳しく説明しません。


MIME で重要なフィールド

Content- で始まる文字列は MIME で使うフィールド名として予約されています。 覚えておきたいのは、以下の 4 つの MIME フィールドです。

Content-Type:
前述のようにデータ型を指定します。
Content-Transfer-Encoding:
前述のように符号方式を指定します。
Content-Description:
そのパートの説明を記述します。パートに対する Subject: だと考えてもらって結構です。
Content-Disposition:
そのパートをファイルに保存する際のファイル名を指定します。

以下に本文がマルチパートであり、 第1パートが日本語テキスト、 第2パートが GIF である例を示します。 「←」の右は説明であり、実際には存在しません。

Message-Id: <19981013232843D.kazu@iijlab.net>
Subject: Washington
From: Kazu Yamamoto (=?iso-2022-jp?B?GyRCOzNLXE9CSScbKEI=?=) ← 山本和彦が符号化されている
 <kazu@iijlab.net>
To: itojun@itojun.org
Date: Tue, 13 Oct 1998 23:28:43 +0900
Mime-Version: 1.0 ← MIME メール
Content-Type: Multipart/Mixed; boundary="--Next_Part--" ← 本文はマルチパート
Content-Transfer-Encoding: 7bit ← 本文全体は無変換

----Next_Part-- ← 境界
Content-Type: Text/Plain; charset=iso-2022-jp ← データは日本語テキスト
Content-Transfer-Encoding: 7bit ← 無変換

この前取った写真を送ります。

--かず

----Next_Part-- ← 境界
Content-Type: Image/Gif ← データ型は GIF
Content-Transfer-Encoding: base64 ← 符号方式は base64
Content-Description: Picture in Washington ← このパートの説明
Content-Disposition: attachment; filename="wash.gif" ← このパートのファイル名

LS0tIGRyYWZ0LXlhbWFtb3RvLWNoYXJzZXQtaXNvLTIwMjItanAtMDAudHh0Lm9yaWcJRnJp
IE5vdiAgNyAwNTo1NDoxMyAxOTk3CisrKyBkcmFmdC15YW1hbW90by1jaGFyc2V0LWlzby0y
MDIyLWpwLTAwLnR4dAlGcmkgTm92ICA3IDEwOjAwOjIwIDE5OTcKQEAgLTU2LDE0ICs1Niwx

----Next_Part---- ← 最後の境界

おわりに

今回は MIME の基礎を説明しました。 このような書式は優れたメールリーダなら MIME メールリーダ自身が解析、生成しますので ユーザは気にする必要はありません。

MIME で重要な点は、 すべてがテキスト・ベースなので、 テキスト・メールのメールリーダでも (ゴミが表示されますが)まがりなりにも表示可能ということです。 また、ルールのなかった世界に 共通の汎用的なルールを定義したことも重要です。 これからメールを拡張する時は、 MIME という枠組の中で考える必要があります。 書式よりも、 このような MIME の心を理解していただければ幸いです。