うならぼ

申し訳程度のアフィリエイトとか広告とか解析とかは/aboutを参照

ThunderbirdのAPIを叩いてアドレス帳にリストをインポートする

単純にアドレス帳のエントリをインポートするのは標準機能でできたが、リストを取りこむ方法が見つからなかった。数が多く手打ちも面倒だったので、なんとかしてインポートしたい。ここでふたつの可能性を見つけた。

  • リスト編集画面で メアド→候補が出るまで待つ→Enter ないし 名前 <メアド>→Enter という操作を再現する
  • JavaScriptAPIを叩く

せっかくなので後者に挑戦した。幸い、MDNにアドレス帳を操作するサンプルを見つけることができた。

Address Book examples - Mozilla | MDN

結論から言えば十分な情報が得られたわけではなく、開発者ツールでひたすらメンバや挙動を探ったりしたわけだが。

調査結果

前述のサンプルにもあるが、大筋はこういうことになる。

  • リストの親となるアドレス帳を取得する(列挙はできても名前等で検索する方法はない気がする)
  • アドレス帳からユーザーを検索する
    • addressLists等を列挙する
    • cardForEmailAddress や getCardFromProperty で検索する
    • Lisp式風のクエリで抽出する(論理演算子 and/or/not は必須*1で、それと (フィールド,比較演算子,値) を組みあわせていく)
  • リストの操作
    • 追加
      1. nsIAbDirectoryのインスタンスを作る
      2. isMailListやdirNameを設定する
      3. addressLists.appendElementでエントリを追加する
      4. アドレス帳に addMailList で登録する
    • 取得
      • A: 親の addressLists を列挙する→nsIAbDirectoryが取れる
      • B: クエリでIsMailListがtrueなエントリを抽出する→nsIAbCardが取れる
      • nsIAbCardからnsIAbDirectoryは card.mailListURI を引数に abManager.GetDirectory() を呼びだすことで取得できる
    • 編集
      1. nsIAbDirectoryを取得する
      2. いじる
      3. リスト自身のプロパティは即時反映、addressList は list.editMailListToDatabase(null) で保存する。 *2
    • 削除
      1. nsIAbDirectory を取得、まあしなくてもよい
      2. abManager.deleteAddressBook()URIを渡す

ところでaddressListsプロパティは GetElementAt() で取得するとそのまま nsIAbCard なり nsIAbDirectory として使えたが、enumerate() を経由すると別途 QueryInterface が必要だった。非ジェネリック版の IEnumerator のような感じだろうか。

具体例

各メンバーは既にアドレス帳に登録されていて、それをメアドを見てリストに登録する。最初はリスト名とメアドの配列をJS側で指定することを考えたが、気がついたらリストのdescriptionにメアドをカンマ区切りで書いておいたものを読むようにしていた。

(function(){
    const iterToJS = function*(iter) { while(iter.hasMoreElements()) yield iter.getNext(); };
    const abManager = Components.classes["@mozilla.org/abmanager;1"]
        .getService(Components.interfaces.nsIAbManager);

    for (const directory of iterToJS(abManager.directories)) {
        for (const rawList of iterToJS(directory.addressLists.enumerate())) {
            const list = rawList.QueryInterface(Components.interfaces.nsIAbDirectory);
            const emails = list.description.split(',');
            if (!emails[0].includes('@')) continue;

            console.log(`building list: ${list.dirName}`)

            for (const email of emails) {
                const card = directory.cardForEmailAddress(email);
                if (card) {
                    list.addressLists.appendElement(card, false);
                } else {
                    console.warn(`${email} is not found`);
                }
            }

            list.description = '';
            list.editMailListToDatabase(null);
        }
    }
})();

で、逆にこれをエクスポートしたい時にも困るんだろうなーと逆パターンも適当に書いてみた。ただこちらに関してはリスト自身の情報も含めてJSONかなにかで出力した方が便利かもしれない。というか100件近く登録したらリストの編集画面を開くのに時間がかかってしまってうーんという感じ。

(function(){
    const iterToJS = function*(iter) { while(iter.hasMoreElements()) yield iter.getNext(); };
    const abManager = Components.classes["@mozilla.org/abmanager;1"]
        .getService(Components.interfaces.nsIAbManager);

    for (const directory of iterToJS(abManager.directories)) {
        for (const rawList of iterToJS(directory.addressLists.enumerate())) {
            const list = rawList.QueryInterface(Components.interfaces.nsIAbDirectory);
            if (list.description !== '') continue;

            list.description =
                [...iterToJS(list.addressLists.enumerate())]
                .map(x => x.QueryInterface(Components.interfaces.nsIAbCard).primaryEmail)
                .join(',');
        }
    }
})();

実行方法としては…メインウィンドウで Ctrl+Shift+I を押して開発者ツールを開いてスクラッチパッドで…。

ところで、Thunderbirdのアドレス帳は abook.mab というファイルに保存されているが、どうも削除した連絡先やリストの情報も残っているように見える。だとすると、descriptionに書くやりかたは余計なゴミを残してしまうのでよくないかもしれない。

*1:このあたりドキュメントがなくて結局ソースを見た mailnews/addrbook/src/nsAbQueryStringToExpression.cpp

*2:サンプルではnsIAbCardを変更するよう書かれているが、そもそもThunderbird本体でもツリーから編集画面を開くとnsIAbCardを使っていない気がする。suite/mailnews/addrbook/abCommon.js