読者です 読者をやめる 読者になる 読者になる

Thunderbirdのメール(メッセージ)を検索・選択するURLプロトコルの追加方法

昨晩から、自分では使わないThunderbird拡張機能周りを何故かいろいろ調べていたので、そのメモ。

やりたいこと

HTMLに記述された次のようなリンクが記述されているとする。

<a href="mailmsg://HelloWorld">件名に「HelloWorld」を含むメールを検索する</a>

このリンクをクリックすると、thunderbirdが立ち上がって(or 既に起動している場合、それにフォーカスが移って)、「HelloWorld」という文字列を件名(Subject)に含むメールが選択された状態になるようにしたい。

方法

以下の三つを組み合わせると、それっぽいのができそう。

  • "mailmsg://..."というリンクがクリックされた場合に、thunderbirdコマンドが呼び出されるようにレジストリに設定を追加する。 ※ Windowsの場合
  • thunderbirdコマンドにメール検索用のオプションを追加する ※ 検索文字列をコマンドラインから渡せるようにする
  • thunderbirdにメール検索・選択用のアドオンを追加する ※ 上で取得した検索文字列を元に、メールの検索を行う

実装など

以下は、その実装や手順などのメモ。
※ ちなみにメールの検索にGoldaを用いているため、バージョン3未満のThunderbirdには非対応

コマンドラインの拡張

まずは、thinderbirdコマンドに次のようなオプションを追加する。

# thunderbirdを起動して、件名に<検索文字列>を含むメールを選択する
> thunderbird -srchmsg <検索文字列>

そのためには、C:\Program Files\Mozilla Thunderbird\components\に以下の内容のファイルを追加する*1

/**
 * ファイル名: clh_setquery.js
 * 
 *【参考URL】
 * [CommandLineHandler] https://developer.mozilla.org/en/Chrome/Command_Line
 */
const nsISupports           = Components.interfaces.nsISupports;
const nsICategoryManager    = Components.interfaces.nsICategoryManager;
const nsIComponentRegistrar = Components.interfaces.nsIComponentRegistrar;
const nsICommandLine        = Components.interfaces.nsICommandLine;
const nsICommandLineHandler = Components.interfaces.nsICommandLineHandler;
const nsIFactory            = Components.interfaces.nsIFactory;
const nsIModule             = Components.interfaces.nsIModule;

// contract id
const clh_contractID = "@mozilla.org/commandlinehandler/set_message_search_query;1";

// use uuidgen to generate a unique ID
const clh_CID = Components.ID("{cfd3d465-ddf2-45dd-8044-464dfcd32b13}");

// category names are sorted alphabetically. Typical command-line handlers use a
// category that begins with the letter "m".
const clh_category = "m-srchmsg";

/**
 * The XPCOM component that implements nsICommandLineHandler.
 * It also implements nsIFactory to serve as its own singleton factory.
 */
const myAppHandler = {
  /* nsISupports */
  QueryInterface : function clh_QI(iid) 
  {
    if (iid.equals(nsICommandLineHandler) || iid.equals(nsIFactory) || iid.equals(nsISupports))
	return this;

    throw Components.results.NS_ERROR_NO_INTERFACE;
  },
       
  /* nsICommandLineHandler */
  handle : function clh_handle(cmdLine)
  {
    try {
      // コマンドラインで、'-srchms <検索文字列>'形式のオプションが指定された場合の処理
      var query = cmdLine.handleFlagWithParam("srchmsg", false);
      if (query) {
        this.helpInfo = query;   // 簡単のため、検索文字列の保存には(元々用意されている)helpInfo変数を使うことにする
        cmdLine.preventDefault = false;
      }
    }
    catch (e) {
      Components.utils.reportError("incorrect parameter passed to -viewapp on the command line.");
    }
  },

  /* ヘルプ文字列: 今回はこれを検索文字列保存のために流用するので、初期値は空 */
  helpInfo : "",

  /* nsIFactory */
  createInstance : function clh_CI(outer, iid)
  {
    if (outer != null)
      throw Components.results.NS_ERROR_NO_AGGREGATION;

    return this.QueryInterface(iid);
  },

  lockFactory : function clh_lock(lock) { /* no-op */ }
};

/**
 * The XPCOM glue that implements nsIModule
 */
