ククログ

株式会社クリアコード > ククログ > 2026 > Thunderbird 153以降で作成した日本語のタグが保存されない問題の原因と回避方法

Thunderbird 153以降で作成した日本語のタグが保存されない問題の原因と回避方法

先日、当社でご提供しているThunderbirdの法人向け技術サポートサービスのお客さまより、「Thunderbird 153以降のバージョンでタグが保存されなくなった(タグが失われるようになった)」というトラブルのお問い合わせを頂きました。

そこで調査を行った結果、Bug 650623の修正で行われた変更が原因で、そのような現象が起こる可能性があることが分かりました。 当該タグが日本語などの非ASCII文字で6文字以上の長さで、メールサーバーがGmail以外のIMAPサーバーである場合、本件と同一の事象と考えられます。

本記事では、この現象が発生する背景を説明し、暫定的な回避方法をご紹介します。

原因

冒頭で紹介したBug 650623は、元々は2011年に報告された問題で、非ASCII文字のタグがIMAPサーバー上で破損するというものです。 これは、Thunderbird 152およびそれ以前のバージョンにおいて、非ASCII文字を含むタグ名を「内部的に、UTF-7でエンコードし、さらにアルファベットの大文字を小文字に統一した後の文字列で識別する」仕様であったことに起因して発生していた問題です。

この問題の修正として、Thunderbird 153およびそれ以降のバージョンでは、非ASCII文字のタグは「内部的に、Quoted-Printable形式に変換した文字列で識別する」仕様に変更されました。 この結果、例えば 日本語 というタグ名の場合、Thunderbird 152以前で作成したときの内部名は &zevnliqe- となっていましたが、Thunderbird 153以降で作成したときの内部名は =e6=97=a5=e6=9c=ac=e8=aa=9e となるようになりました。

ここで注目して欲しいのが、Thunderbird 153以降での変換後の内部名は元の文字列より大幅に長くなっているという点です。 Quoted-Printableでは非ASCII文字の1バイトの文字は3文字になり、日本語の文字は基本的に3バイトで表現されることから、文字数で言うと9倍に膨れあがることになります。 ここに、IMAPでメールサーバー上のメールに設定するタグを「表示名」ではなく「内部名」の方で管理するというThunderbirdの仕様と、IMAPサーバーが受け付けるタグの最大文字数の制約が組み合わさることによって、問題が生じます。 例えばオープンソースのIMAPサーバーであるDovecotの場合、タグの既定の最大文字数は50のため、日本語で6文字以上のタグを作成すると内部名はQuoted-Printable形式では54文字になり、もう制限に抵触してしまいます。 その結果、IMAPサーバー上にあるメールにタグが保存されず、IMAPサーバー側の状態をThunderbirdがローカルに同期するとタグが未設定の状態に戻る、という現象が起こります。

このことはThunderbirdのIMAPのログを見ると確かめられます。 以下は、環境変数 MOZ_LOG を使った低レイヤーのログ採取手順で採取したIMAP:5のログの抜粋(匿名化済み)ですが、サーバー(Dovecot)側からのレスポンスとして Keyword length too long というメッセージが返されているのが分かります。

[Parent 10868: IMAP]: I/IMAP 1f8d2726600:example.com:S-INBOX:ProcessCurrentURL:imap://user@example.com:993/customKeywords%3EUID%3E.INBOX%3E1824:1827%3E=e3=81=82=e3=81=82=e3=81=82=e3=81=82=e3=81=82=e3=81=82%3E:  = currentUrl
[Parent 10868: IMAP]: D/IMAP ProcessSelectedStateURL [this=1f8d2726600], m_imapAction = 0x10000037
[Parent 10868: IMAP]: I/IMAP 1f8d2726600:example.com:S-INBOX:SendData: 36 uid store 1824:1827 +FLAGS (=e3=81=82=e3=81=82=e3=81=82=e3=81=82=e3=81=82=e3=81=82)
[Parent 10868: IMAP]: V/IMAP ReadNextLine [rv=0x0 stream=1f8d126de50 nb=62 needmore=0]
[Parent 10868: IMAP]: I/IMAP 1f8d2726600:example.com:S-INBOX:CreateNewLineFromSocket: 36 NO [CANNOT] Keyword length too long (0.001 + 0.000 secs).

このような発生機序のため、この問題はあくまで「Thunderbird 153以降で日本語の文字を含むタグを新規に作成したとき」に発生します。 過去のバージョンのThunderbirdで作成した古いタグの場合、作成時点で生成された短い内部名が引き続き使われ続けるため、この問題は起こりません。

また、IMAPサーバー側の仕様や設定によっても、実際に現象が発生するかどうかは変わってきます。 当社で検証した際には、GmailへのIMAPアクセスに関しては、メールに設定できるタグの最大文字数に制限を設けていないか充分に長い文字数を上限にしているようで、現象が再現しませんでした。

回避方法

根本的解決:メールサーバーの最大キーワード長の制限を緩和する

