thunderbird から todoist にタスクを登録する

#Thunderbirdに「Todoistのタスクを登録する」ボタンを追加する

Firefox、Thunderbirdのアドオンを作ったことが無い私が挑戦してみた。
環境はWindows10、Thunderbird　52.1.1で作成した。




完成したのはこんなアドオンです

▼追加したボタン
![1.png](https://qiita-image-store.s3.amazonaws.com/0/184636/d4108b5e-7a09-f1da-31f7-61ff76c791ec.png)

▼登録確認画面
![1.png](https://qiita-image-store.s3.amazonaws.com/0/184636/137293f1-7b1c-275e-34fa-6ceee0eb0b29.png)



##そもそも、なぜこんなアドオンを開発しようとしたか

###■公式アドオンがない

公式Todoistが、Thunderbird対応を止めてしまったので、Thunderbird 52.1.1で受け取ったメールを簡単にタスク化するには、メール本文などをコピペするしかなかった。（代わりになるアドオンなどは無さそう？）


###■タスク管理方法

下記のような方法でタスクを管理しようとしたが、性に合わなくて続かなかった。

* Lightningアドオンを使う方法
* 受信箱をタスクリストにする方法
* 既読にしない方法
* Thunderbirdのタグをつける方法など


唯一Todoistは続いている。
あまり有料ソフトは使わないのだが、Todoistは年会費を払っている。

Todoistは、いつまでに、優先度付けなどを活用し、タスクを管理するサービス
私は、進捗管理をしない前提（コメントの内容やサブタスクの消化具合で判断している）


##アドオンの仕様を検討した

* メールを選択して、ヘッド表示部（Fromなどのヘッダ情報が表示される部分）にある返信ボタンの横に、ボタンを追加する
* タスクのタイトルは、メールの件名or本文の選択したテキスト
* タスクに自動的にコメントを追加する
* コメントの内容は、メールのヘッダ情報の一部（日付、件名、Fromなど）とメール本文
* 追加したボタンを押したら登録確認画面が表示されて、登録ボタンを押したら、APIで登録される

これが実現できれば、とりあえず使えそうだなということで、仕様はなるべく簡単にした。

###■あきらめたこと

・各自のTodoistの「APIトークン」を設定する画面を作ること
　→　設定エディタに直接登録することにした
・タスクを追加するプロジェクトを選択すること
・期限を自由に選択すること
・タスクに書かれたリンクをクリックすると、メールが開くこと（できないよね？）
・関連メールのやり取り（スレッド）のタスクは、サブタスクにすること
　ヘッダ情報をコメントに埋め込めば、できるかなぁ

などなど。

インボックスで、今日の期限のタスクに表示されるので、ブラウザ上で修正することにした。 


##やったこと 

###(1) 開発環境準備 

・thunderebirdの開発用プロファイルを準備する

私はポータブル版だったので、普通とやり方が少し違うのかも？

C:\ThunderbirdPortable\App\Thunderbird\thunderbird.exe -p
だったか（覚えていない）
C:\ThunderbirdPortable\App\Thunderbird\thunderbird.exe -ProfileManager
として起動後、プロファイルを新規作成した。

・開発用希望batの作成

batファイルを作って、中身を
C:\ThunderbirdPortable\App\Thunderbird\thunderbird.exe -p "開発用"
とした。

・いくつかの設定を変更
[このサイト](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment#Recommended_development_preferences)を参考にして、
ツール＞オプション＞詳細＞設定エディタ
の画面で変更した。（ここで変更することすら知らなかったので苦労した）


・作業ディレクトリの準備
C:\develop\t2t\
を作成した。ここで開発する。

C:\Users\xxxx\AppData\Roaming\Thunderbird\Profiles\xxxxx.開発用\extensions\
に新規で
thunderbirdToDoist@sample.com
というファイルを作成した。

ファイルの中身は、
C:\develop\t2t\
とした。 

###(2) 開発開始 

詳しくは、先人たちのWEBと、添付したソースを見てもらいたい。
正確な内容は理解していないので。
・ソースにコメントを少しだけ入れた。（途中で飽きた）
・面倒なので、インデントもテキトウ（－－；）
・デバッグログなども残したまま

※javascriptは全然わからない、firefox/thunderebird開発経験ゼロといった
　プログラム初心者なので、色々ご容赦いただきたい。
　質問などされても全く分からない。 


###▼タスク登録する部分だけ少し解説（ソースのコメントで）
あと、登録数に制限があるのかわからないが、日を跨いだら同じソースでも
登録に成功したということがあった。
なにかTodoist側で問題でもあったのだろうか・・・？

apiトークンは、Todiostの設定画面から拾ってくる、
設定エディタで「extensions.t2t.setting.apikey」として登録済みとする。 

```js
//APIトークンを、設定から持ってくる
let prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
let apitoken = prefs.getCharPref("extensions.t2t.setting.apikey");


//タスクを登録する
  var url = "https://todoist.com/API/v7/sync",
      formData, response;

  formData = {
    "token": apitoken,
    "commands": JSON.stringify(
    [
      { "type"    : "item_add",
        "uuid": uuid,    //なんの文字列でも良さそう？
        "temp_id" : uuid+"a", //<<<<ここのtemp_idと、コメント（note_add）のitem_idを同じにしないと、コメントが登録されない（中身はなんの文字列でも良さそう？）
        "args": {
           "content": taskname,   //タスク名
           "date_string": "today" //タスク期限を今日にする
        }
      },
      {
        "type": "note_add", //タスクコメントを追加する
        "uuid": uuid+"c",  //なんの文字列でも良さそう？
        "temp_id": uuid+"b",    //なんの文字列でも良さそう？
        "args": {
           "content": comment,   //コメント本文
           "item_id": uuid+"a"   //item_addと同じ文字列
        }
      }
    ])
  }; 
...
```

あとは、XMLHttpRequestでテキトウにPOST送信した。 


※先人達のサンプルをコピペしていじっただけです。感謝。
※ソースは自由に改変してもらって良いので、誰かもっと良いものに仕上げてください。お願いします。 

##ソース

###taskAdd.xul
```xml
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>

<dialog id="asdf" title="Add task to todoist"
        buttons="accept,cancel"
        buttonlabelaccept="Add Task"
        buttonaccesskeyaccept="a"
        ondialogaccept="return doOK();"
        buttonlabelcancel="Cancel"
        buttonaccesskeycancel="c"
        ondialogcancel="return doCancel();"
        onload="onLoad();"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

<vbox>
<hbox>
<description value="タスク名：" /><textbox id="txttaskname" width="700px" />
</hbox>
<hbox>
<description value="コメント：" /><textbox id="txtcomment" multiline="true" height="400px" width="700px" />
</hbox>
</vbox>

<script type="application/x-javascript" src="taskAdd.js" />

</dialog>
...
```

###taskAdd.js
```js
function onLoad() {
  //カーソルを先頭に
  document.getElementById("txttaskname").value = window.arguments[0].taskname;
  document.getElementById("txtcomment").value = window.arguments[0].comment;
  document.getElementById("txtcomment").selectionStart = 0;
  document.getElementById("txtcomment").selectionEnd = 0;
  document.getElementById("txtcomment").focus();

}


function doCancel()
{
  //alert("You pressed Cancel!");
  //return true;
  
  return true;
}


function doOK()
{
  //alert("You pressed OK!");
  //return true;
  
  
  //1:タスク名
  //2:uuid = fromaddress+date
  //3:コメント内容：本文
  createTask(window.arguments[0].taskname,
             window.arguments[0].uuid,
             window.arguments[0].comment);

  return true;

}




// タスク登録
function createTask(taskname, uuid, comment) {


let prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
let apitoken = prefs.getCharPref("extensions.t2t.setting.apikey");
//nsPreferences.setUnicharPref("extensions.t2t.setting.apitoken", 123);

console.log(uuid);

  var url = "https://todoist.com/API/v7/sync",
      formData, response;

  formData = {
    "token": apitoken,
    "commands": JSON.stringify(
    [
      { "type"    : "item_add",
        "uuid": uuid,
        "temp_id" : uuid+"a",
        "args": {
           "content": taskname,
           "date_string": "today"
        }
      },
      {
        "type": "note_add",
        "uuid": uuid+"c",
        "temp_id": uuid+"b",
        "args": {
           "content": comment,
           "item_id": uuid+"a"
        }
      }
    ])
  };



var xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.onreadystatechange = function()
{

    var READYSTATE_COMPLETED = 4;
    var HTTP_STATUS_OK = 200;

    if( xmlHttpRequest.readyState == READYSTATE_COMPLETED
     && xmlHttpRequest.status == HTTP_STATUS_OK)
    {
        // レスポンスの表示
        console.log( xmlHttpRequest.responseText );
        alert("タスク登録　成功");// +  xmlHttpRequest.responseText  );
    }

    //ステータスNGの時もレスポンスが見たい
	console.log( "-------------------------" );
	console.log( xmlHttpRequest.readyState );
	console.log( xmlHttpRequest.status );
	console.log( xmlHttpRequest.responseText );
	console.log( "^^^^^^^^^^^^^^^^^^^^^^^^^" );

}

xmlHttpRequest.open( 'POST', url, false );//非同期true

// サーバに対して解析方法を指定する
xmlHttpRequest.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );

// データをリクエスト ボディに含めて送信する
xmlHttpRequest.send( EncodeHTMLForm( formData ) );
/*
if (xmlHttpRequest.status===200){
console.log("OK");
}else{
console.log("NG");
}
*/

}

function EncodeHTMLForm( data )
{
    var params = [];

    for( var name in data )
    {
        var value = data[ name ];
        var param = encodeURIComponent( name ).replace( /%20/g, '+' )
            + '=' + encodeURIComponent( value ).replace( /%20/g, '+' );

        params.push( param );
    }

    return params.join( '&' );
} 
...
```


###taskAddConfirm.xul
```xml
<?xml version="1.0"?>
<overlay id="t2t"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

<script type="application/x-javascript" src="taskAddConfirm.js" />

<toolbox id="header-view-toolbox" position="1" >
  <toolbar position="1">
    <toolbarbutton label="タスク追加"  oncommand="taskAddConfirm(this)" />
  </toolbar>
</toolbox>

</overlay>
...
```



###taskAddConfirm.js
```js
function taskAddConfirm( me ) 
{

/* ログを出力する方法
    var consoleService = Components.
            classes["@mozilla.org/consoleservice;1"].
            getService( Components.interfaces.nsIConsoleService );
    consoleService.logStringMessage( 'Some messages' );
*/


var tname;    //タスク名（メール件名or選択した文字列）
var comm;     //コメント（メール本文）


//本文を選択していなければ、タスク名はメール件名。
//本文を選択していたら、タスク名は、選択文字列


//メッセージペインで選択した文字列の取得方法
//me.label = document.getElementById("messagepane").contentDocument.getSelection().toString().length;


//メッセージペインで選択した文字列の取得
//文字列を選択していたら、その文字列がタスク名となる。
//文字列を選択していなければ、メール件名が、タスク名となる。
var deltag;
deltag = document.getElementById("messagepane").contentDocument.getSelection().toString();
if (deltag.length == 0) {
  tname = document.getAnonymousElementByAttribute(document.getElementById('expandedsubjectBox'), 'anonid', 'headerValue').textContent;
} else {
  tname = deltag;
}


//タスクのコメント内容は、次の順序
//From、受信日時、件名
//空行を２つ
//メール本文(HTMLタグとCSSタグは除去するが、&amp;などは元に戻す）

let strEmail = document.getAnonymousElementByAttribute(document.getElementById('expandedfromBox'), 'headerName', 'from').getAttribute('emailAddress');
let strFrom = document.getAnonymousElementByAttribute(document.getElementById('expandedfromBox'), 'headerName', 'from').getAttribute('fullAddress');
let strSubject = document.getAnonymousElementByAttribute(document.getElementById('expandedsubjectBox'), 'anonid', 'headerValue').textContent;
let strDate = document.getElementById('dateLabel').textContent;

var buf;
buf = strFrom +"\r\n" + strDate + "\r\n" + strSubject + "\r\n\r\n";

deltag = document.getElementById("messagepane").contentDocument.body.innerHTML;
//  deltag = document.getElementById("messagepane").contentDocument.body.innerHTML;

  deltag = deltag.replace(/<style.*?>[.\s\S]*?<\/style.*?>/g,'');
  deltag = deltag.replace(/<script.*?>[.\s\S]*?<\/script.*?>/g,'');
  deltag = deltag.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'');
  deltag = deltag.replace(/ */gm,'');
  deltag = deltag.replace(/^\r*\n+/gm,'');
  //deltag = deltag.replace(/([ \s\S]){2,}/g,'\r\n');
  //comm = unescape(deltag);

comm = buf + unescapeHTML(deltag);
//comm = buf + deltag;



var uid = new Date().getTime().toString();
var params = {taskname: tname,   //タスク名
              comment: comm,     //コメント
              uuid: uid};        //uuid

          
window.openDialog("chrome://t2t/content/taskAdd.xul","addtask",
                  "chrome,scrollbars,resizable,centerscreen,height=500,width=800", params).focus();

}


function unescapeHTML(html) {
   var htmlNode = document.createElement("div");
   htmlNode.innerHTML = html;
   if(htmlNode.innerText !== undefined)
      return htmlNode.innerText; // IE
   return htmlNode.textContent; // FF
}

...
```