[C#] QuotedPrintable エンコード・デコードの実装方法

[C#] QuotedPrintable エンコード・デコードの実装方法

Quoted-Printableとは

Quoted-Printable とは、Ascii文字しか扱えない電子メールの送信などでよく使われる符号化方式の一種です。Ascii文字以外を印字可能な文字に変換(エンコード)します。

同じように電子メールのエンコードに用いられるBase64と比べ、Ascii文字はそのままなのである程度読める形でデータがエンコードされます。一方で、バイナリ形式やAscii文字以外が多く含まれるデータをQPでエンコードする場合は、データサイズが大きくなって非効率になります。

以下、Wikipediaからの引用です。

Quoted-printable(QP encodingとも呼ばれる)は、印字可能な文字(例えば、英数字や等号「=」)を使用した符号化方式であり、8ビットデータを7ビットデータパスで転送するためのものである。インターネット電子メールで使用できるようにするため、Content-Transfer-Encoding として定義されている。

Quoted-Printableの仕様

RFC2045というので定義されているみたいです。詳細は上記URLページ19くらいを参照してください。以下要約です。

  1. 原則、オクテットを「=00」のように、イコール(=)に続く16進数文字列(大文字)の形で符号化する。
  2. 33(0x21, '!')から60(0x3C, '<')まで、および62(0x3E, '>')から126(0x7E, '~')は符号化せずそのままにする。
    61(0x3D, '=') は符号化する必要あり。
  3. 9(0x09, タブ)と32(0x20, スペース)は符号化しなくてよい。ただし改行直前に来る場合は符号化する。
  4. 改行は CRLF を使う。テキストの場合、CR(0x0D)とLF(0x0A)は符号化しない。それ以外では符号化することもある?
  5. エンコードされたテキストは1行あたり76文字以下でないといけない。76文字を超える行はソフト改行(=CRLF)を入れる。
    行末の=はその改行が無意味なことを示す。
    =のあとにスペースやタブが続いてもよい。それらは無視する。

おそらくこんな感じだと思います。とにかく読めるAscii文字はそのままでそれ以外は符号化する、という感じです。

C# で実装する

では上記仕様に基づいて、C#を使ってエンコード・デコードの処理を実装してみます。実装したものは上のGithubにあります。

エンコード処理

public string Encode(string text, Encoding encoding)
{
    var sb = new StringBuilder();

    // 対象文字列のバイトデータ取得
    var bytes = encoding.GetBytes(text);

    // 1行当たりの文字数カウンタ
    var characterCount = 0;

    for (int i = 0; i < bytes.Length; i++)
    {
        var octet = bytes[i];
        // =00 の形に変換するかどうか
        var isConverting = false;

        switch (octet)
        {
            case 0x09: // タブ
            case 0x20: // スペース
                // 次に改行(CRLF)が続く場合は変換
                // それ以外はそのまま出力
                isConverting = i < bytes.Length - 2
                    && bytes[i + 1] == 0x0D
                    && bytes[i + 2] == 0x0A;
                break;
            case 0x0A: // LF
            case 0x0D: // CR
                // 変換せずそのまま出力
                isConverting = false;
                break;
            default:
                // 次の範囲の文字は変換して出力
                // 31   0x1f  US(ユニット区切り)以前の制御文字
                // 127    0x7f  DEL(削除) 以降の文字
                // 61    0x3d  = 
                isConverting = octet <= 0x20 || 0x7f <= octet || octet == 0x3d;
                break;
        }

        // 出力文字列に追加
        if (isConverting)
        {
            sb.Append($"={octet.ToString("X2")}");
            characterCount += 3;
        }
        else
        {
            sb.Append((char)octet);
            characterCount += 1;
        }

        // エンコードされた行の長さが76文字以下になるようにする
        // 次の出力で "=00" と3文字出力されて77文字以上になるときはソフト改行
        if (characterCount > 73)
        {
            sb.Append("=\r\n"); // =CRLF
        }

        // 改行されていれば文字数カウンタリセット
        if (sb.ToString().EndsWith("\r\n"))
        {
            characterCount = 0; // カウンタリセット
        }
    }

    return sb.ToString();
}

対象文字列の文字コード

Encode メソッドは引数でエンコードする対象の文字列を受け取ります。その際、その文字列をどの文字コードに対応させて扱うかEncodingも合わせて受け取ります。

指定されたEncodingで文字列をバイト型配列に変換し、QPエンコードしていきます。

符号化(=00)する

取得したバイト配列から1個(octet)ずつ取り出して符号化します。"=FF"の形で符号化します。16進数文字列は必ず大文字を用います。

幾つかの例外では符号化しないパターンがあります。

タブとスペースは基本的にそのまま符号化せずに出力します。ただし改行直前のタブとスペースに限り、符号化しないといけません。

CR,LFについては符号化しません。

0x21("!")~07E("~")の範囲のAscii文字はすべて印字可能なので符号化しません。ただし、0x61("=")は符号化します。

ソフト改行

ソフト改行とは無意味な改行のことです。文末の"="は無意味な改行を意味します。

符号化された文字列は76文字以下でなければなりません。それを超える場合にはソフト改行を挟み、76文字以下に抑えます。

実装では出力文字を追加するたびに文字数をカウントしておき、76文字を超えそうな場合には、ソフト改行("=CRLF")を追加します。改行のたびにカウンタはリセットしておきます。

76文字以下なら別に1文字ごとにソフト改行があっても仕様上は問題ないはずです。

デコード処理

public string Decode(string text, Encoding encoding)
{
    var bytes = new List<Byte>();

    // ソフト改行をすべて削除
    var reg = new Regex("=[ \t]*\r\n");
    text = reg.Replace(text, string.Empty);

    // 次に読む文字位置
    var index = 0;

    while (index < text.Length)
    {
        var c = text[index];

        if (c == '=')
        {
            // = が出たら次の2文字を合わせて読む
            // 範囲外参照になる場合は不正なQP
            var octetStr = text.Substring(index + 1, 2);
            // 16進数文字列としてByte型に変換
            var octet = Convert.ToByte(octetStr, 16);
            bytes.Add(octet);
            // 詠み込んだ2文字すすめる
            index += 2;
        }
        else
        {
            // = で始まらない文字はそのまま出力
            // Ascii文字なのでByte型にキャスト
            bytes.Add((byte)c);
        }

        index++;
    }

    var result = encoding.GetString(bytes.ToArray());
    return result;
}

ソフト改行の処理

上記コードでは正規表現を使ってソフト改行を削除しています。ソフト改行はエンコード時に追加された無意味な改行です。

"="の後にスペースやタブがあっても無視してもよいのでこれも合わせて削除してます。

デコード処理

デコードは単純です。引数で渡された文字列を順番に処理していきます。

"="が出現したらその次の2文字を取り出して16進数文字列としてバイト型にキャストします。この時ソフト改行は削除済なので、範囲外参照や無効な16進数にはならないはずです。そうなる場合は不正なQPエンコード文字列のはずです。

"="に続く文字以外はすべて印字可能な文字(Ascii文字)なのでバイト型にしてしまいます。

最後にデコードされたバイト型配列を、指定の文字コードの文字列として変換すると元の文字列が得られます。

以上。

参考URL

C#カテゴリの最新記事