バイナリ/テキスト相互変換(BASE64とQuoted-printable)
電子メールによるバイナリファイルの送受信
インタネットの爆発的な普及に伴い、電子メール(e-mail)が至極アタリマエの通信メディアとして用いられるようになってきました。
e-mailはインタネットが今ほど一般的ではなく、コンピュータ間を公衆回線(一般電話網)を介して繋いでいた頃からのメディアです。ですから現在でもe-mailに含まれる文字は7bit-ASCIIであることが求められています。
文字だけの、いわゆる"お手紙"だけでなく、画像、音声、あるいはプログラムなどの"バイナリファイル"をe-mailでやりとりするには、送り手はバイナリファイルを構成するバイト(8bit=256文字)をASCII文字に変換し、受け手はそれを元に戻さなければなりません。
e-mail界で、バイナリ/ASCII相互の変換規則として一般的に用いられているのがBASE64とQuoted-printableです。
BASE64
BASE64は、バイナリデータすなわちバイト列を3byteすなわち24bitづつに分割します。そしてその24bitをさらに6bitの4つの組に分割します。そうすればそれぞれは0から63の(64種類の)数として表すことができます。
0〜63のそれぞれに対し、以下のように文字を割り当てます:
0 : 'A' 1 : 'B' ... 25 : 'Z' 26 : 'a' 27 : 'b' ... 51 : 'z' 52 : '0' 53 : '1' ... 61 : '9' 62 : '+' 63 : '/'
こうやって割り当てられた文字をe-mailのメッセージとして送り出すわけです。1文字が64進法での1桁に相当するため、BASE64の名が付いています。
送信したいバイナリデータのバイト数が3の倍数でないときは"パディング"が行われます。
- 1byte(8bit)余るとき、それに4bitの0を付加して12bit(6bit*2)にし、2文字分変換して'='を2つ追加します。
- 2byte(16bit)余るとき、それに2bitの0を付加して18bit(6bit*3)にし、3文字分変換して'='を1つ追加します。
BASE64ではバイナリの3文字がASCIIの4文字に変換されるため、転送されるデータの大きさは33%増加します。
Quoted-printable
Quoted-printbleは送りたいバイナリデータのほとんどがASCII文字で構成されている場合に用いられます。
Quoted-printableでは送りたいバイナリデータのひとつひとつがASCII文字であればそれをそのまま、そうでなければ'='に続く2桁の16進文字('0'-'9','A'-'F')に変換します。
エンコーダ・デコーダの実装
では前述の変換規則に基づいて、BASE64およびQuoted-printableによる変換/逆変換を行なうクラスを実装しましょう。
まず、ベースクラスでインタフェースを定義します。
template <class InputIterator, class OutputIterator>
class MimeCoder {
public:
virtual OutputIterator filter(InputIterator first, InputIterator last,
OutputIterator result, bool fin =false) = 0;
virtual OutputIterator finish(OutputIterator& result) = 0;
};
メソッドfilter(first,last,redult,fin)は、範囲[first,last)にあるすべてのバイナリに対して変換を行ない、結果をresultに出力します。最後のパラメータfinがtrueであれば、バイナリデータはこれで終わりとみなし、末尾のパディングを出力します。
メソッドfinish(result)はパディングをresultに出力します。
入力/出力先はtemplateで指定する方式としました。こうしておけばファイルだけでなく、バイト配列や文字列、さらにはバイトを要素とする任意のコンテナを入出力先として利用できるからです。
つぎにBASE64,Quoted-printableそれぞれのencoder/decoderをMimeCoderから導出します。
/*
* Base64
*/
template <class InputIterator, class OutputIterator>
class Base64Encoder : public MimeCoder<InputIterator, OutputIterator> {
public:
Base64Encoder() : len(0), linepos(0) {}
virtual OutputIterator filter(InputIterator first, InputIterator last,
OutputIterator result, bool fin =false );
virtual OutputIterator finish(OutputIterator& result );
private:
int linepos;
unsigned char curr[3];
int len;
void encodeCurr(OutputIterator& result);
};
template <class InputIterator, class OutputIterator>
class Base64Decoder : public MimeCoder<InputIterator, OutputIterator> {
public:
Base64Decoder() : len(0), ended(false) {}
virtual OutputIterator filter(InputIterator first, InputIterator last,
OutputIterator result, bool fin =false );
virtual OutputIterator finish(OutputIterator& result);
private:
bool ended;
unsigned char curr[4];
int len;
int err;
void decodeCurr(OutputIterator& result);
};
/*
* Quoted-Printable
*/
template <class InputIterator, class OutputIterator>
class QpEncoder : public MimeCoder<InputIterator, OutputIterator> {
public:
QpEncoder() : linepos(0), prevCh('x') {}
virtual OutputIterator filter(InputIterator first, InputIterator last,
OutputIterator result, bool fin =false);
virtual OutputIterator finish(OutputIterator& result);
private:
int linepos;
unsigned char prevCh;
};
template <class InputIterator, class OutputIterator>
class QpDecoder : public MimeCoder<InputIterator, OutputIterator> {
public:
QpDecoder() : hexlen(0) {}
virtual OutputIterator filter(InputIterator first, InputIterator last,
OutputIterator result, bool fin =false );
virtual OutputIterator finish(OutputIterator& result);
private:
int hexlen;
unsigned char hex2[2];
};
それぞれの実装は以下のようになります。変換規則を忠実にコードに落としました。
static const int cLineLen = 72;
/*
* Base64Encoder
*/
static const char cBase64Codes[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
template <class InputIterator, class OutputIterator>
OutputIterator Base64Encoder<InputIterator, OutputIterator>::filter(InputIterator first, InputIterator last, OutputIterator result, bool fin) {
for(;;) {
for(; linepos < cLineLen; linepos += 4) {
for (; len < 3; len++) {
if ( first == last ) {
if ( fin )
finish(result);
return result;
}
curr[len] = *first;
++first;
}
encodeCurr(result);
len = 0;
}
*result++ = 13;
*result++ = 10;
linepos = 0;
} // for (;;)
}
template <class InputIterator, class OutputIterator>
OutputIterator Base64Encoder<InputIterator, OutputIterator>::finish(OutputIterator& result) {
if ( len )
encodeCurr(result);
len = 0;
linepos = 0;
return result;
}
template <class InputIterator, class OutputIterator>
void Base64Encoder<InputIterator, OutputIterator>::encodeCurr(OutputIterator& result) {
if ( len < 3 )
curr[len] = 0;
*result++ = cBase64Codes[ curr[0] >> 2 ];
*result++ = cBase64Codes[ ((curr[0] & 0x03) << 4) |
((curr[1] & 0xF0) >> 4) ];
if ( len == 1 )
*result++ = '=';
else
*result++ = cBase64Codes[ ((curr[1] & 0x0F) << 2) |
((curr[2] & 0xC0) >> 6) ];
if ( len < 3 )
*result++ = '=';
else
*result++ = cBase64Codes[ curr[2] & 0x3F ];
}
/*
* Base64Decoder
*/
#define XX 127
static const unsigned char cIndex64[256] = {
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,62, XX,XX,XX,63,
52,53,54,55, 56,57,58,59, 60,61,XX,XX, XX,XX,XX,XX,
XX, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
15,16,17,18, 19,20,21,22, 23,24,25,XX, XX,XX,XX,XX,
XX,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
41,42,43,44, 45,46,47,48, 49,50,51,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
};
template <class InputIterator, class OutputIterator>
OutputIterator Base64Decoder<InputIterator, OutputIterator>::filter(InputIterator first, InputIterator last, OutputIterator result, bool fin) {
unsigned char c;
err = 0;
for (;;) {
while ( len < 4) {
if ( first == last ) {
if ( fin )
finish(result);
return result;
}
c = *first;
if ((cIndex64[c] != XX) || (c == '='))
curr[len++] = c;
else
if ((c != 13) && (c != 10))
++err; // error
++first;
}
decodeCurr(result);
len = 0;
}
}
template <class InputIterator, class OutputIterator>
OutputIterator Base64Decoder<InputIterator, OutputIterator>::finish(OutputIterator& result) {
len = 0;
if ( ended )
return result;
else {
ended = false;
return result;
}
}
template <class InputIterator, class OutputIterator>
void Base64Decoder<InputIterator, OutputIterator>::decodeCurr(OutputIterator& result) {
if ( ended ) {
++err;
ended = false;
}
for (int i = 0; i < 2; i++)
if ( curr[i] == '=') {
++err;
return;
} else
curr[i] = cIndex64[curr[i]];
*result++ = (curr[0] << 2) | ((curr[1] & 0x30) >> 4);
if ( curr[2] == '=') {
if ( curr[3] == '=')
ended = true;
else
++err;
} else {
curr[2] = cIndex64[curr[2]];
*result++ = ((curr[1] & 0x0F) << 4) | ((curr[2] & 0x3C) >> 2);
if ( curr[3] == '=' )
ended = true;
else
*result++ = ((curr[2] & 0x03) << 6) | cIndex64[curr[3]];
}
}
/*
* QpEncoder
*/
static const char cBasisHex[] = "0123456789ABCDEF";
template <class InputIterator, class OutputIterator>
OutputIterator QpEncoder<InputIterator, OutputIterator>::filter(InputIterator first, InputIterator last, OutputIterator result, bool fin) {
unsigned char c;
for (; first != last; ++first ) {
c = *first;
if (c == '¥n') {
if ( prevCh == ' ' || prevCh == '¥t') {
*result++ = '='; // soft & hard lines
*result++ = c;
}
*result++ = c;
linepos = 0;
prevCh = c;
} else
if ( (c < 32 && c != '¥t') || (c == '=') || (c >= 127)
|| ( linepos == 0 && c == '.') ) {
*result++ = '=';
*result++ = cBasisHex[c >> 4];
*result++ = cBasisHex[c & 0xF];
linepos += 3;
prevCh = 'A';
} else { // printable characters
*result++ = prevCh = c;
++linepos;
}
if ( linepos > cLineLen) {
*result++ = '=';
*result++ = prevCh = '¥n';
linepos = 0;
}
}
if ( fin )
finish(result);
return result;
}
template <class InputIterator, class OutputIterator>
OutputIterator QpEncoder<InputIterator, OutputIterator>::finish(OutputIterator& result) {
if ( linepos ) {
*result++ = '=';
*result++ = '¥n';
}
linepos = 0;
prevCh = 'x';
return result;
}
/*
* QpDecoder
*/
static const unsigned char cIndexHex[256] = {
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,XX,XX, XX,XX,XX,XX,
XX,10,11,12, 13,14,15,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,10,11,12, 13,14,15,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
};
template <class InputIterator, class OutputIterator>
OutputIterator QpDecoder<InputIterator, OutputIterator>::filter(InputIterator first, InputIterator last, OutputIterator result, bool fin) {
unsigned char c, c1, c2;
int errn = 0;
for (; first != last; ++first) {
if ( hexlen ) {
if (*first == '¥n')
hexlen = 0;
else {
hex2[hexlen-1] = *first;
if ( hexlen++ == 2) {
if (XX == (c1 = cIndexHex[hex2[0]]))
++errn;
if (XX == (c2 = cIndexHex[hex2[1]]))
++errn;
c = (c1 << 4) | c2;
if (c != '¥r')
*result++ = c;
hexlen = 0;
}
}
} else
if (*first == '=')
hexlen = 1;
else
*result++ = *first;
}
if ( fin )
finish(result);
return result;
}
template <class InputIterator, class OutputIterator>
OutputIterator QpDecoder<InputIterator, OutputIterator>::finish(OutputIterator& result) {
if ( hexlen ) { // error
hexlen = 0;
return result;
}
return result;
}
#undef XX
できあがったencoder/decoderの動作確認コードを用意しました。char配列を入出力先として用いています。
using namespace std;
int main() {
char c[256];
char m[256];
char* o;
MimeCoder<char*, char*> *e, *d;
cout << "Used base64 [b] or quoted-printable [q] ?¥n";
cin >> c;
if (c[0] == 'b' || c[0] == 'B') {
e = new Base64Encoder<char*, char*>;
d = new Base64Decoder<char*, char*>;
} else {
e = new QpEncoder<char*, char*>;
d = new QpDecoder<char*, char*>;
}
cout << "Enter some text to encode.¥n";
cin >> c;
cout << "¥n¥nThe encoded text:¥n";
o = e->filter(c, c + strlen(c), m, true);
*o = '¥0';
cout << m << flush;
cout << "¥n¥nDecoded:¥n";
o = d->filter(m, m + strlen(m), c, true);
*o = '¥0';
cout << c << flush;
delete e;
delete d;
return 0;
}
複数パートからなるメールの形式 (multipart形式の例)
MIME規格にしたがって複数のファイルをメールに載せるには、'multipart'を用いる。
例としてmessage.txtをJIS,binary.lzhとpicture.gifをBASE64でメールに載せるときのformatを以下に示す。ただし、message.txtはファイルとしてではなく、メールのメッセージとしてメールリーダが出力することを想定している。また、(*..)は注釈であり、メールの中には記述されない。
To: どこそこ
Subject: どーのこーの
From: だれそれ
... なんやかんや ...
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary=NeverOccurred (*1)
--NeverOccurred (*2)
Content-Type: text/plain; charset=ISO-2022-JP
επιστημηです。
先日お約束していたbinary.lzh,picture.gifを送ります。
--NeverOccurred (*2)
Content-Type: application/octet-stream; name=binary.lzh (*3)
Content-Transfer-Encoding: base64 (*4)
(binary.lzhをBASE64で変換したテキストがここにある)
--NeverOccurred--NeverOccurred (*2)
Content-Type: application/octet-stream; name=picture.gif (*3)
Content-Transfer-Encoding: base64 (*4)
(picture.gifをBASE64で変換したテキストがここにある)
--NeverOccurred-- (*5)
- ※注釈
- *1:Content-Type: multipart/mixed; boundary=NeverOccurred
- 'multipart': メッセージが複数のパートに別れている。
- 'mixed': 複数のパートそれぞれが互いに独立である。
- 'boundary': パートを区切る任意の文字列。
- *2:--NeverOccurred
- 'boundary=...'で指定した文字列の頭に'--'を付加した文字列がパートの区切りとなる。
- *3:Content-Type: application/octet-stream; name=binary.lzh
-
'application/octet-stream': バイナリ・データ。 'name': ファイル名。ただし、本パラメータは必須ではない。通常のメールリーダにおいては、このパラメータを'名前を付けてファイルの保存(Fileopen-dialog)'に渡すことになろう。
また、Content-Type:がimageやvideo,audioであったとき、テンポラリファイルを作成して、メールリーダにあらかじめ設定されたそれぞれのブラウザを起動する。設定されていなければファイルとしてsaveすることになるであろう。
- *4:Content-Transfer-Encoding: base64
- BASE64でencodeされている。Content-Transfar-Encodingが指定されていないとき、以下のボディはtext/plain; charset=us-asciiとみなす。
- *5:--NeverOccurred--
- multipartの最後の区切りはboundaryの前後に'--'を付加したものである。
メール送信のアルゴリズム
- ヘッダを送信する。
- 空行を送信する。
- ボディを送信する。
- textのとき: 指定したcharsetに従い、テキストを送信する。
- text以外のとき: 指定したEncoding(大抵BASE64)でファイルを送信する。
- multipartのとき、
- 送信したいファイルそれぞれについて:
- '--'とboundaryを送信する。
- ファイルを送信する(再帰!)。
- '--'とboundaryと'--'を送信する。
- 送信したいファイルそれぞれについて:
メール受信のアルゴリズム
- 空行を区切りとしてヘッダとボディに分割する。
- ヘッダを解析し(Content-Type:)、
- multipartのとき: ボディをさらにboundaryで区切り、受信する(再帰!)。
- multipartでないとき:
- textのとき: 指定されたcharsetに従いdecode/表示する。
- text以外のとき: 指定されたencoding(大抵BASE64)に従いdecodeし、ファイルに出力する。
Content-Typeのいろいろ
| type | subtype | parameter |
|---|---|---|
| application | octet-stream/postscript | type/padding |
| audio | basic | (none) |
| image | jpeg/gif | (none) |
| message | rfc822/partial/external-body | ... |
| multipart | mixed/alternative/digest/parallel | boundary |
| text | plain | charset |
| video | mpeg | (none) |