このトラブルの最もストレートな解決方法は、メールサーバー側で受け付けるタグ(IMAPの仕様上の呼び方は「キーワード」)の最大長の制限を緩和することです。 例に名前を挙げたDovecotの場合、mail_max_keyword_lengthという設定で受け付けるタグの最大の長さを制御できますので、使用したい日本語のタグの最大文字数×9を超える大きさに設定すれば、「Keyword length too long」エラーが発生しなくなり、タグを有効に保存できることになります。

ただ、すべてのIMAPサーバーソフトウェアがそのような設定項目を持っているとは限りませんし、他社が提供するメールサーバーを利用するだけの立場であるなど、そもそもメールサーバーの設定を変える権限が無い場合もあります。

暫定回避策:タグの内部名を短い物に振り直す

サーバー側での対処が不可能な場合、Thunderbird側で対処するしかありません。

すでに述べたとおり、今回の問題の原因は「新しくタグを作る際の内部名を決めるロジック」にあるため、過去のバージョンのThunderbirdで作成した古いタグではこの問題は起こりません。 実は新しく作成したタグについても、内部名を短く改めれば問題は起こらなくなります。

以下は、Thunderbirdのメインウィンドウでキーボードショートカット「Ctrl-Shift-J」で開けるエラーコンソールで実行可能な形にまとめた、タグの内部名を一括変換するスクリプトです。

(async () => {
  const { MailServices } =
    ChromeUtils.importESModule("resource:///modules/MailServices.sys.mjs");
  const tags = MailServices.tags;

  async function md5(s) {
    const hash = Cc["@mozilla.org/security/hash;1"]
      .createInstance(Ci.nsICryptoHash);
    hash.init(Ci.nsICryptoHash.MD5);
    const bytes = new TextEncoder().encode(s);
    hash.update(bytes, bytes.length);
    const binary = hash.finish(false);
    return Array.from(binary, c =>
      c.charCodeAt(0).toString(16).padStart(2, "0")
    ).join("");
  }

  function updateFilters(oldKey, newKey) {
    for (const account of MailServices.accounts.accounts) {
      const server = account.incomingServer;
      if (!server) continue;
      let list;
      try {
        list = server.getFilterList(null);
      } catch (_) {
        continue;
      }
      if (!list) continue;
      for (let i = 0; i < list.filterCount; i++) {
        const filter = list.getFilterAt(i);
        let changed = false;
        for (const action of filter.sortedActionList) {
          if (action.type == Ci.nsMsgFilterAction.AddTag &&
              action.strValue == oldKey) {
            action.strValue = newKey;
            changed = true;
            console.log(
              `filter "${filter.filterName}" action: ` +
              `${oldKey} -> ${newKey}`
            );
          }
        }
        for (const term of filter.searchTerms) {
          if (term.attrib == Ci.nsMsgSearchAttrib.Keywords &&
              term.value.str == oldKey) {
            const value = term.value;
            value.str = newKey;
            term.value = value;
            changed = true;
            console.log(
              `filter "${filter.filterName}" condition: ` +
              `${oldKey} -> ${newKey}`
            );
          }
        }
        if (changed)
          list.saveToDefaultFile();
      }
    }
  }
  for (const t of tags.getAllTags()) {
    if (t.key.length <= 32 ||
        !/=[0-9a-f]{2}/.test(t.key))
      continue;
    const newKey = await md5(t.tag);
    console.log(`${t.tag}: ${t.key} -> ${newKey}`);
    tags.addTagForKey(newKey, t.tag, t.color, t.ordinal);
    updateFilters(t.key, newKey);
    for (const account of MailServices.accounts.accounts) {
      const root = account.incomingServer?.rootFolder;
      if (!root) continue;
      for (const folder of root.descendants) {
        try {
          const msgs = [];
          for (const e of folder.msgDatabase.enumerateMessages()) {
            const h = e.QueryInterface(Ci.nsIMsgDBHdr);
            if (h.getStringProperty("keywords")
                .split(/\s+/).includes(t.key))
              msgs.push(h);
          }
          if (msgs.length) {
            folder.addKeywordsToMessages(msgs, newKey);
            folder.removeKeywordsFromMessages(msgs, t.key);
          }
        } catch (_) {}
      }
    }
    tags.deleteKey(t.key);
  }
  console.log('Done.');
})();

これをコピーしてエラーコンソール下部の入力欄に貼り付けて実行することで、以下のことが行われます。

  1. タグの長すぎる内部名(具体的には、Quoted-Printableを含んでいて32文字より長い場合)を安全な短い名前(MD5ハッシュ文字列)に変換する。
  2. メッセージフィルター内でタグを参照している箇所について、振り直した短い名前でタグを参照するよう再設定する。
  3. 長すぎる内部名でタグが設定済みのローカルのメールについて、振り直した短い名前でタグを再設定する。
  4. 処理完了を示すメッセージ「Done.」をコンソールに出力する。

複数の端末で同じIMAPアカウントを使用している場合は、全ての端末でこのスクリプトを実行する必要があります。 (未実行の環境では、メールに振り直されたタグを認識できなくなります。)

