うならぼ

申し訳程度のアフィリエイトとか広告とか解析とかは/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

Mastodonで見つけた循環import

Mastodonのフロント側で、あるモジュールの関数において、そのモジュールの依存モジュールが一部読み込まれていないらしい、という問題がありました。こんなエラー。

Uncaught TypeError: Cannot read property 'a' of undefined
    at Object.configureStore [as a] (98:31)
    at Object.eval (94:59)
    at eval (94:199)
    at Object.<anonymous> (common.js:1197)
    at __webpack_require__ (common.js:55)
    at Object.eval (98:8)
    at eval (98:55)
    at Object.<anonymous> (common.js:1244)
    at __webpack_require__ (common.js:55)
    at Object.eval (589:7)

下のコードで configureStore() を呼び出そうとしていたのですが、何故か loadingBarMiddleware が読み込まれない。調べていった結果、そもそも以下のimport群のうち hydrateAction の行までしか実行されていないことがわかりました。

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import appReducer, { createReducer } from '../reducers';
import { hydrateStoreLazy } from '../actions/store';
import { hydrateAction } from '../containers/mastodon';
import loadingBarMiddleware from '../middleware/loading_bar';
import errorsMiddleware from '../middleware/errors';
import soundsMiddleware from '../middleware/sounds';

export default function configureStore() {
  ...
}

上記のコードはWebpackによって次のようなコードに変換されています。

/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (immutable) */ __webpack_exports__["a"] = configureStore;
/* harmony export (immutable) */ __webpack_exports__["b"] = injectAsyncReducer;
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_redux__ = __webpack_require__(/*! redux */ 169);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_redux_thunk__ = __webpack_require__(/*! redux-thunk */ 370);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_redux_thunk___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_redux_thunk__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__reducers__ = __webpack_require__(/*! ../reducers */ 371);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__actions_store__ = __webpack_require__(/*! ../actions/store */ 35);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__middleware_loading_bar__ = __webpack_require__(/*! ../middleware/loading_bar */ 430);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__middleware_errors__ = __webpack_require__(/*! ../middleware/errors */ 431);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_6__middleware_sounds__ = __webpack_require__(/*! ../middleware/sounds */ 432);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_7__containers_mastodon__ = __webpack_require__(/*! ../containers/mastodon */ 94);
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

function configureStore() {
  ...
}

全体を包む無名関数の中で、import文はそれぞれ __webpack_require__() の呼び出しで実現されていることがわかります。JavaScriptにおいて関数宣言は定義ごと巻き上げられるので、もしimportの最中に configureStore() が呼ばれてしまったら、同じような結果を得ることができそうです。

ん?/containers/mastodon だって?

import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
...
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
const { localeData, messages } = getLocale();
addLocaleData(localeData);

export const store = configureStore();
const initialState = JSON.parse(document.getElementById('initial-state').textContent);

…ありますね、configureStore()。変数の初期化に使われているので、モジュールを読み込んだだけで実行されます。結果として、configureStoreモジュールのimportをすべて終える前に、間接的に configureStore() が呼ばれてしまう、ということになります。

逆に mastodon モジュールから読み込んだ場合も循環してしまいますが、configureStore モジュールを読み込んだだけで mastodon モジュールの関数が呼び出されることはないので、この場合は問題ありません。

__webpack_require__() は各モジュールを実行する前に、先にキャッシュに登録を行います。一方先に載せたモジュールのコードを見ると、importより先にexportした関数を登録していることがわかります。ですから configureStore モジュールの読み込みが完了する前に mastodon モジュールから configureStore() 関数が呼び出せた、というわけです。