const myAppHandlerModule = {
  /* nsISupports */
  QueryInterface : function mod_QI(iid)
  {
    if (iid.equals(nsIModule) ||
        iid.equals(nsISupports))
      return this;

    throw Components.results.NS_ERROR_NO_INTERFACE;
  },

  /* nsIModule */
  getClassObject : function mod_gch(compMgr, cid, iid)
  {
    if (cid.equals(clh_CID))
      return myAppHandler.QueryInterface(iid);

    throw Components.results.NS_ERROR_NOT_REGISTERED;
  },

  registerSelf : function mod_regself(compMgr, fileSpec, location, type)
  {
    compMgr.QueryInterface(nsIComponentRegistrar);

    compMgr.registerFactoryLocation(clh_CID,
                                    "myAppHandler",
                                    clh_contractID,
                                    fileSpec,
                                    location,
                                    type);

    var catMan = Components.classes["@mozilla.org/categorymanager;1"].
      getService(nsICategoryManager);
    catMan.addCategoryEntry("command-line-handler",
                            clh_category,
                            clh_contractID, true, true);
  },

  unregisterSelf : function mod_unreg(compMgr, location, type)
  {
    compMgr.QueryInterface(nsIComponentRegistrar);
    compMgr.unregisterFactoryLocation(clh_CID, location);

    var catMan = Components.classes["@mozilla.org/categorymanager;1"].
      getService(nsICategoryManager);
    catMan.deleteCategoryEntry("command-line-handler", clh_category);
  },

  canUnload : function (compMgr)
  {
    return true;
  }
};

/* The NSGetModule function is the magic entry point that XPCOM uses to find what XPCOM objects
 * this component provides
 */
function NSGetModule(comMgr, fileSpec)
{
  return myAppHandlerModule;
}

ファイル追加後は、起動しているThunderbirdプロセスがあれば終了し、C:\Documents and Settings\<ログインユーザ名>\Application Data\Mozilla\Firefox\Profiles\<適当な文字列>.default\以下にあるcompreg.datというキャッシュファイルを削除する。
これで、thunderbirdコマンドに"srchmsg"オプションが追加された。

・起動時に指定された文字列でメールを検索・選択するアドオン

次は、上のコマンドオプションに指定された文字列を元に、メールの検索・選択を行うアドオンの実装。
 ※ アドオンの作り方は、次のサイトを参考にした: Building a Thunderbird extension 1: introduction - Mozilla | MDN

ディレクトリ構造:

- search_message/
  - install.rdf
  - chrome.manifest
  - content/
    - search_message.js
    - search_message.xul

install.rdf:

<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
  <Description about="urn:mozilla:install-manifest">
    <em:id>search_message@ab.cd</em:id>
    <em:name>Search Message</em:name>
    <em:version>1.0</em:version>
    <em:creator>sile</em:creator>  
    <em:targetApplication>
      <Description>
        <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id>
        <em:minVersion>3.0</em:minVersion>
        <em:maxVersion>3.0.*</em:maxVersion>
      </Description>
    </em:targetApplication>
  </Description>      
</RDF>

chrome.manifest:

content     search_message    content/
overlay chrome://messenger/content/messenger.xul chrome://search_message/content/search_message.xul

search_message.xul:

<?xml version="1.0"?>
<!-- search_message.jsをロードするだけ -->
<overlay id="search_message" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/x-javascript" src="chrome://search_message/content/search_message.js" />
</overlay>

search_message.js:

/**
 *  このjavascriptは、"focus"イベントにメッセージ検索関数(search_message)を登録する。
 *  フォーカスが移るたびに、search_message関数が呼び出されることになるが、(かなり手抜きの)重複呼び出しチェックがあるので、
 * 実質上コマンドの実行直後の一回しか、メッセージの検索・選択は行われない。
 *
 * [注意1] 条件に該当するメールが複数ある場合でも、一つのメールしか選択されない。
 * [注意2] 同じ文字列を続けて検索した場合、二度目以降は無視される。 ※ 重複チェックとの関係上
 *
 *【参考URL】
 * [Gloda] https://developer.mozilla.org/en/Thunderbird/Gloda_examples
 * [Gloda] https://developer.mozilla.org/en/Thunderbird/Creating_a_Gloda_message_query
 * [Message選択] https://developer.mozilla.org/en/Extensions/Thunderbird/HowTos/Common_Thunderbird_Use_Cases/Open_Folder
 * [JavaScriptからのXPCOM参照] https://developer.mozilla.org/en/How_to_Build_an_XPCOM_Component_in_Javascript
 */
