Modified UTF-7のエンコード・デコード方法について、およびC#での実装例。

Modified UTF-7

Modified UTF-7は"修正 UTF-7", "IMAP-UTF7"とも呼ばれるもので、IMAPで非ASCII文字を含むメールボックス名をASCII文字のみで表記するためのエンコード方式。

UTF-7へのエンコードはUTF-16文字列中の非ASCII文字のみBase64に変換することで得られるが、Modified UTF-7へのエンコードはUTF-16文字列(ビッグエンディアン)に対して次の変換ルールを適用することで得ることが出来る。

  1. '&' は '&-' に変換する
  2. '&' を除く印字可能な文字(0x20〜0x25と0x27〜0x7e)はそのまま
  3. 上記以外の印字不可能な文字は後述するModified Base64に変換し、'&' (shift)と '-' (shift back)で括る

例えば文字列"INBOX.日本語"を変換するときは、"INBOX.&日本語-"とシフトしたあと、"日本語"の部分にだけModified Base64を適用する。

ここで、Modified Base64の変換ルールは次のとおり。

  1. 基本の変換ルールは通常のBase64と同じ
  2. ただし変換テーブルは '/' の代わりに ',' を使う
  3. 上記ルールで変換した後の文字数が4の倍数にならなくてもパディング( '=' )は入れない
  4. 印字可能な文字(0x20〜0x25と0x27〜0x7e)にModified Base64を適用してはいけない

このルールに則り文字列"日本語"をModified Base64で変換すると"ZeVnLIqe"となる。 したがって、"INBOX.日本語"をModified UTF-7へエンコードした結果は"INBOX.&ZeVnLIqe-"となる。

C#での実装

(ソース中のコメントはRFC 3501のsection 5.1.3より抜粋)

Modified UTF-7へのエンコード
public static string ToModifiedUTF7String(string str)
{
  var encoded = new StringBuilder();
  var shiftFrom = -1;

  for (var index = 0; index < str.Length; index++) {
    var c = str[index];

    if ((0x20 <= c && c <= 0x7e)) {
      if (0 <= shiftFrom) {
        // string -> modified UTF7
        encoded.Append('&');
        encoded.Append(ToModifiedUTF7(str.Substring(shiftFrom, index - shiftFrom)));
        encoded.Append('-');

        shiftFrom = -1;
      }

      // printable US-ASCII characters
      if (c == 0x26)
        // except for "&"
        encoded.Append("&-");
      else
        encoded.Append(c);
    }
    else {
      if (shiftFrom == -1)
        shiftFrom = index;
    }
  }

  if (0 <= shiftFrom) {
    // string -> modified UTF7
    encoded.Append('&');
    encoded.Append(ToModifiedUTF7(str.Substring(shiftFrom)));
    encoded.Append('-');
  }

  return encoded.ToString();
}

private static string ToModifiedUTF7(string str)
{
  return ToModifiedBase64(Encoding.BigEndianUnicode.GetBytes(str));
}

private static string ToModifiedBase64(byte[] bytes)
{
  var base64 = Convert.ToBase64String(bytes).Replace('/', ',');
  var padding = base64.IndexOf('=');

  if (0 <= padding)
    return base64.Substring(0, padding);
  else
    return base64;
}
Modified UTF-7からのデコード
public static string FromModifiedUTF7String(string str)
{
  if (!str.Contains("&"))
    return str;

  var bytes = Encoding.ASCII.GetBytes(str);
  var decoded = new StringBuilder();

  for (var index = 0; index < bytes.Length; index++) {
    // In modified UTF-7, printable US-ASCII characters, except for "&",
    // represent themselves
    // "&" is used to shift to modified BASE64
    if (bytes[index] != 0x26) { // '&'
      decoded.Append((char)bytes[index]);
      continue;
    }

    if (bytes.Length <= ++index)
      // incorrect form
      throw new FormatException("incorrect form");

    if (bytes[index] == 0x2d) { // '-'
      // The character "&" (0x26) is represented by the two-octet sequence "&-".
      decoded.Append('&');
      continue;
    }

    var nonprintable = new StringBuilder();

    for (; index < bytes.Length; index++) {
      if (bytes[index] == 0x2d) // '-'
        // "-" is used to shift back to US-ASCII
        break;

      nonprintable.Append((char)bytes[index]);
    }

    // modified UTF7 -> string
    decoded.Append(FromModifiedUTF7(nonprintable.ToString()));
  }

  return decoded.ToString();
}

private static string FromModifiedUTF7(string str)
{
  return Encoding.BigEndianUnicode.GetString(FromModifiedBase64(str));
}

private static byte[] FromModifiedBase64(string str)
{
  // "," is used instead of "/"
  str = str.Replace(',', '/');

  var padding = 4 - str.Length & 3;

  if (padding == 4)
    return Convert.FromBase64String(str);
  else if (padding == 3)
    throw new FormatException("incorrect form");
  else
    return Convert.FromBase64String(str + (new string('=', padding)));
}