// ==UserScript==
// @name		   GmailSpaceCharsFix
// @namespace	   mtamaki.com
// @include 	   https://mail.google.com/mail/*
// @include 	   http://mail.google.com/mail/*
// ==/UserScript==

(function(){
////////////////////////////////////////////////////////////////////////////////

function xpath_get( xpath, node ) {
	//ref: http://subtech.g.hatena.ne.jp/cho45/20071119/1195408940
	node = node || document;
	var result = ( node.ownerDocument || node ).createExpression( xpath,
		function( prefix ) {
			var result = document.createNSResolver( node ).lookupNamespaceURI( prefix );
			if( result )
				return result;
			else
				return ( document.contentType == "application/xhtml+xml" ) ? "http://www.w3.org/1999/xhtml" : "";
		}
	).evaluate( node, XPathResult.ANY_TYPE, null );
	switch ( result.resultType ) {
		case XPathResult.STRING_TYPE : return result.stringValue;
		case XPathResult.NUMBER_TYPE : return result.numberValue;
		case XPathResult.BOOLEAN_TYPE: return result.booleanValue;
		case XPathResult.UNORDERED_NODE_ITERATOR_TYPE: {
			var results = [];
			var item;
			while( item = result.iterateNext(), item )
				results.push( item );
			return results;
		}
	}
	return null;
}

////////////////////////////////////////

function dom_get_parent_by_tag_name( element, tag_name ) {
	while( element && tag_name != element.tagName )
		element = element.parentElement;
	return element
}

////////////////////////////////////////

//objの要素を一つづつ順に取り出してfuncに処理させる。funcが真を返すと処理を中断する。
function array_each( obj, func ){
	//数値：その数だけループ。引数はインデックス
	if( "number" == typeof obj ) { for( var index = 0; index < obj; ++index ) { if( func( index ) ) return } }
	//文字列：文字ごとにループ
	else if( "string" == typeof obj ) { for( var index = 0; index < obj.length; ++index ) { if( func( obj.substr( index, 1 ) ) ) return } }
	//e4x：プロパティの値ごとにループ
	else if( "xml" == typeof obj ) { for each( var element in obj ) { if( func( element ) ) return } }
	else if( obj ) {
		//配列：要素ごとにループ
		if( null != obj.length ) { for( var index = 0; index < obj.length; ++index ) { if( func( obj[index] ) ) return } }
		//オブジェクト：プロパティの名前ごとにループ
		else { for( var name in obj ) { if( func( name ) ) return } }
	}
}

////////////////////////////////////////

function dom_insert_before ( target, element ) {
	target.parentNode.insertBefore( element, target )
}

////////////////////////////////////////

function dom_create( name, attrs, childs, events ) {
	//ref $N in fastlookupalc.user.js
	attrs = attrs || {}; events = events || {}; childs = childs || []
	var result = document.createElement( name );
	for( attr in attrs )
		result.setAttribute( attr, attrs[attr] )
	for( var index = 0; index < childs.length; ++index ) {
		if( "string" == typeof childs[index] )
			result.appendChild( document.createTextNode( childs[index] ) )
		else
			result.appendChild( childs[index] )
	}
	for( event in events )
		result.addEventListener( event, events[event], false )
	return result
}

////////////////////////////////////////

function event_observe( target_element, event_name, observer_func, capture_or_bubbling ) {
	target_element.addEventListener( event_name, observer_func, capture_or_bubbling || false );
}

////////////////////////////////////////

function http_get( url, after_func, options ) {
	var count = 0;
	if( !http_get.is_not_log ) {
		if( !http_get.count )
			http_get.count = 0
		count = http_get.count++
		GM_log( count + ": " + url )
	}
	options = options || {}
	options["url"] = url
	options["onload"] = function(details) {
		if( !http_get.is_not_log )
			GM_log( count + ": " + url + " " + details.status )
		after_func( details.responseText, details )
	}
	options["headers"] = options["headers"] || {}
	if( options["data"] ) {
		options["method"] = "POST"
		options["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
	}
	else
		options["method"] = "GET"
	GM_xmlhttpRequest(options)
}

////////////////////////////////////////

function dom_remove_all_children( element ) {
	while( element.childNodes.length )
		element.removeChild( element.childNodes[0] )
}

////////////////////////////////////////

function dom_remove( element ) {
	element.parentNode.removeChild( element )
}

////////////////////////////////////////////////////////////////////////////////

//gmail用GMの初期化用関数。callback(gmail)を実装する。
function gmail_ready( event_observe ) {
	return function ( callback ) {
		event_observe(window, 'load',
			function() {
				if (unsafeWindow.gmonkey)
					unsafeWindow.gmonkey.load('1.0', callback);
			}
		)
	}
}
gmail_ready = gmail_ready( event_observe )

////////////////////////////////////////

//ナビゲーションバーに第二引数の名前のリンクを追加する。リンクがクリックされると第三引数のコールバック関数が呼ばれる。引数はクリック時のEventオブジェクトと、クリック後、処理が終わったら呼び出すための関数。
//クリック時にリンクの名前が～Processing...に変化し、処理が終わったら呼び出すための関数を呼ぶとリンクが消える。
function gmail_navigation_bar_add_link( dom_create, dom_insert_before, dom_remove ) {
	return function( gmail, name, callback ) {
		gmail.registerViewChangeCallback(
			function () {
				if( "cv" == gmail.getActiveViewType() ) {
					//ビューのトップのdivを取る。
					var element = gmail.getActiveViewElement()
					//子が複数あるdivの最初のdivまでたどる。これがメールの上のナビゲーションバー
					while( 1 == element.childNodes.length )
						element = element.childNodes[0]
					element = element.childNodes[0]
					//もう一度子が複数あるdivの最初のdivまでたどる。これがナビゲーションバーの左側
					while( 1 == element.childNodes.length )
						element = element.childNodes[0]
					element = element.childNodes[0]
					//もう一度子が複数あるdivまでたどる。これがナビゲーションバーの左側の、実際に各要素が入ってる親div
					while( 1 == element.childNodes.length )
						element = element.childNodes[0]
					//取れたelementの末尾にSpaceCharsFixのリンクを追加する
					element.appendChild( dom_create("div", {"class":element.firstChild.className},[name],{"click":
						function( event ){
							var processing = dom_create("div", {"class":element.firstChild.className},[name + " Processing..."])
							dom_insert_before( event.target, processing )
							dom_remove( event.target )
							callback( event, function(){ dom_remove( processing ) } )
						}
					}) )
				}
			}
		);
	}
}
gmail_navigation_bar_add_link = gmail_navigation_bar_add_link( dom_create, dom_insert_before, dom_remove )

////////////////////////////////////////

//配列を順に見ながら、値を返していく。コールバック関数では[戻り値本体, 列挙を中断するならtrue]を返す。
function array_each_result( array_each ) {
	return function( array, init, func ) {
		array_each(array, function( item ){ result = func( init, item ); init = result[0]; if( 1 < result.length ) return result[1]; return false; } )
		return init
	}
}
array_each_result = array_each_result( array_each )

////////////////////////////////////////

//funcが最初にtrueを返した要素を返す。見つからなかった場合はnull。
//findした結果を加工したいときは、コールバックの中ではなく、この関数の戻り値に対して処理する。
function array_find( array_each ) {
	return function ( array, func ) {
		return array_each_result( array, null, function( result, item ){ if( func(item) ) return [item, true]; return [null] } )
	}
}
array_find = array_find( array_each_result )

////////////////////////////////////////

//gmail2.0に唯一あるform要素のaction属性の値のクエリの、キーに対応する値を取得する。
function gmail_get_form_data( array_find ) {
	return function( key ) {
		var match = array_find( document.body.childNodes, function( node ){ return ("FORM"==node.tagName) } ).getAttribute( "action" ).match(new RegExp( key + "=([^&]+)" ))
		if( match )
			return match[1]
		return null
	}
}
gmail_get_form_data = gmail_get_form_data( array_find )

////////////////////////////////////////

//セッションキーであるikを取得する
function gmail_get_ik( callback ) {
	if( !gmail_get_ik.ik ) {
		http_get( location.protocol + "//" + location.host + "/mail/", function(result){ gmail_get_ik.ik = result.match( /ID_KEY:"([^"]+)"/)[1]; callback( gmail_get_ik.ik )  } )
	}
	else
		callback( gmail_get_ik.ik )
}

////////////////////////////////////////

//array_reduce( [0,1,2,3], function( result, item ){ return result + item; }, 0 ) => 0+1+2+3 = 6
//中断しないarray_each_result
function array_reduce( array_each_result ) {
	return function( array, init, func ){
		return array_each_result( array, init, function( result, item ){ return [func(result,item)]; } )
	}
}
array_reduce = array_reduce( array_each_result )

////////////////////////////////////////

function mail_parse( array_each ) {
	return function ( raw ) {
		var splited = raw.replace(/\r/mg,"").split("\n\n")
		var headers = array_reduce( splited[0].replace(/;\n/mg, ";").split("\n"), {},
			function( headers, header ){
				header = header.split(": ")
				headers[header[0].toLowerCase()] = header[1]
				return headers
			}
		)
		splited.shift()
		return [headers, splited.join("\n\n")]
	}
}
mail_parse = mail_parse( array_reduce )

////////////////////////////////////////

//第二引数のidを持つメールのオリジナルデータを取得する。コールバック関数には[{"header-key":"header-value", ...}, "body"]というデータが渡される。
function gmail_get_original_raw( mail_parse ) {
	return function( ik, msg, callback ) {
		http_get( location.protocol + "//" + location.host + "/mail/?ui=2&ik=" + ik + "&view=om&th=" + msg,
			function( result ) {
				//メールデータをパース
				callback( mail_parse( result ) )
			}
		)
	}
}
gmail_get_original_raw = gmail_get_original_raw( mail_parse )

////////////////////////////////////////

/* Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>
 * Version: 1.0
 * LastModified: Dec 25 1999
 * This library is free.  You can redistribute it and/or modify it.
 */

/*
 * Interfaces:
 * b64 = base64encode(data);
 * data = base64decode(b64);
 */

var base64DecodeChars = new Array(
	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
	52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
	-1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
	15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
	-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
	41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1);

function base64decode(str) {
	var c1, c2, c3, c4;
	var i, len, out;

	len = str.length;
	i = 0;
	out = "";
	while(i < len) {
	/* c1 */
	do {
		c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
	} while(i < len && c1 == -1);
	if(c1 == -1)
		break;

	/* c2 */
	do {
		c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
	} while(i < len && c2 == -1);
	if(c2 == -1)
		break;

	out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));

	/* c3 */
	do {
		c3 = str.charCodeAt(i++) & 0xff;
		if(c3 == 61)
		return out;
		c3 = base64DecodeChars[c3];
	} while(i < len && c3 == -1);
	if(c3 == -1)
		break;

	out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));

	/* c4 */
	do {
		c4 = str.charCodeAt(i++) & 0xff;
		if(c4 == 61)
		return out;
		c4 = base64DecodeChars[c4];
	} while(i < len && c4 == -1);
	if(c4 == -1)
		break;
	out += String.fromCharCode(((c3 & 0x03) << 6) | c4);
	}
	return out;
}