// メッセージ検索用のリスナー
srchListener = {
  onItemsAdded: function _onItemsAdded(aItems, aCollection) {},  
  onItemsModified: function _onItemsModified(aItems, aCollection) {},  
  onItemsRemoved: function _onItemsRemoved(aItems, aCollection) {},  
  onQueryCompleted: function _onQueryCompleted(msg_coll) {
    try{
      // 最初の一つだけを選択する
      msg=msg_coll.items.pop();
      gFolderTreeView.selectFolder(msg.folderMessage.folder);  // フォルダを移動
      gFolderDisplay.selectMessage(msg.folderMessage);         // メッセージを選択
    } catch(e) {
      alert("Error: "+e);
    }
  }
}

// 検索対象となる件名文字列を取得する
var get_search_subject = function () {
  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
  var clh = Components.classes['@mozilla.org/commandlinehandler/set_message_search_query;1']
                                    .createInstance(Components.interfaces.nsICommandLineHandler);
  return clh.helpInfo;
}

// 前回の検索した件名文字列を保存するグローバル変数 
//  ※ 現在は、'focus'イベントに検索関数を登録しているため、この変数が必要。
//    これがないと、ユーザが画面を切り替えるたびに、メッセージの検索+選択が行われてしまう。
//    (thunderbirdコマンド実行時にだけ、検索が行われてほしい)。
//  ※ 以下の実装では、件名文字列をそのまま保存・比較しているが、'文字列+コマンド実行時のタイムスタンプ'などの形式の方が望ましい。
//  ※ もしnsICommandLineHandlerから、thunderbirdのイベントを呼び出せるなら、この検索専用のイベントを作成・使用した方が良い。
g_prev_sbj=''; 

// メッセージを検索(+ 選択)する
var search_message = function() {
  // 検索文字列を取得する
  sbj = decodeURIComponent(get_search_subject().replace(/^mailmsg:\/\//,"").replace(/\/$/,""));
  if(sbj==g_prev_sbj) return;  // 重複検索を避ける
  g_prev_sbj=sbj;

  try {
    // Glodaを用いて検索を行う
    query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
    query.subjectMatches(sbj);
    query.getCollection(srchListener);
  } catch(e) {
    alert("ERROR: "+e);
  }
}

// イベントを登録する
addEventListener('focus', search_message, false);

これらのファイルを、zip圧縮でまとめ、Thunderbirdのアドオン画面からインストールすれば、コマンドラインから(件名による)メールの検索・選択が行えるようになる。
※ 圧縮ファイルの拡張子はzipではなく、xpiにする。
※ 圧縮ディレクトリの直下に、install.rdfchrome.manifestが配置されるようにする。

# 件名に"scheme"を含むメールを検索
> thunderbird -srchmsg scheme
・srchmsg URLプロトコルの追加

ここまででコマンドラインからのメールの検索・選択はできるようになったので、最後はmailtoなどと同様に、HTMLのリンクを使ってブラウザから同様のことを行えるようにする。
そのために必要なのは、以下のようなレジストリの設定のみ。

;; 基本的には、mailtoの設定とほぼ同様
+ HKEY_LOCAL_MACHINE\SOFTWARE\Classes\mailmsg\
  - (既定):REG_SZ  "URL:mailmsg Protocol"
  - EditFlags:REG_BINARY  02 00 00 00
  - URL Protocol:REG_SZ  ""
  + DefaultIcon\
    - (既定):REG_SZ  "[thunderbird.exeのパス]"
  + shell\
    + open\
      + command\
        - (既定):REG_SZ  "[thunderbird.exeのパス] -srchmsg "%1"


以上で、冒頭に載せたようなHTMLリンクが使えるようになる(はず)

追記(2010/02/22): テスト用HTML

<html>
<head>
 <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
 <title>メール選択テスト</title>
</head>
<body>
<script>
<!--
var srchmail = function() {
  location.href = "mailmsg://"+encodeURIComponent(document.getElementById("subject").value);
}
-->
</script>
Subject: <input type="text" id="subject" /><input type="button" value="メールを選択する" onclick="srchmail()" />
</body>
</html>

*1:Windowsの場合。以下同