例えばDovecotの場合は初期状態で50文字までの内部名を受け付ける事が分かっていますが、ここではより安全側に倒して、内部名の最大文字数を32文字としています。 厳密には変換が必要無い長さのタグも変換対象となりますので、あしからずご了承ください。

保存済みのメールの件数が多い場合は3の処理に長時間かかる可能性があるためご注意下さい。 途中で止めるとデータの不整合が発生し、タグを認識できなくなったり、メールフィルターが機能しなくなったりする恐れがあります。 安全のためには、トラブル発生時に切り戻せるよう事前にThunderbirdのプロファイル全体をバックアップしておいてから、オフラインモードでスクリプトを実行して、無事に完了したらオフラインモードを解除する、という順で操作を行うのがおすすめです。

POP3で受信済みのメールがないなど、そのようなタグが設定されているメールがないことが確実な場合は、3の処理を省略して1と2だけを行う以下のスクリプトが使えます。

(async () => {
  const { MailServices } =
    ChromeUtils.importESModule("resource:///modules/MailServices.sys.mjs");
  const tags = MailServices.tags;

  async function md5(s) {
    const hash = Cc["@mozilla.org/security/hash;1"]
      .createInstance(Ci.nsICryptoHash);
    hash.init(Ci.nsICryptoHash.MD5);
    const bytes = new TextEncoder().encode(s);
    hash.update(bytes, bytes.length);
    const binary = hash.finish(false);
    return Array.from(binary, c =>
      c.charCodeAt(0).toString(16).padStart(2, "0")
    ).join("");
  }

  function updateFilters(oldKey, newKey) {
    for (const account of MailServices.accounts.accounts) {
      const server = account.incomingServer;
      if (!server) continue;
      let list;
      try {
        list = server.getFilterList(null);
      } catch (_) {
        continue;
      }
      if (!list) continue;
      for (let i = 0; i < list.filterCount; i++) {
        const filter = list.getFilterAt(i);
        let changed = false;
        for (const action of filter.sortedActionList) {
          if (action.type == Ci.nsMsgFilterAction.AddTag &&
              action.strValue == oldKey) {
            action.strValue = newKey;
            changed = true;
            console.log(
              `filter "${filter.filterName}" action: ` +
              `${oldKey} -> ${newKey}`
            );
          }
        }
        for (const term of filter.searchTerms) {
          if (term.attrib == Ci.nsMsgSearchAttrib.Keywords &&
              term.value.str == oldKey) {
            const value = term.value;
            value.str = newKey;
            term.value = value;
            changed = true;
            console.log(
              `filter "${filter.filterName}" condition: ` +
              `${oldKey} -> ${newKey}`
            );
          }
        }
        if (changed)
          list.saveToDefaultFile();
      }
    }
  }
  for (const t of tags.getAllTags()) {
    if (t.key.length <= 32 ||
        !/=[0-9a-f]{2}/.test(t.key))
      continue;
    const newKey = await md5(t.tag);
    console.log(`${t.tag}: ${t.key} -> ${newKey}`);
    tags.addTagForKey(newKey, t.tag, t.color, t.ordinal);
    updateFilters(t.key, newKey);
    tags.deleteKey(t.key);
  }
  console.log('Done.');
})();

開発元へのフィードバック

本件は直接的にはThunderbirdの「不具合」ではなく、メールサーバー側に元々あった制限によって発生する現象です。 ただ、「誰の責任でもなく、どこにも報告しようがない」と考えてしまうと、システム間の責任分界の隙間に落ちた問題が、誰にも対処されないまま存在し続ける状態になってしまいます。

今回は、Thunderbird側の仕様変更によって今までよりもメールサーバー側の制限に抵触しやすくなってしまったという点に着目し、調査結果を添えて2073409 - Non-ASCII tags easily hit max-length limitation of IMAP servers after 650623 was fixedとしてThunderbirdの開発元に報告を行いました。 その結果、開発チームにも問題として認識してもらえたようで、タグの内部名を生成する際により短い名前を使うようにする修正案を早々に出してもらえました。

この修正案が将来のバージョンに反映されれば、前述のような回避策を取らないでも「タグを保存できない」状態は解消されます。 ただ、セキュリティに関わる問題ではないため、Thunderbird ESR153にも修正が反映される可能性はあまり高くないのではないかとも見込まれます。 本件の影響を実際に受けている方は、前述の暫定回避策を試しつつ、こちらのBugの動向を注視しておくとよいかもしれません。

まとめ

以上、Thunderbird 153以降のバージョンで日本語のタグを設定しても消えてしまう現象の原因と暫定的な回避策、および今後の展望をご紹介しました。

当社で提供しているThunderbirdの法人向け有償サポートでは、本件のようなトラブルのお問い合わせを受けての原因調査と回避策の立案・提供、根本的解決のための開発元へのフィードバックなどを行っています。 Thunderbirdの運用で何かお困り事がある企業の運用担当者さまは、是非お問い合わせフォームよりご連絡下さい。