/* utf.js - UTF-8 <=> UTF-16 convertion
 *
 * Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>
 * Version: 1.0
 * LastModified: Dec 25 1999
 * This library is free.  You can redistribute it and/or modify it.
 */

/*
 * Interfaces:
 * utf8 = utf16to8(utf16);
 * utf16 = utf16to8(utf8);
 */

function utf8to16(str) {
	var out, i, len, c;
	var char2, char3;

	out = "";
	len = str.length;
	i = 0;
	while(i < len) {
		c = str.charCodeAt(i++);
		switch(c >> 4)
		{
		  case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
			// 0xxxxxxx
			out += str.charAt(i-1);
			break;
		  case 12: case 13:
			// 110x xxxx   10xx xxxx
			char2 = str.charCodeAt(i++);
			out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
			break;
		  case 14:
			// 1110 xxxx  10xx xxxx  10xx xxxx
			char2 = str.charCodeAt(i++);
			char3 = str.charCodeAt(i++);
			out += String.fromCharCode(((c & 0x0F) << 12) |
						   ((char2 & 0x3F) << 6) |
						   ((char3 & 0x3F) << 0));
			break;
		}
	}

	return out;
}

////////////////////////////////////////

function gmail_get_original( gmail_get_original_raw, base64decode, utf8to16, mail_parse, http_get ) {
	return function( ik, msg, callback ) {
		gmail_get_original_raw( ik, msg,
			function( mail_data ) {
				//エンコードを設定して改めて取得
				//もしbase64エンコードされてたらデコード
				if( "base64" == mail_data[0]["content-transfer-encoding"] ) {
					//utf-8なら何とか変換できるのでする。
					mail_data[1] = base64decode(mail_data[1].replace(/\n/mg,""));
					if( mail_data[0]["content-type"].match(/"([utf\-8]*)"/) )
						mail_data[1] = utf8to16(mail_data[1]);
					//メールデータのCRLFをCRに。
					mail_data[1] = mail_data[1].replace(/\r\n/mg, "\n")
					callback( mail_data )
				} else {
					var url = location.protocol + "//" + location.host + "/mail/?ui=2&ik=" + ik + "&view=om&th=" + msg
					http_get( url,
						function( result ) {
							var mail_data = mail_parse( result )
							//メールデータのCRLFをCRに。
							mail_data[1] = mail_data[1].replace(/\r\n/mg, "\n")
							callback( mail_data )
						},
						{"overrideMimeType":mail_data[0]["content-type"]}
					)
				}
			}
		)
	}
}
gmail_get_original = gmail_get_original( gmail_get_original_raw, base64decode, utf8to16, mail_parse, http_get )

////////////////////////////////////////

//要素全てをfuncで処理して返す
function array_map( array_reduce ) {
	return function( array, func ) {
		return array_reduce(array, [], function(results, item){ results.push(func( item )); return results } )
	}
}
array_map = array_map( array_reduce )

////////////////////////////////////////

//第ニ引数で指定されたメールスレッド内の全てのメールのIDを返す。
function gmail_get_ids_of_mail_thread( http_get, array_map ) {
	return function( ik, current_mail_thread_id, callback ) {
		http_get("https://mail.google.com/mail/?ui=2&ik=" + ik + "&view=cv&th=" + current_mail_thread_id + "&rt=h&search=inbox",
			function(result){
				callback( array_map( result.match( /D\(\["ms","[^"]+"/gmi ), function( match ){ return match.match(/D\(\["ms","([^"]+)"/ )[1] } ) )
			}
		)
	}
}
gmail_get_ids_of_mail_thread = gmail_get_ids_of_mail_thread( http_get, array_map )

////////////////////////////////////////

//現在表示されているメールスレッドの先頭のメールのIDを取得する
function gmail_get_current_mail_thread_id() {
	//基本的にURLの末尾がメールスレッドの先頭のメールID
	var th = window.parent.location.href.split("/")
	th = th[th.length-1]
	//たまにURLがルートのままのときがある。
	if( !th.match(/[0-9a-z]+/i) ) {
		if( confirm( "can't get mail thread id. reload?" ) )
			window.parent.location.href = location.protocol + "//mail.google.com"
	}
	return th
}

////////////////////////////////////////

function gmail_check_thread_id( gmail ) {
	//ビューが切り替わったのにURLが変わってないときには正しくスレッドIDが取れないのでリロードを促す。
	var state = function(){}
	state = function() {
		//まず、最初のURLとビュータイプを保存。
		var view = gmail.getActiveViewType()
		var url = window.parent.location.href
		state = function(){
			//次に、ビュータイプが変わった時にURLが同一ならおかしい。
			if( view != gmail.getActiveViewType() && url == window.parent.location.href ) {
				if( confirm( "can't get thread id. reload?" ) )
					window.parent.location.href = location.protocol + "//mail.google.com"
			}
			else
				//変わってたらチェック終了
				state = function(){}
		}
	}
	gmail.registerViewChangeCallback(
		function () {
			state()
		}
	);
}
////////////////////////////////////////

//funcがtrueを返すもののみ返す
function array_filter( array_reduce ) {
	return function( array, func ) {
		return array_reduce(array, [], function(results, item){ if( func( item ) ) results.push(item); return results } )
	}
}
array_filter = array_filter( array_reduce )

////////////////////////////////////////

//開かれている各メールの先頭にある"show details"という要素を全て取得する
function gmail_get_elements_of_show_details( array_filter ) {
	return function( gmail ) {
		var spans = gmail.getActiveViewElement().getElementsByTagName("SPAN");
		var details = array_filter(spans, function(span){return("show details" == span.innerHTML)})
		return details
	}
}
gmail_get_elements_of_show_details = gmail_get_elements_of_show_details( array_filter )

////////////////////////////////////////

function event_dispatch( target, event_type, type ) {
	var event = document.createEvent(event_type);
	event.initMouseEvent(type, true, true, null, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
	target.dispatchEvent(event)
}

////////////////////////////////////////

function event_dispatch_click( event_dispatch ) {
	return function( target ) {
		event_dispatch(target, 'MouseEvents', "click")
	}
}
event_dispatch_click = event_dispatch_click( event_dispatch )

////////////////////////////////////////

//全てのメールを開く。第二引数にはこのメールスレッドにいくつのメールがあるかを渡す。開き終わったらコールバック関数に各メールの"show details"という要素のリストが渡される。
function gmail_expand_all( array_find, gmail_get_elements_of_show_details, event_dispatch_click ) {
	return function( gmail, thread_mail_count, callback ) {
		var expand_all = array_find( gmail.getActiveViewElement().getElementsByTagName("U"), function( element ){ return ( "Expand all" == element.innerHTML ) } )
		if( expand_all ) {
			//Expand allをクリック
			event_dispatch_click( expand_all );
			//全てのメールが開き終わるまで待つ
			(function wait_expand_all( callback ) {
				setTimeout(
					function(){
						var details = gmail_get_elements_of_show_details( gmail )
						if( thread_mail_count <= details.length )
							callback( details )
						else
							wait_expand_all( callback )
					},
					250
				)
			})( callback )
		} else
			callback( gmail_get_elements_of_show_details( gmail ) )
	}
}
gmail_expand_all = gmail_expand_all( array_find, gmail_get_elements_of_show_details, event_dispatch_click )

////////////////////////////////////////

//全ての要素が条件を満たすか？
function array_is_every( array_each_result ) {
	return function ( array, func ) {
		return array_each_result( array, true, function( result, item ){ if( !func(item) ) return [false, true]; return [true] } )
	}
}
array_is_every = array_is_every( array_each_result )

////////////////////////////////////////

//一つでも条件を満たす要素があるか？
function array_is_some( array_each_result ) {
	return function ( array, func ) {
		return array_each_result( array, false, function( result, item ){ if( func(item) ) return [true, true]; return [false] } )
	}
}
array_is_some = array_is_some( array_each_result )

////////////////////////////////////////

//valueは配列の中に存在するか？
function array_is_contain( array_is_some ) {
	return function ( array, value ) {
		return array_is_some( array, function( item ){ return (item == value) } )
	}
}
array_is_contain = array_is_contain( array_is_some )

////////////////////////////////////////

//valueが配列になければ入れる。
//文字列や数値ならset_insertのほうがいい。
function array_push_uniq( array_is_some ) {
	return function ( array, value ) {
		if( !array_is_contain( array, value ) )
			array.push( value )
	}
}
array_push_uniq = array_push_uniq( array_is_contain )

////////////////////////////////////////

function array_uniq( array_reduce, array_is_contain ) {
	return function( array ) {
		return array_reduce( array, [], function( results, item ){ if( !array_is_contain( results, item ) ) results.push( item ); return results } )
	}
}
array_uniq = array_uniq( array_reduce, array_is_contain )

////////////////////////////////////////

function dom_is_ancestor( child, ancestor ) {
	while( child = child.parentNode, child && (child != ancestor) );
	return (child == ancestor)
}

////////////////////////////////////////

function gmail_get_elements_of_body( array_map, array_uniq, array_filter, array_is_every, dom_is_ancestor ) {
	return function( gmail ){
		//まず、全てのBRの親を列挙
		var br_parents = array_map( gmail.getActiveViewElement().getElementsByTagName("BR"), function( br ){ return br.parentNode} )
		//次にuniq
		br_parents = array_uniq( br_parents )
		//次に、要素の中で、他の要素を祖先に持つものがあれば削除
		//各要素の内、他の要素全てを祖先に持たないもののみ残す
		br_parents = array_filter( br_parents, function( child ){ return array_is_every( br_parents, function( parent ){ return !dom_is_ancestor( child, parent ) } ) } )
		//さらにdivだけ残す。
		return array_filter( br_parents, function( item ){ return ( "DIV" == item.tagName ) } )
	}
}
gmail_get_elements_of_body = gmail_get_elements_of_body( array_map, array_uniq, array_filter, array_is_every, dom_is_ancestor )

////////////////////////////////////////////////////////////////////////////////

gmail_ready(
	function( gmail ) {
		if( gmail ) {
			gmail_check_thread_id( gmail )
			gmail_navigation_bar_add_link( gmail, "SpaceCharsFix",
				function( event, done_func ) {
					//リンクがクリックされたら、このスレッドのメールのIDを全て取り、次に全てのメールを展開し、本文とオリジナルのメールのbodyをpreでくくったものと置き換える。

					//セッションIDを取得
					gmail_get_ik(
						function( ik ){
							//IDを取る。
							gmail_get_ids_of_mail_thread( ik, gmail_get_current_mail_thread_id(),
								function( ids ){
									//全てのメールを展開
									gmail_expand_all( gmail, ids.length,
										function( details ){
											//本文の要素を取得
											var bodies = gmail_get_elements_of_body( gmail )
											//メールごとに処理
											var done_count = 0;
											array_each( ids.length,
												function( index ) {
													//オリジナルのデータを取得
													gmail_get_original( ik, ids[index],
														function( mail_data ) {
															//本文要素の中身を削除
															dom_remove_all_children( bodies[index] )
															//preでくくったオリジナルデータを挿入
															bodies[index].appendChild( dom_create("pre",{},[mail_data[1]]) )
															//全部終わったら終了関数を呼ぶ
															if( ids.length <= ++done_count )
																done_func()
														}
													)
												}
											)
										}
									)
								}
							)
						}
					)
				}
			)
		}
	}
)

////////////////////////////////////////////////////////////////////////////////
})()
