? beagled/ThunderbirdQueryable
Index: ./Util/Mork.cs
===================================================================
RCS file: ./Util/Mork.cs
diff -N ./Util/Mork.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./Util/Mork.cs	23 Jun 2006 07:34:37 -0000
@@ -0,0 +1,349 @@
+//
+// Mork.cs: A parser for mork files (used by software such as Firefox and Thunderbird)
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Text;
+using System.Collections;
+using System.Text.RegularExpressions;
+
+namespace Beagle.Util
+{
+	public class MorkDatabase : IEnumerable {
+		protected string mork_file;
+		protected string enum_namespace;
+		
+		protected Hashtable dicts;
+		protected Hashtable metadicts;
+		protected Hashtable rows;
+		protected Hashtable tables;
+		
+		protected string regex_dict = @"(?<id>\w+)\s*=(?<value>(?>[^()]+|\((?<n>)|\) (?<-n>))*)\)(\(|>)";
+		protected string regex_row = @"(?<action>[-!+]?)\[(-|)(?<roid>[0-9A-Za-z:\^]+)(?<cells>(?>[^\[\]]+)?)\]";
+		protected string regex_cell = @"\^(?<key>[0-9A-Fa-f]+)(\^(?<pvalue>[0-9A-Fa-f]+)|=(?<value>[0-9A-Fa-f]+))";
+		protected string regex_table = @"{.*?:(?<ns>[0-9A-Fa-f\^]+) {\(k\^(?<tbl>[0-9A-Fa-f]+):c\)";
+
+		public MorkDatabase (string mork_file)
+		{
+			this.mork_file = mork_file;
+			this.dicts = new Hashtable ();
+			this.metadicts = new Hashtable ();
+			this.rows = new Hashtable ();
+			this.tables = new Hashtable ();
+		}
+		
+		public void Read ()
+		{
+			string content;
+			StreamReader reader;
+			
+			reader = new StreamReader (mork_file);
+			content  = reader.ReadToEnd ();
+			reader.Close ();
+			
+			Reset ();
+			Read (content);
+		}
+
+		protected void Read (string content)
+		{
+			int position = -1;
+
+			while (++position != content.Length) {
+				
+				if (content [position].Equals ('/') && content [position].Equals ('/'))
+					// Ignore comments
+					position = content.IndexOf ('\n');
+				else if (content [position].Equals ('<') && content [position+2].Equals ('<'))
+					// Parse metadict information
+					ParseDict (Read (content, ref position, "<(", ")>"), true);
+				else if (content [position].Equals ('<'))
+					// Parse dict information
+					ParseDict (Read (content, ref position, "<(", ")>"), false);
+				else if (content [position].Equals ('{')) {
+					// Parse table information
+					ParseTable (Read (content, ref position, "{", "}"));
+				 }else if (content [position].Equals ('[')) 
+					// Parse rows
+					ParseRows (Read (content, ref position, "[", "]"), null, null);
+				else if (content [position].Equals ('@') && content [position+1].Equals ('$'))
+					// Parse groups
+					ParseGroups (Read (content, ref position, "@$${", "@$$}"));
+			}
+		}
+		
+		protected string Read (string content, ref int position, string start, string end)
+		{
+			int tmp = position, start_position = position;
+			
+			do {
+				position = content.IndexOf (end, position+1);
+				if ((tmp = content.IndexOf (start, tmp+1)) < 0)
+					break;
+			} while (tmp < position);
+			
+			return content.Substring (start_position, position-start_position+1);
+		}
+		
+		protected virtual void ParseDict (string dict, bool metadict)
+		{
+			Regex reg = new Regex (@"(?<id>[0-9A-Fa-f]+)\s*=(?<value>(.*))", RegexOptions.Compiled);
+			
+			// This is sooo lame that, but it's an easy solution that works
+			foreach (string t in Regex.Replace (dict.Substring (2, dict.Length-3).Replace ("\n", ""), @"\)\s*\(", "\n").Split ('\n')) {
+				
+				Match m = reg.Match (t);
+				
+				if (metadict) 
+					metadicts [m.Result ("${id}")] = m.Result ("${value}");
+				else
+					dicts [m.Result ("${id}")] = Decode (m.Result ("${value}"), "iso-8859-1");;
+			}
+			
+		}
+		
+		protected virtual void ParseTable (string table)
+		{
+			int start = table.IndexOf ('}')+1;
+			Match m = new Regex (regex_table, RegexOptions.Compiled).Match (table);
+			
+			ParseRows (table.Substring (start, table.Length-start-1), m.Result ("${ns}"), m.Result ("${tbl}"));
+		}
+		
+		protected virtual void ParseRows (string rows, string ns, string table)
+		{
+			Regex reg = new Regex (regex_row, RegexOptions.Compiled);
+			
+			foreach (Match m in reg.Matches (Clean (rows))) {
+				// tmp [0] == id, tmp [1] == ns
+				string[] tmp = m.Result ("${roid}").Split (':');
+				
+				if (m.Result ("${action}") == "-" || m.Result ("${cells}") == string.Empty)
+					RemoveRow (tmp [0], (tmp.Length > 1 ? tmp [1] : ns));
+				else
+					AddRow (tmp [0], (tmp.Length > 1 ? tmp [1] : ns), table, m.Result ("${cells}"));
+			}
+		}
+		
+		protected virtual void ParseGroups (string groups)
+		{
+			int start = groups.IndexOf ("{@")+2;
+			
+			Read (groups.Substring (start, groups.Length-start-1));
+		}
+		
+		protected string Clean (string str)
+		{
+			return str.Replace ("\n", "").Replace (" ", "");
+		}
+		
+		public string ParseNamespace (string ns)
+		{
+			if (ns.StartsWith ("^"))
+				return ns;
+			else {
+				foreach (string key in metadicts.Keys)
+					if ((metadicts [key] as string) == ns)
+						return String.Format ("^{0}", key);
+			}
+			
+			return ns;
+		}
+		
+		public void AddRow (string id, string ns, string table, string cols)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			if (id == string.Empty || ns2 == string.Empty || cols == string.Empty)
+				return;
+			else if (!rows.ContainsKey (ns2))
+				rows [ns2] = new Hashtable ();
+			
+			// Add row with a twist... (aka hack)
+			if (!Exists (id, ns2))
+				(rows [ns2] as Hashtable).Add (id, cols);
+			else if (cols.Length >= GetCells (id, ns2).Length)
+				(rows [ns2] as Hashtable) [id] = cols;
+			else
+				(rows [ns2] as Hashtable) [id] = String.Format ("{0}{1}", (rows [ns2] as Hashtable) [id], cols);
+			
+			// Save table association
+			if (!tables.ContainsKey (id))
+				tables [id] = table;
+		}
+		
+		public void RemoveRow (string id, string ns)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			if (!rows.ContainsKey (ns2))
+				return;
+			
+			(rows [ns2] as Hashtable).Remove (id);
+			tables.Remove (id);
+		}
+		
+		public string GetCells (string id, string ns)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			return (ns2 != null ?(rows [ns2] as Hashtable) [id] as string : null);
+		}
+		
+		public Hashtable Compile (string id, string ns)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			if (!Exists (id, ns2))
+				return null;
+			
+			Hashtable tbl = new Hashtable ();
+			Regex reg = new Regex (regex_cell, RegexOptions.Compiled);
+			
+			foreach (Match m in reg.Matches (GetCells (id, ns2))) {
+				tbl [metadicts [m.Result ("${key}")]] = (m.Result ("${pvalue}") != string.Empty ? 
+					dicts [m.Result("${pvalue}")] : m.Result ("${value}"));
+			}
+			
+			tbl ["id"] = id;
+			tbl ["table"] = tables [id];
+			
+			return tbl;
+		}
+
+		public bool Exists (string id, string ns)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			return (ns2 != null ? (rows [ns] as Hashtable).ContainsKey (id) : false);
+		}
+		
+		public int GetRowCount (string ns)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			if (ns2 == null || rows [ns2] == null)
+				return -1;
+			
+			return (rows [ns2] as Hashtable).Count;
+		}
+		
+		public int GetRowCount (string ns, string table)
+		{
+			int count = 0;
+			string ns2 = ParseNamespace (ns);
+			
+			if (ns2 == null || rows [ns2] == null)
+				return -1;
+			
+			foreach (string id in (rows [ns2] as Hashtable).Keys) {
+				if ((string) tables [id] == table)
+					count++;
+			}
+			
+			return count;
+		}
+		
+		public IEnumerator GetEnumerator ()
+		{
+			string ns = ParseNamespace (EnumNamespace);
+			
+			if (ns == null || (rows [ns] as Hashtable) == null || Empty)
+				return null;
+			
+			return (rows [ns] as Hashtable).Keys.GetEnumerator ();
+		}
+		
+		public void Reset ()
+		{
+			dicts.Clear ();
+			metadicts.Clear ();
+			rows.Clear ();
+			tables.Clear ();
+		}
+		
+		// A _very_ hackish method to convert an UTF-8-morkish-encoded string to another encoding
+		// (see Encoding-property)
+		public static string Decode (string str, string encoding)
+		{
+			int start = 0, old;
+			string decoded = string.Empty;
+			
+			if (str == string.Empty || encoding == string.Empty || str == null || encoding == null)
+				return str;
+			
+			foreach (Match m in Regex.Matches (str, @"\$(?<1>[0-9A-Fa-f]{2})\$(?<2>[0-9A-Fa-f]{2})")) {
+				old = start;
+				start = m.Index+6;
+				
+				byte[] b = new byte [] { 
+					Convert.ToByte (Thunderbird.Hex2Dec (m.Result ("${1}"))), 
+					Convert.ToByte (Thunderbird.Hex2Dec (m.Result ("${2}"))) };
+				
+				byte[] tmp = System.Text.Encoding.Convert (
+					System.Text.Encoding.UTF8, 
+					System.Text.Encoding.GetEncoding (encoding), b);
+				
+				decoded = String.Concat (decoded, 
+					str.Substring (old, start-old-6), 
+					System.Text.Encoding.GetEncoding (encoding).GetString (tmp));
+			}
+			
+			if (start == 0 && decoded.Length == 0)
+				return str;
+			else if (start != decoded.Length)
+				decoded = String.Concat (decoded, str.Substring (start, str.Length-start));
+			
+			return decoded;
+		}
+		
+		public int Rows {
+			get {
+				int count = 0;
+				
+				foreach (Hashtable r in rows.Values)
+					count += r.Count;
+			
+				return count;
+			}
+		}
+		
+		public string EnumNamespace {
+			get { return enum_namespace; }
+			set { enum_namespace = value; }
+		}
+		
+		public string Filename {
+			get { return mork_file; }
+		}
+
+		// There will always exist an item with id 1 in namespace 80, which means
+		// that when there are less than two items in the database, it's empty
+		public bool Empty {
+			get { return (rows.Count > 1 ? false : true); }
+		}
+	}
+}
Index: ./Util/Makefile.am
===================================================================
RCS file: /cvs/gnome/beagle/Util/Makefile.am,v
retrieving revision 1.94
diff -u -1 -2 -r1.94 Makefile.am
--- ./Util/Makefile.am	25 Apr 2006 22:56:05 -0000	1.94
+++ ./Util/Makefile.am	23 Jun 2006 07:34:37 -0000
@@ -105,24 +105,29 @@
 	$(srcdir)/SemWeb/RSquaryFilters.cs		\
 	$(srcdir)/SemWeb/Sparql.cs			\
 	$(srcdir)/SemWeb/SQLStore.cs			\
 	$(srcdir)/SemWeb/Statement.cs			\
 	$(srcdir)/SemWeb/Store.cs			\
 	$(srcdir)/SemWeb/UriMap.cs			\
 	$(srcdir)/SemWeb/Util.cs			\
 	$(srcdir)/SemWeb/XmlParser.cs			\
 	$(srcdir)/SemWeb/XmlWriter.cs			\
 	$(srcdir)/SemWeb/XPathSemWebNavigator.cs	\
 	$(srcdir)/PropertyKeywordAttribute.cs
 
+if ENABLE_THUNDERBIRD
+UTIL_CSFILES +=				\
+	$(srcdir)/Mork.cs		\
+	$(srcdir)/Thunderbird.cs
+endif
 
 if HAS_LIBCHM 
 UTIL_CSFILES +=					\
 	$(srcdir)/ChmFile.cs
 endif
 
 UTIL_ASSEMBLIES =				\
 	$(BEAGLED_LIBS)				\
 	-r:System.Web.Services			\
 	-r:System.Data				\
 	-r:Mono.Data.SqliteClient		\
 	-r:Mono.Posix				\
Index: ./Util/Thunderbird.cs
===================================================================
RCS file: ./Util/Thunderbird.cs
diff -N ./Util/Thunderbird.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./Util/Thunderbird.cs	23 Jun 2006 07:34:37 -0000
@@ -0,0 +1,836 @@
+//
+// Thunderbird.cs: A utility class with methods and classes that might be needed to parse Thunderbird data
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Text;
+using System.Collections;
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+using Beagle;
+using Beagle.Util;
+
+using GMime;
+
+namespace Beagle.Util {
+
+	public class Thunderbird {
+	
+		public static bool Debug = false;
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+	
+		public enum AccountType {
+			Pop3,
+			Imap,
+			Rss,
+			Nntp,
+			AddressBook,
+			MoveMail,
+			Invalid
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		public class Account {
+			private string server_string = null;
+			private string path = null;
+			private int server_port = -1;
+			private AccountType account_type;
+			private char delimiter;
+			
+			public Account (string server, string path, int port, AccountType type, char delim)
+			{
+				this.server_string = server;
+				this.path = path;
+				this.server_port = port;
+				this.account_type = type;
+				this.delimiter = delim;
+			}
+
+			public string Server {
+				get { return server_string; }
+			}
+			
+			public string Path {
+				get { return path; }
+			}
+			
+			public int Port {
+				get { return (server_port > 0 ? server_port : Thunderbird.ParsePort (Type)); }
+			}
+			
+			public AccountType Type {
+				get { return account_type; }
+			}
+			
+			public char Delimiter {
+				get { return (delimiter ==  char.MinValue ? '/' : delimiter); }
+			}
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		public class StorageBase {
+			protected Hashtable data;
+			protected System.Uri uri;
+			protected Account account;
+			
+			public StorageBase ()
+			{
+				data = new Hashtable ();
+			}
+			
+			public string GetString (string key)
+			{
+				return Convert.ToString (data [key]);
+			}
+			
+			public int GetInt (string key)
+			{
+				try {
+					if (!data.ContainsKey (key))
+						return -1;
+					
+					return Convert.ToInt32 (data [key]);
+				} catch (Exception e) {
+					return -1;
+				}
+			}
+			
+			public bool GetBool (string key)
+			{
+				try {
+					return Convert.ToBoolean (data [key]);
+				} catch (Exception e) {
+					return false;
+				}
+			}
+			
+			public object GetObject (string key)
+			{
+				return data [key];
+			}
+			
+			public void SetObject (string key, object value)
+			{
+				data [key] = value;
+			}
+			
+			public System.Uri Uri {
+				get { return uri; }
+				set { uri = value; }
+			}
+			
+			public Account Account {
+				get { return account; }
+			}
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		// String types:
+		// id, sender, subject, recipients, date, mailbox
+		// Integer types:
+		// size, msgOffset, offlineMsgSize
+		// Bool types:
+		// FullIndex
+		public class Mail : StorageBase {
+			private string workfile;
+		
+			public Mail (Account account, Hashtable data, string workfile)
+			{
+				foreach (string key in data.Keys) {
+					if (key == "id")
+						SetObject (key, data [key]);
+					else if (key == "sender")
+						SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
+					else if (key == "subject")
+						SetObject (key, Utils.HeaderDecodeText ((string) data [key]));
+					else if (key == "recipients")
+						SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
+					else if (key == "date")
+						SetObject (key, Thunderbird.HexDateToString ((string) data [key]));
+					else if (key == "size")
+						SetObject (key, Thunderbird.Hex2Dec ((string) data [key]));
+					else if (key == "msgOffset")
+						SetObject (key, Thunderbird.Hex2Dec ((string) data[key]));
+					else if (key == "offlineMsgSize")
+						SetObject (key, Thunderbird.Hex2Dec ((string) data [key]));
+					else if (key == "message-id")
+						SetObject (key, (string) data [key]);
+					else if (key == "references")
+						SetObject (key, (data [key] as string).Replace ("\\", ""));
+				}
+				
+				this.account = account;
+				this.workfile = workfile;
+				SetObject ("mailbox", Thunderbird.ConstructMailboxString (workfile, account));
+				this.uri = Thunderbird.NewUri (Account, GetString ("mailbox"), GetString ("id"));
+			}
+			
+			private GMime.Message ConstructMessage ()
+			{
+				GMime.Message message = null;
+				
+				// Try to fully index this mail by loading the entire mail into memory
+				if (GetBool ("FullIndex"))
+					message = FullMessage ();
+				
+				// Make sure we have the correct status set on this message, in case something went wrong
+				if (message == null || (message != null && message.Stream.Length <= 1)) {
+					SetObject ("FullIndex", (object) false);
+					return PartialMessage ();
+				} else 
+					return message;
+			}
+			
+			private GMime.Message PartialMessage ()
+			{
+				GMime.Message message = new GMime.Message (true);
+
+				message.Subject = GetString ("subject");
+				message.Sender = GetString ("sender");
+				message.MessageId = GetString ("message-id");
+				message.SetDate (DateTime.Parse (GetString ("date")), 0);
+				
+				// Add references
+				if (data.ContainsKey ("references")) {
+					foreach (Match m in Regex.Matches ((data ["references"] as string), @"\<(?>[^\<\>]+)\>"))
+						message.AddHeader ("References", m.Value);
+				}
+				
+				return message;
+			}
+
+			private GMime.Message FullMessage ()
+			{
+				int fd;
+				string file = Thunderbird.GetFullyIndexableFile (workfile);
+				GMime.Message message = null;
+
+				// gmime will go nuts and make the daemon "segmentation fault" in case the file doesn't exist!
+				if (!File.Exists (file))
+					return message;
+
+				try {
+					fd = Mono.Unix.Native.Syscall.open (file, Mono.Unix.Native.OpenFlags.O_RDONLY);
+					StreamFs stream = new StreamFs (fd, Offset, Offset + Size);
+					Parser parser = new Parser (stream);
+					message = parser.ConstructMessage ();
+				
+					stream.Dispose ();
+					parser.Dispose ();
+				} catch (Exception e) {}
+
+				return message;
+			}
+			
+			public int Offset {
+				get {
+					int msg_offset = GetInt ("msgOffset"); 
+					return (msg_offset >= 0 ? msg_offset : Thunderbird.Hex2Dec (GetString ("id"))); 
+				}
+			}
+			
+			public int Size {
+				get {
+					int msg_offline_size = GetInt ("offlineMsgSize");
+					return (msg_offline_size >= 0 ? msg_offline_size : GetInt ("size")); 
+				}
+			}
+
+			public GMime.Message Message {
+				get { return ConstructMessage (); }
+			}
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		// String types:
+		// id, FirstName, LastName, DisplayName, NickName, PrimaryEmail, SecondEmail,
+		// WorkPhone, FaxNumber, HomePhone, PagerNumber, CellularNumber, HomeAddress,
+		// HomeAddress2, HomeCity, HomeState, HomeZipCode,  HomeCountry, WorkAddress,
+		// WorkAddress2, WorkCity, WorkState, WorkZipCode, WorkCountry, JobTitle, Department,
+		// Company, _AimScreenName, FamilyName, WebPage1, WebPage2, BirthYear, BirthMonth
+		// , BirthDay, Custom1, Custom2
+		// Integer types:
+		// None
+		public class Contact : StorageBase {
+			private string workfile;
+		
+			public Contact (Account account, Hashtable data, string workfile)
+			{
+				this.account = account;
+				this.data = data;
+				this.workfile = workfile;
+				this.uri = NewUri (account, GetString ("PrimaryEmail"), GetString ("id"));
+			}
+			
+			public string Workfile {
+				get { return workfile; }
+			}
+		
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		// String types:
+		// id, subject, sender, date, message-id
+		// Integer types:
+		// size
+		public class RssFeed : StorageBase {
+			private string workfile;
+		
+			public RssFeed (Account account, Hashtable data, string workfile)
+			{
+				foreach (string key in data.Keys) {
+					if (key == "id")
+						SetObject (key, data [key]);
+					else if (key == "subject") // title
+						SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
+					else if (key == "sender") // publisher
+						SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
+					else if (key == "date") // date
+						SetObject (key, HexDateToString ((string) data [key]));
+					else if (key == "size") // size
+						SetObject (key, Hex2Dec ((string) data [key]));
+					else if (key == "message-id") { // links
+						string tmp = (string) data [key];
+						SetObject (key, Utils.HeaderDecodePhrase (tmp.Substring (0, tmp.LastIndexOf ("@"))));
+					}
+				}
+				
+				this.account = account;
+				this.workfile = workfile;
+				this.uri = NewUri (account, ConstructMailboxString (workfile, account), GetString ("id"));
+			}
+			
+			// FIXME: Make this a lot faster!
+			private StringReader ConstructContent ()
+			{
+				string content = null;
+				string file = GetFullyIndexableFile (workfile);
+				
+				if (!File.Exists (file))
+					return null;
+				
+				try {
+					StreamReader reader = new StreamReader (file);
+					
+					char[] tmp = new char [GetInt ("size")];
+					reader.BaseStream.Seek (Hex2Dec (GetString ("id")), SeekOrigin.Begin);
+					reader.Read (tmp, 0, tmp.Length);
+					
+					// We don't want to index all HTTP headers, so we cut 'em off
+					content = new string (tmp);
+					content = content.Substring (content.IndexOf ("<html>"));
+					
+					reader.Close ();
+				} catch (Exception e) { }
+				
+				return (content != null ? new StringReader (content) : null);
+			}
+			
+			public string Workfile {
+				get { return workfile; }
+			}
+			
+			public StringReader Content {
+				get { return ConstructContent (); }
+			}
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		// String types:
+		// id, subject, sender, date
+		// Integer types:
+		// size
+		// An NNTP message resambles a mail so very much...
+		public class NntpMessage : Mail {
+		
+			public NntpMessage (Account account, Hashtable data, string workfile)
+				: base (account, data, workfile)
+			{
+				foreach (string key in data.Keys) {
+					if (key == "id")
+						SetObject (key,  data [key]);
+					else if (key == "subject")
+						SetObject (key, Utils.HeaderDecodeText ((string) data [key]));
+					else if (key == "sender")
+						SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
+					else if (key == "date")
+						SetObject (key, Thunderbird.HexDateToString ((string) data [key]));
+					else if (key == "size")
+						SetObject (key, Thunderbird.Hex2Dec ((string) data [key]));
+				}
+				
+				Uri = NewUri (account, ConstructMailboxString (workfile, account), GetString ("id"));
+			}
+		
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		// Still just a stub, will be fixed later on
+		public class MoveMail : StorageBase {
+		
+			public MoveMail (Account account, Hashtable data, string workfile)
+			{
+				this.account = account;
+				this.data = data;
+				//this.workfile = workfile;
+				this.uri = NewUri (account, GetString ("tmp"), GetString ("id"));
+			}
+		
+		}
+
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		public class Database : IEnumerable {
+			private static MorkDatabase db;
+			private Account account;
+			private string file;
+			
+			private IEnumerator current = null;
+
+			public Database (Account account, string file)
+			{
+				this.account = account;
+				this.file = file;
+			}
+			
+			public void Load ()
+			{
+				db = new MorkDatabase (file);
+				db.Read();
+				
+				switch (account.Type) {
+				case AccountType.Pop3:
+				case AccountType.Imap:
+				case AccountType.Rss:
+				case AccountType.Nntp:
+				case AccountType.MoveMail:
+					db.EnumNamespace = "ns:msg:db:row:scope:msgs:all";
+					break;
+				case AccountType.AddressBook:
+					db.EnumNamespace = "ns:addrbk:db:row:scope:card:all";
+					break;
+				}
+				
+				current = db.GetEnumerator ();
+			}
+			
+			public Account Account {
+				get { return account; }
+			}
+			
+			public int Count {
+				get {
+					if (db == null)
+						return 0;
+						
+					return (account.Type == AccountType.AddressBook ? 
+						db.GetRowCount ("ns:addrbk:db:row:scope:card:all", "BF") : 
+						db.GetRowCount ("ns:msg:db:row:scope:msgs:all"));
+				}
+			}
+			
+			public string Filename {
+				get { return (db != null ? db.Filename : string.Empty); }
+			}
+			
+			public MorkDatabase Db {
+				get { return db; }
+			}
+			
+			public IEnumerator GetEnumerator ()
+			{
+				return new DatabaseEnumerator (db, account, current);
+			}
+			
+			public class DatabaseEnumerator : IEnumerator {
+				private MorkDatabase db;
+				private Account account;
+				private IEnumerator enumerator;
+
+				public DatabaseEnumerator (MorkDatabase db, Account account, IEnumerator enumerator)
+				{
+					this.db = db;
+					this.enumerator = enumerator;
+					this.account = account;
+				}
+				
+				public bool MoveNext ()
+				{
+					return (enumerator != null ? enumerator.MoveNext () : false);
+				}
+				
+				public void Reset ()
+				{
+					enumerator.Reset ();
+				}
+				
+				public object Current {
+					get { 
+						switch (account.Type) {
+						case AccountType.Pop3:
+						case AccountType.Imap:
+							return new Mail (account, db.Compile ((string) enumerator.Current, 
+								"ns:msg:db:row:scope:msgs:all"), db.Filename); 
+						case AccountType.AddressBook:
+							return new Contact (account, db.Compile ((string) enumerator.Current,
+								"ns:addrbk:db:row:scope:card:all"), db.Filename);
+						case AccountType.Rss:
+							return new RssFeed (account, db.Compile ((string) enumerator.Current, 
+								"ns:msg:db:row:scope:msgs:all"), db.Filename);
+						case AccountType.Nntp:
+							return new NntpMessage (account, db.Compile ((string) enumerator.Current, 
+								"ns:msg:db:row:scope:msgs:all"), db.Filename);
+						case AccountType.MoveMail:
+							return new MoveMail (account, db.Compile ((string) enumerator.Current,
+								"ns:msg:db:row:scope:msgs:all"), db.Filename);
+						}
+						
+						return null;
+					}
+				}
+			}
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+
+		public static string HexDateToString (string hex)
+		{
+			DateTime time = new DateTime (1970,1,1,0,0,0);
+
+			try {
+				time = time.AddSeconds (
+					Int32.Parse (hex, NumberStyles.HexNumber));
+			} catch (Exception e) { }
+			
+			return time.ToString ();
+		}
+		
+		public static int Hex2Dec (string hex)
+		{
+			int dec = -1;
+			
+			try {
+				dec = Convert.ToInt32 (hex, 16);
+			} catch (Exception e) { }
+			
+			return dec;
+		}
+		
+		public static int ParsePort (AccountType type)
+		{
+			int port = 0;
+			
+			switch (type) {
+			case AccountType.Pop3:
+				port = 110;
+				break;
+			case AccountType.Imap:
+				port = 143;
+				break;
+			}
+			
+			return port;
+		}
+		
+		public static AccountType ParseAccountType (string type_str)
+		{
+			AccountType type;
+
+			switch (type_str.ToLower ()) {
+			case "pop3":
+				type = AccountType.Pop3;
+				break;
+			case "imap":
+				type = AccountType.Imap;
+				break;
+			case "rss":
+				type = AccountType.Rss;
+				break;
+			case "nntp":
+				type= AccountType.Nntp;
+				break;
+			case "addressbook":
+				type = AccountType.AddressBook;
+				break;
+			case "movemail":
+				type = AccountType.MoveMail;
+				break;
+			default:
+				type = AccountType.Invalid;
+				break;
+			}
+			
+			return type;
+		}
+		
+		// A hack to extract a potential delimiter from a namespace-string
+		public static char GetDelimiter (string[] namespace_str)
+		{
+			MatchCollection matches = null;
+			Regex reg = new Regex (@"\\\""(.*)(?<delimiter>[^,])\\\""", RegexOptions.Compiled);
+			
+			if (namespace_str == null)
+				return char.MinValue;
+			
+			foreach (string str in namespace_str) {
+				try {
+					matches = reg.Matches (str);
+				} catch (Exception e) {
+					continue;
+				}
+
+				foreach (Match m in matches) {
+					char delim = Convert.ToChar (m.Result ("${delimiter}"));
+					if (delim != ' ')
+						return delim;
+				}
+			}
+			
+			return char.MinValue;
+		}
+		
+		public static Uri NewUri (Account account, string mailbox, string id)
+		{
+			Uri uri = null;
+
+			switch (account.Type) {
+				case AccountType.Pop3:
+				case AccountType.MoveMail:
+				case AccountType.Rss: // rss, movemail and pop3 share the same uri scheme
+					uri = new Uri (String.Format ("mailbox://{0}/{1}?number={2}", 
+						account.Path, mailbox, Convert.ToInt32 (id, 16))); 
+					break;
+				case AccountType.Imap:
+					uri = new Uri (String.Format ("imap://{0}:{1}/fetch%3EUID%3E{2}%3E{3}",
+						account.Server, account.Port, mailbox, Convert.ToInt32 (id, 16)));
+					break;
+				case AccountType.AddressBook:
+					uri = new Uri (String.Format ("mailto:{0}",mailbox));
+					break;
+				case AccountType.Nntp:
+					uri = new Uri (String.Format ( "news://{0}:{1}/{2}?number={3}" , 
+						new string[] {account.Server, account.Port.ToString(), mailbox, id} ));
+					break;
+				case AccountType.Invalid:
+					break;
+			}
+
+			return uri;
+		}
+		
+		public static  string GetFullyIndexableFile (string mork_file)
+		{
+			string mailbox_file = Path.Combine (
+				Path.GetDirectoryName (mork_file), 
+				Path.GetFileNameWithoutExtension (mork_file));
+			
+			return mailbox_file;
+		}
+		
+		// a generic way to determine where thunderbird is storing it's files
+		public static string GetRootPath ()
+		{
+			foreach (string dir in Directory.GetDirectories (PathFinder.HomeDir, ".*thunderbird*")) {
+				if (File.Exists (Path.Combine (dir, "profiles.ini")))
+					return dir;
+			}
+			
+			return null;
+		}
+		
+		public static string[] GetProfilePaths (string root)
+		{
+			string line;
+			StreamReader reader;
+			ArrayList profiles = new ArrayList ();
+			
+			try {
+				reader = new StreamReader (Path.Combine (root, "profiles.ini"));
+			} catch (Exception e) { 
+				return (string[]) profiles.ToArray ();
+			}
+			
+			// Read the profile path
+			while ((line = reader.ReadLine ()) != null) {
+				if (line.StartsWith ("Path=")) {
+					profiles.Add (String.Format ("{0}/{1}", root, line.Substring (5)));
+					continue;
+				}
+			}
+
+			return (string[]) profiles.ToArray (typeof (string));
+		}
+		
+		public static string GetRelativePath (string mork_file)
+		{
+			string path = null;
+			foreach (string root in Thunderbird.GetProfilePaths (Thunderbird.GetRootPath ())) {
+				if (!mork_file.StartsWith (root))
+					continue;
+
+				path = mork_file.Substring (root.Length+1);
+				break;
+			}
+			
+			return path;
+		}
+		
+		public static ArrayList ReadAccounts (string profile_dir)
+		{
+			string line = null;
+			Queue accounts = new Queue();
+			Hashtable tbl = new Hashtable ();
+			ArrayList account_list = new ArrayList ();
+			StreamReader reader;
+			Regex id_reg = new Regex (@"account.account(?<id>\d).server");
+			Regex reg = new Regex (@"user_pref\(""mail\.(?<key>.*)""\s*,\s*(""(?<value>.*)"" | (?<value>.*))\);",
+				RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
+
+			try {
+				reader = new StreamReader (Path.Combine (profile_dir, "prefs.js"));
+			} catch (Exception e) {
+				if (Debug)
+					Logger.Log.Debug ("Failed to open file {0}: {1}", Path.Combine (profile_dir , "prefs.js"), e.Message);
+
+				return account_list;
+			}
+
+			while ((line = reader.ReadLine()) != null) {
+				if (!line.StartsWith ("user_pref(\"mail."))
+					continue;
+
+				try {
+					string key = reg.Match (line).Result ("${key}");
+
+					if (key.StartsWith ("account.account")) {
+						if (Debug)
+							Logger.Log.Debug ("account.account: {0}", id_reg.Match (key).Result ("${id}"));
+
+						accounts.Enqueue (id_reg.Match (key).Result ("${id}"));
+					}
+
+					tbl [key] = reg.Match (line).Result ("${value}");
+				} catch (Exception e) { 
+					if (Debug)
+						Logger.Log.Debug ("ReadAccounts 1: {0}", e.Message);				
+				}
+			}
+			
+			if (Debug)
+				Logger.Log.Info ("ReadAccounts: {0} accounts", accounts.Count);
+
+			while (accounts.Count > 0) {
+				string id = "server.server" + (accounts.Dequeue() as string);
+				AccountType type = ParseAccountType ((string) tbl [id + ".type"]);
+				char delimiter = GetDelimiter (new string [] { (string) tbl [id + ".namespace.personal"], 
+					(string) tbl [id + ".namespace.public"], (string) tbl [id + ".namespace.other_users"]});
+				
+				if (type == AccountType.Invalid)
+					continue;
+				
+				if (Debug)
+					Logger.Log.Debug ("ReadAccounts 2: {0}", id);
+
+				try {
+					account_list.Add (new Account (
+						String.Format ("{0}@{1}", (string) tbl [id + ".userName"], (string) tbl [id + ".hostname"]), 
+						(string) tbl [id + ".directory"], Convert.ToInt32 ((string) tbl [id + ".port"]), type, delimiter));
+				} catch (Exception e) {
+					if (Debug)
+						Logger.Log.Debug ("ReadAccounts 3: {0}", e.Message);
+					continue;
+				}
+			}
+			
+			// In case the address book file exists, add it as well
+			if (File.Exists (Path.Combine (profile_dir, "abook.mab"))) {
+				account_list.Add (new Account (Path.GetFileName (profile_dir), 
+					Path.Combine (profile_dir, "abook.mab"), 0, AccountType.AddressBook, ' '));
+			}
+
+			return account_list;
+		}
+		
+		public static bool IsMorkFile (string path, string filename)
+		{
+			string full_path = Path.Combine (path, filename);
+			
+			if (Path.GetExtension (filename) == ".msf" && File.Exists (full_path))
+				return true;
+		
+			return false;
+		}
+		
+		public static bool IsFullyIndexable (string mork_file)
+		{
+			try {
+				FileInfo file_info = new FileInfo (GetFullyIndexableFile (mork_file));
+				if (file_info.Length > 0)
+					return true;
+			} catch (Exception e) {}
+			
+			return false;
+		}
+		
+		public static string ConstructMailboxString (string mork_file, Account account)
+		{
+			string mailbox = null;
+
+			switch (account.Type) {
+			case AccountType.Pop3:
+			case AccountType.Rss:
+			case AccountType.MoveMail:
+				mailbox = GetFullyIndexableFile (mork_file.Substring (account.Path.Length+1));
+				break;
+			case AccountType.Imap:
+				mailbox = mork_file.Substring (account.Path.Length+1);
+				mailbox = account.Delimiter + GetFullyIndexableFile (mailbox).Replace 
+					(".sbd/", Convert.ToString (account.Delimiter));
+				break;
+			case AccountType.AddressBook:
+			case AccountType.Nntp:
+				// Doesn't really matter what this is as long as it's unique (at least until I've figure the uri schemes)
+				mailbox = account.Server;
+				break;
+			case AccountType.Invalid:
+				mailbox = String.Format ("InvalidMailbox-{0}", mork_file);
+				break;
+			}
+
+			return mailbox;
+		}
+	}
+
+}
Index: ./beagled/ThunderbirdQueryable/Contact.cs
===================================================================
RCS file: ./beagled/ThunderbirdQueryable/Contact.cs
diff -N ./beagled/ThunderbirdQueryable/Contact.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./beagled/ThunderbirdQueryable/Contact.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,147 @@
+//
+// Contact.cs: Adds address book indexing support to the Thunderbird backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+using GMime;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {	
+	
+	[ThunderbirdIndexableGenerator (TB.AccountType.AddressBook, "Address book support", true)]
+	public class ContactIndexableGenerator : ThunderbirdIndexableGenerator {
+
+		public ContactIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string abook_file)
+			: base (indexer, account, abook_file)
+		{
+		}
+		
+		public override bool HasNextIndexable ()
+		{
+			do {
+				if (DbEnumerator == null || !DbEnumerator.MoveNext ()) {
+					Done = true;
+					indexer.NotificationEvent -= OnNotification;
+					indexer.ChildComplete ();
+					return false;
+				}
+			} while ((DbEnumerator.Current as TB.Contact).GetString ("table") != "BF" ||
+				IsUpToDate ((DbEnumerator.Current as TB.Contact).Uri));
+			
+			return true;
+		}
+		
+		public override Indexable GetNextIndexable ()
+		{
+			return ContactToIndexable (DbEnumerator.Current as TB.Contact);
+		}
+		
+		public override void LoadDatabase ()
+		{
+			try {
+				db = new TB.Database (account, DbFile);
+				db.Load ();
+			} catch (Exception e) {
+				Logger.Log.Debug ("Failed to load {0}: {1}", DbFile, e.Message);
+				return;
+			}
+			
+			if (db.Count <= 0)
+				return;
+			
+			ResetEnumerator ();
+			Logger.Log.Info ("Indexing address book containing {0} contact(s) ({1})", db.Count, RelativePath);
+		}
+
+		private Indexable ContactToIndexable (TB.Contact contact)
+		{
+			Indexable indexable = NewIndexable (contact.Uri, DateTime.Now.ToUniversalTime (), "Contact");
+			
+			indexable.AddProperty (Property.New ("fixme:FirstName", contact.GetString ("FirstName")));
+			indexable.AddProperty (Property.New ("fixme:LastName", contact.GetString ("LastName")));
+			indexable.AddProperty (Property.New ("fixme:DisplayName", contact.GetString ("LastName")));
+			indexable.AddProperty (Property.New ("fixme:NickName", contact.GetString ("NickName")));
+			indexable.AddProperty (Property.NewKeyword ("fixme:PrimaryEmail", contact.GetString ("PrimaryEmail")));
+			indexable.AddProperty (Property.NewKeyword ("fixme:SecondEmail", contact.GetString ("SecondEmail")));
+			indexable.AddProperty (Property.New ("fixme:WorkPhone", contact.GetString ("WorkPhone")));
+			indexable.AddProperty (Property.New ("fixme:FaxNumber", contact.GetString ("FaxNumber")));
+			indexable.AddProperty (Property.New ("fixme:HomePhone", contact.GetString ("HomePhone")));
+			indexable.AddProperty (Property.New ("fixme:PagerNumber", contact.GetString ("PagerNumber")));
+			indexable.AddProperty (Property.New ("fixme:CellularNumber", contact.GetString ("CellularNumber")));
+			indexable.AddProperty (Property.New ("fixme:HomeAddress", contact.GetString ("HomeAddress")));
+			indexable.AddProperty (Property.New ("fixme:HomeAddress2", contact.GetString ("HomeAddress2")));
+			indexable.AddProperty (Property.New ("fixme:HomeCity", contact.GetString ("HomeCity")));
+			indexable.AddProperty (Property.New ("fixme:HomeState", contact.GetString ("HomeState")));
+			indexable.AddProperty (Property.New ("fixme:HomeZipCode", contact.GetString("HomeZipCode")));
+			indexable.AddProperty (Property.New ("fixme:HomeCountry", contact.GetString ("HomeCountry")));
+			indexable.AddProperty (Property.New ("fixme:WorkAddress", contact.GetString ("WorkAddress")));
+			indexable.AddProperty (Property.New ("fixme:WorkAddress2", contact.GetString ("WorkAddress2")));
+			indexable.AddProperty (Property.New ("fixme:WorkCity", contact.GetString ("WorkCity")));
+			indexable.AddProperty (Property.New ("fixme:WorkState", contact.GetString ("WorkState")));
+			indexable.AddProperty (Property.New ("fixme:WorkZipCode", contact.GetString ("WorkZipCode")));
+			indexable.AddProperty (Property.New ("fixme:WorkCountry", contact.GetString ("WorkCountry")));
+			indexable.AddProperty (Property.New ("fixme:JobTitle", contact.GetString ("JobTitle")));
+			indexable.AddProperty (Property.New ("fixme:Department", contact.GetString ("Department")));
+			indexable.AddProperty (Property.New ("fixme:Company", contact.GetString ("Company")));
+			indexable.AddProperty (Property.New ("fixme:_AimScreenName", contact.GetString ("_AimScreenName")));
+			indexable.AddProperty (Property.New ("fixme:FamilyName", contact.GetString ("FamilyName")));
+			indexable.AddProperty (Property.NewKeyword ("fixme:WebPage1", contact.GetString ("WebPage1")));
+			indexable.AddProperty (Property.NewKeyword ("fixme:WebPage2", contact.GetString ("WebPage2")));
+			indexable.AddProperty (Property.New ("fixme:BirthYear", contact.GetString ("BirthYear")));
+			indexable.AddProperty (Property.New ("fixme:BirthMonth", contact.GetString ("BirthMonth")));
+			indexable.AddProperty (Property.New ("fixme:BirthDay", contact.GetString ("BirthDay")));
+			indexable.AddProperty (Property.New ("fixme:Custom1", contact.GetString ("Custom1")));
+			indexable.AddProperty (Property.New ("fixme:Custom2", contact.GetString ("Custom2")));
+			
+			indexable.AddProperty (Property.NewKeyword ("fixme:Email", contact.GetString ("PrimaryEmail")));
+			indexable.AddProperty (Property.New ("fixme:Name", contact.GetString ("DisplayName")));
+			
+			return indexable;
+		}
+		
+		// Why? Because it's very likely that the user will sometimes change contact details. Current IsUpToDate
+		// (in ThunderbirdIndexableGenerator-class) only checks the "fullyIndexed" property and when the contact
+		// was indexed, thus if the user changes an email address it won't be updated until beagle is restarted.
+		// By always returning false here, we make sure that beagle always re-index contacts when something
+		// happens. It's a really fast and not a very cpu intensive task, so it doesn't really matter.
+		protected new bool IsUpToDate (Uri uri)
+		{
+			// Remove this uri from the cache
+			if (stored_cache != null)
+				stored_cache.Remove (uri.ToString ());
+			
+			return false;
+		}
+	}
+}
+	
Index: ./beagled/ThunderbirdQueryable/LuceneAccess.cs
===================================================================
RCS file: ./beagled/ThunderbirdQueryable/LuceneAccess.cs
diff -N ./beagled/ThunderbirdQueryable/LuceneAccess.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./beagled/ThunderbirdQueryable/LuceneAccess.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,136 @@
+//
+// LuceneAccess.cs: Provides low level access to the underlying Lucene database
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.Collections;
+
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using LNS = Lucene.Net.Search;
+
+using Beagle.Daemon;
+using Beagle.Util;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	public class LuceneAccess : LuceneQueryingDriver {
+		public class StoredInfo 
+		{
+			public DateTime LastIndex;
+			public bool FullyIndexed;
+			public Uri Uri;
+		}
+		
+		public LuceneAccess (string index_name, int minor_version, bool read_only)
+			: base (index_name, minor_version, read_only)
+		{
+		
+		}
+		
+		public StoredInfo DocumentToStoredInfo (Document doc)
+		{
+			int count = 0;
+			StoredInfo info = new StoredInfo ();
+
+			info.Uri = GetUriFromDocument (doc);
+
+			foreach (Field f in doc.Fields ()) {
+				Property prop = GetPropertyFromDocument (f, doc, false);
+				if (prop == null)
+					continue;
+
+				switch (prop.Key) {
+				case "fixme:indexDateTime":
+					info.LastIndex = StringFu.StringToDateTime (prop.Value);
+					count++;
+					break;
+				case "fixme:fullyIndexed":
+					info.FullyIndexed = Convert.ToBoolean (prop.Value);
+					count++;
+					break;
+				}
+				
+				if (count == 2)
+					break;
+			}
+
+			return info;
+		}
+		
+		private class SingletonCollector : LNS.HitCollector
+		{
+			public int MatchId = -1;
+			
+			public override void Collect (int id, float score)
+			{
+				MatchId = id;
+			}
+		}
+		
+		public StoredInfo GetStoredInfo (Uri uri)
+		{
+			StoredInfo info = new StoredInfo ();
+
+			LNS.Query query = UriQuery ("Uri", uri);
+			SingletonCollector collector = new SingletonCollector ();
+			
+			LNS.IndexSearcher searcher = LuceneCommon.GetSearcher (PrimaryStore);
+			searcher.Search (query, null, collector);
+			
+			if (collector.MatchId != -1) { 
+				Document doc = searcher.Doc (collector.MatchId);
+				info = DocumentToStoredInfo (doc);
+			}
+			
+			LuceneCommon.ReleaseSearcher (searcher);
+			
+			return info;
+		}
+		
+		public Hashtable GetStoredUriStrings (string server, string file)
+		{
+			Hashtable uris = new Hashtable ();
+
+			Term term = new Term (PropertyToFieldName (PropertyType.Keyword, "fixme:file"), file);
+			LNS.QueryFilter filter = new LNS.QueryFilter (new LNS.TermQuery (term));
+			
+			term = new Term (PropertyToFieldName (PropertyType.Keyword, "fixme:account"), server);
+			LNS.TermQuery query = new LNS.TermQuery (term);
+			
+			LNS.IndexSearcher searcher = LuceneCommon.GetSearcher (PrimaryStore);
+			LNS.Hits hits = searcher.Search (query, filter);
+			
+			for (int i = 0; i < hits.Length (); i++) {
+				StoredInfo info = DocumentToStoredInfo (hits.Doc (i));	
+				uris.Add (info.Uri.ToString (), info.FullyIndexed);
+			}
+
+			LuceneCommon.ReleaseSearcher (searcher);
+			
+			return uris;
+		}
+	}
+}
Index: ./beagled/ThunderbirdQueryable/Mail.cs
===================================================================
RCS file: ./beagled/ThunderbirdQueryable/Mail.cs
diff -N ./beagled/ThunderbirdQueryable/Mail.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./beagled/ThunderbirdQueryable/Mail.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,172 @@
+//
+// Mail.cs: Adds POP3 and IMAP indexing support to the Thunderbird backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+using GMime;
+	
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	[ThunderbirdIndexableGenerator (TB.AccountType.Pop3, "POP3 Support", true)]
+	[ThunderbirdIndexableGenerator (TB.AccountType.Imap, "IMAP Support", true)]
+	public class MailIndexableGenerator : ThunderbirdIndexableGenerator {
+
+		public MailIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string mork_file)
+			: base (indexer, account, mork_file)
+		{
+		}
+		
+		public override bool HasNextIndexable ()
+		{
+			do {
+				while (DbEnumerator == null || !DbEnumerator.MoveNext ()) {
+					Done = true;
+					indexer.NotificationEvent -= OnNotification;
+					indexer.ChildComplete ();
+					return false;
+				}
+			} while (IsUpToDate ((DbEnumerator.Current as TB.Mail).Uri));
+			
+			return true;
+		}
+		
+		public override Indexable GetNextIndexable ()
+		{
+			TB.Mail mail = DbEnumerator.Current as TB.Mail;
+
+			// If status is different, than something happend when loading this mail and we dont'
+			// want to change it's status.
+			if (mail.GetObject ("FullIndex") == null)
+				mail.SetObject ("FullIndex", (object) FullIndex);
+			
+			return MailToIndexable (mail);
+		}
+
+		public override void LoadDatabase ()
+		{
+			try {
+				db = new TB.Database (account, DbFile);
+				db.Load ();
+			} catch (Exception e) {
+				Logger.Log.Warn ("Failed to load {0}: {1}", DbFile, e.Message);
+				return;
+			}
+			
+			if (db.Count <= 0)
+				return;
+			
+			FullIndex = (Thunderbird.IsFullyIndexable (DbFile) ? true : false);
+			ResetEnumerator ();
+			Logger.Log.Info ("Indexing {0} containing {1} mails ({2})", RelativePath, db.Count, (FullIndex ? "Full" : "Partial"));
+		}
+
+		private Indexable MailToIndexable (TB.Mail mail)
+		{
+			Indexable indexable;
+			GMime.Message message = mail.Message;
+			FullIndex = mail.GetBool ("FullIndex"); // Make sure this is up to date
+			
+			indexable = NewIndexable (mail.Uri, message.Date.ToUniversalTime (), "MailMessage");
+			indexable.MimeType = "message/rfc822";
+			indexable.CacheContent = true;
+			indexable.AddProperty (Property.NewKeyword ("fixme:folder", mail.GetString ("mailbox")));
+			if (mail.GetBool ("FullIndex"))
+				indexable.ContentUri = UriFu.PathToFileUri (Thunderbird.GetFullyIndexableFile (DbFile));
+			else
+				indexable.NoContent = true;
+
+			string subject = GMime.Utils.HeaderDecodePhrase (message.Subject);
+			indexable.AddProperty (Property.New ("dc:title", subject));
+
+			indexable.AddProperty (Property.NewDate ("fixme:date", message.Date.ToUniversalTime ()));
+
+			GMime.InternetAddressList addrs;
+			addrs = message.GetRecipients (GMime.Message.RecipientType.To);
+			foreach (GMime.InternetAddress ia in addrs) {
+				indexable.AddProperty (Property.NewUnsearched ("fixme:to", ia.ToString (false)));
+				if (ia.AddressType != GMime.InternetAddressType.Group) {
+					indexable.AddProperty (Property.New ("fixme:to_address", ia.Addr));
+					indexable.AddProperty (Property.NewUnstored ("fixme:to_sanitized", StringFu.SanitizeEmail (ia.Addr)));
+				}
+				indexable.AddProperty (Property.New ("fixme:to_name", ia.Name));
+			}
+			addrs.Dispose ();
+
+			addrs = message.GetRecipients (GMime.Message.RecipientType.Cc);
+			foreach (GMime.InternetAddress ia in addrs) {
+				indexable.AddProperty (Property.NewUnsearched ("fixme:cc", ia.ToString (false)));
+				if (ia.AddressType != GMime.InternetAddressType.Group) {
+					indexable.AddProperty (Property.New ("fixme:cc_address", ia.Addr));
+					indexable.AddProperty (Property.NewUnstored ("fixme:cc_sanitized", StringFu.SanitizeEmail (ia.Addr)));
+				}
+				indexable.AddProperty (Property.New ("fixme:cc_name", ia.Name));
+			}
+			addrs.Dispose ();
+
+			addrs = GMime.InternetAddressList.ParseString (GMime.Utils.HeaderDecodePhrase (message.Sender));
+			foreach (GMime.InternetAddress ia in addrs) {
+				indexable.AddProperty (Property.NewUnsearched ("fixme:from", ia.ToString (false)));
+				if (ia.AddressType != GMime.InternetAddressType.Group) {
+					indexable.AddProperty (Property.New ("fixme:from_address", ia.Addr));
+					indexable.AddProperty (Property.NewUnstored ("fixme:from_sanitized", StringFu.SanitizeEmail (ia.Addr)));
+				}
+				indexable.AddProperty (Property.New ("fixme:from_name", ia.Name));
+			}
+			addrs.Dispose ();
+
+			if (HasAttachments (message.MimePart))
+				indexable.AddProperty (Property.NewFlag ("fixme:hasAttachments"));
+
+			if (message.ContentType.ToString ().ToLower () == "text/plain")
+				indexable.SetTextReader (new StreamReader (message.Stream));
+			indexable.SetBinaryStream (message.Stream);
+			
+			return indexable;
+		}
+		
+		private bool HasAttachments (GMime.Object mime_part)
+		{
+			if (mime_part is GMime.MessagePart)
+				return true;
+
+			// Messages that are multipart/alternative shouldn't be considered as having
+			// attachments.  Unless of course they do.
+			if (mime_part is GMime.Multipart && mime_part.ContentType.Subtype.ToLower () != "alternative")
+				return true;
+
+			return false;
+		}
+	}
+}
+	
Index: ./beagled/ThunderbirdQueryable/MoveMail.cs
===================================================================
RCS file: ./beagled/ThunderbirdQueryable/MoveMail.cs
diff -N ./beagled/ThunderbirdQueryable/MoveMail.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./beagled/ThunderbirdQueryable/MoveMail.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,107 @@
+//
+// MoveMail: Adds Unix Mailspool (MoveMail) indexing support to the Thunderbird backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+using GMime;
+	
+namespace Beagle.Daemon.ThunderbirdQueryable {	
+	
+	[ThunderbirdIndexableGenerator (TB.AccountType.MoveMail, "Movemail Support", false)]
+	public class MoveMailIndexableGenerator : ThunderbirdIndexableGenerator {
+	
+		public MoveMailIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string file)
+			: base (indexer, account, file)
+		{
+		}
+		
+		public override bool HasNextIndexable ()
+		{
+			do {
+				if (DbEnumerator == null || !DbEnumerator.MoveNext ()) {
+					Done = true;
+					indexer.NotificationEvent -= OnNotification;
+					indexer.ChildComplete ();
+					return false;
+				}
+			} while (IsUpToDate ((DbEnumerator.Current as TB.MoveMail).Uri));
+			
+			return true;
+		}
+		
+		public override Indexable GetNextIndexable ()
+		{
+			TB.MoveMail message = DbEnumerator.Current as TB.MoveMail;
+			
+			// If status is different, than something happend when loading this mail and we dont'
+			// want to change it's status.
+			if (message.GetObject ("FullIndex") == null)
+				message.SetObject ("FullIndex", (object) FullIndex);
+			
+			return MoveMailToIndexable (message);
+		}
+		
+		public override void LoadDatabase ()
+		{
+			try {
+				db = new TB.Database (account, DbFile);
+				db.Load ();
+			} catch (Exception e) {
+				Logger.Log.Debug ("Failed to load {0}: {1}", DbFile, e.Message);
+				return;
+			}
+			
+			if (db.Count <= 0)
+				return;
+			
+			FullIndex = (Thunderbird.IsFullyIndexable (DbFile) ? true : false);
+			ResetEnumerator ();
+			Logger.Log.Info ("Indexing {0} Movemails ({1})", db.Count, RelativePath);
+		}
+		
+		// FIXME: This need some more info
+		private Indexable MoveMailToIndexable (TB.MoveMail mail)
+		{
+			Indexable indexable;
+
+			indexable = NewIndexable (mail.Uri, DateTime.UtcNow, "MailMessage");
+			indexable.MimeType = "message/rfc822";
+			
+			string subject = GMime.Utils.HeaderDecodePhrase (mail.GetString ("subject"));
+			indexable.AddProperty (Property.New ("dc:title", subject));
+			
+			return indexable;
+		}
+	}
+}
Index: ./beagled/ThunderbirdQueryable/NNTP.cs
===================================================================
RCS file: ./beagled/ThunderbirdQueryable/NNTP.cs
diff -N ./beagled/ThunderbirdQueryable/NNTP.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./beagled/ThunderbirdQueryable/NNTP.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,113 @@
+//
+// NNTP.cs: Adds NNTP indexing support to the Thunderbird backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+using GMime;
+	
+namespace Beagle.Daemon.ThunderbirdQueryable {	
+	
+	[ThunderbirdIndexableGenerator (TB.AccountType.Nntp, "NNTP Support", false)]
+	public class NNTPIndexableGenerator : ThunderbirdIndexableGenerator {
+	
+		public NNTPIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string file)
+			: base (indexer, account, file)
+		{
+		}
+		
+		public override bool HasNextIndexable ()
+		{
+			do {
+				if (DbEnumerator == null || !DbEnumerator.MoveNext ()) {
+					Done = true;
+					indexer.NotificationEvent -= OnNotification;
+					indexer.ChildComplete ();
+					return false;
+				}
+			} while (IsUpToDate ((DbEnumerator.Current as TB.NntpMessage).Uri));
+			
+			return true;
+		}
+		
+		public override Indexable GetNextIndexable ()
+		{
+			TB.NntpMessage message = DbEnumerator.Current as TB.NntpMessage;
+			
+			// If status is different, than something happend when loading this mail and we dont'
+			// want to change it's status.
+			if (message.GetObject ("FullIndex") == null)
+				message.SetObject ("FullIndex", (object) FullIndex);
+			
+			return NntpMessageToIndexable (message);
+		}
+		
+		public override void LoadDatabase ()
+		{
+			try {
+				db = new TB.Database (account, DbFile);
+				db.Load ();
+			} catch (Exception e) {
+				Logger.Log.Debug ("Failed to load {0}: {1}", DbFile, e.Message);
+				return;
+			}
+			
+			if (db.Count <= 0)
+				return;
+			
+			FullIndex = (Thunderbird.IsFullyIndexable (DbFile) ? true : false);
+			ResetEnumerator ();
+			Logger.Log.Info ("Indexing {0} NNTP messages", db.Count);
+		}
+		
+		// FIXME: This need some more info
+		private Indexable NntpMessageToIndexable (TB.NntpMessage message)
+		{
+			Indexable indexable;
+			
+			indexable = new Indexable (message.Uri);
+			indexable.HitType = "MailMessage";
+			indexable.MimeType = "message/rfc822";
+			indexable.Timestamp = DateTime.Parse (message.GetString ("date")).ToUniversalTime ();
+			
+			indexable.AddProperty (Property.NewKeyword ("fixme:client", "thunderbird"));
+			indexable.AddProperty (Property.NewUnsearched ("fixme:fullyIndexed", message.GetBool ("FullIndex")));
+			indexable.AddProperty (Property.NewDate ("fixme:indexDateTime", DateTime.UtcNow));
+			
+			string subject = GMime.Utils.HeaderDecodePhrase (message.GetString ("subject"));
+			indexable.AddProperty (Property.New ("dc:title", subject));
+			
+			return indexable;
+		}
+	}
+}
Index: ./beagled/ThunderbirdQueryable/RSS.cs
===================================================================
RCS file: ./beagled/ThunderbirdQueryable/RSS.cs
diff -N ./beagled/ThunderbirdQueryable/RSS.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./beagled/ThunderbirdQueryable/RSS.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,122 @@
+//
+// RSS.cs: Adds RSS feed indexing support to the Thunderbird backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {	
+
+	[ThunderbirdIndexableGenerator (TB.AccountType.Rss, "RSS Support", true)]
+	public class RSSIndexableGenerator : ThunderbirdIndexableGenerator {
+		private string feed_url;
+		
+		public RSSIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string mork_file)
+			: base (indexer, account, mork_file)
+		{
+		}
+			
+		public override bool HasNextIndexable ()
+		{
+			do {
+				if (DbEnumerator == null || !DbEnumerator.MoveNext ()) {
+					Done = true;
+					indexer.NotificationEvent -= OnNotification;
+					indexer.ChildComplete ();
+					return false;
+				}
+			} while (IsUpToDate ((DbEnumerator.Current as TB.RssFeed).Uri));
+			
+			return true;
+		}
+		
+		public override Indexable GetNextIndexable ()
+		{
+			TB.RssFeed feed = DbEnumerator.Current as TB.RssFeed;
+			
+			// If status is different, than something happend when loading this mail and we dont'
+			// want to change it's status.
+			if (feed.GetObject ("FullIndex") == null)
+				feed.SetObject ("FullIndex", (object) FullIndex);
+			
+			return RssFeedToIndexable (feed);
+		}
+		
+		public override void LoadDatabase ()
+		{
+			string folder_name = null;
+			
+			try {
+				db = new TB.Database (account, DbFile);
+				db.Load ();
+				
+				Hashtable tbl = db.Db.Compile ("1", "ns:msg:db:row:scope:dbfolderinfo:all");
+				feed_url = tbl ["feedUrl"] as string;
+				folder_name = tbl ["folderName"] as string;
+			} catch (Exception e) {
+				Logger.Log.Debug ("Failed to load {0}: {1}", DbFile, e.Message);
+				return;
+			}
+			
+			if (db.Count <= 0)
+				return;
+			
+			ResetEnumerator ();
+			Logger.Log.Info ("Indexing \"{0}\" RSS feed containing {1} entries ({2})", folder_name, db.Count, RelativePath);
+		}
+		
+		private Indexable RssFeedToIndexable (TB.RssFeed feed)
+		{
+			Indexable indexable;
+			StringReader content = feed.Content;
+			
+			indexable = new Indexable (feed.Uri);
+			indexable.MimeType = "text/html";
+			indexable.HitType = "FeedItem";
+			indexable.Timestamp = DateTime.Parse (feed.GetString ("date")).ToUniversalTime ();
+
+			indexable.AddProperty (Property.NewKeyword ("fixme:client", "thunderbird"));
+			indexable.AddProperty (Property.NewUnsearched ("fixme:fullyIndexed", feed.GetBool ("FullIndex")));
+			indexable.AddProperty (Property.NewDate ("fixme:indexDateTime", DateTime.UtcNow));
+			
+			indexable.AddProperty (Property.NewKeyword ("dc:identifier", feed.GetString ("message-id")));
+			indexable.AddProperty (Property.NewKeyword ("dc:source", feed_url));
+			indexable.AddProperty (Property.New ("dc:publisher", feed.GetString ("sender")));
+			if (content == null)
+				indexable.AddProperty (Property.New ("dc:title", feed.GetString ("subject")));
+			
+			indexable.SetTextReader (feed.Content);
+			
+			return indexable;
+		}
+	}
+}
Index: ./beagled/ThunderbirdQueryable/ThunderbirdIndexableGenerator.cs
===================================================================
RCS file: ./beagled/ThunderbirdQueryable/ThunderbirdIndexableGenerator.cs
diff -N ./beagled/ThunderbirdQueryable/ThunderbirdIndexableGenerator.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./beagled/ThunderbirdQueryable/ThunderbirdIndexableGenerator.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,258 @@
+//
+// ThunderbirdIndexableGenerator.cs: A helper class that makes it very easy to add new features to this backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+using GMime;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	// This is a generic IndexableGenerator-class and should be used to index mork files only!
+	public abstract class ThunderbirdIndexableGenerator : IIndexableGenerator {
+		protected ThunderbirdIndexer indexer;
+		protected TB.Database db;
+		protected TB.Account account;
+		protected IEnumerator db_enumerator;
+		
+		private bool full_index;
+		private string db_file;
+		private bool done;
+		private string relative_path;
+		protected Hashtable stored_cache;
+		
+		public ThunderbirdIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string db_file)
+		{
+			this.indexer = indexer;
+			this.indexer.NotificationEvent += OnNotification;
+			this.account = account;
+			this.full_index = true;
+			this.db_file = db_file;
+			this.done = false;
+			this.relative_path = Thunderbird.GetRelativePath (db_file);
+			
+			// Load the database and make sure the enumerator is up to date. Otherwise we will
+			// get lots of null exceptions when enumerating the database.
+			LoadDatabase ();
+			ResetEnumerator ();
+
+			// Fetch all already stored uris in the index. This way we can remove one uri at the time
+			// while we are indexing and thus in the end now which mails that doesn't exist anymore.
+			stored_cache = indexer.Lucene.GetStoredUriStrings (account.Server, relative_path);
+		}
+		
+		public abstract bool HasNextIndexable ();
+		public abstract Indexable GetNextIndexable ();
+		public abstract void LoadDatabase ();
+		
+		public virtual bool IsUpToDate (Uri uri)
+		{
+			if (uri == null)
+				return false;
+			
+			LuceneAccess.StoredInfo info = indexer.Lucene.GetStoredInfo (uri);
+
+			// Remove this uri from the cache
+			if (stored_cache != null)
+				stored_cache.Remove (uri.ToString ());
+			
+			// Check if this time is "older" than the time we began to index and if the index
+			// status has changed (partial vs. full indexing)
+			if (info != null && ThunderbirdQueryable.IndexingStart.CompareTo (info.LastIndex) < 0 && 
+				FullIndex == info.FullyIndexed) {
+				return true;
+			}
+
+			return false;
+		}
+		
+		public virtual void PostFlushHook ()
+		{
+			if (!Done || (stored_cache == null) || (Done && stored_cache.Count == 0))
+				return;
+			
+			if (Thunderbird.Debug)
+				Logger.Log.Debug ("Cleaning out old objects in {0} ({1})", RelativePath, stored_cache.Count);
+		
+			ArrayList uris = new ArrayList ();
+			foreach (string uri_str in stored_cache.Keys)
+				uris.Add (new Uri (uri_str));
+				
+			indexer.ScheduleRemoval ((Uri[]) uris.ToArray (typeof (Uri)), 
+				String.Format ("PostFlushHook-{0}", RelativePath), Scheduler.Priority.Delayed);
+		}
+		
+		protected virtual Indexable NewIndexable (Uri uri, DateTime timestamp, string hit_type)
+		{
+			Indexable indexable;
+			
+			indexable = new Indexable (uri);
+			indexable.HitType = hit_type;
+			indexable.Timestamp = timestamp;
+			
+			indexable.AddProperty (Property.NewKeyword ("fixme:account", account.Server));
+			indexable.AddProperty (Property.NewKeyword ("fixme:client", "thunderbird"));
+			indexable.AddProperty (Property.NewUnsearched ("fixme:fullyIndexed", full_index));
+			indexable.AddProperty (Property.NewUnsearched ("fixme:file", RelativePath));
+			indexable.AddProperty (Property.NewDate ("fixme:indexDateTime", DateTime.UtcNow));
+			
+			return indexable;
+		}
+		
+		protected virtual void ResetEnumerator ()
+		{
+			if (db != null && db.Count > 0)
+				db_enumerator = db.GetEnumerator ();
+			else
+				db_enumerator = null;
+		}
+		
+		protected virtual void OnNotification (object o, NotificationEventArgs args)
+		{
+			if (args.Account != account)
+				return;
+			
+			switch (args.Type) {
+			case NotificationType.StopIndexing:
+				indexer.NotificationEvent -= OnNotification;
+				Logger.Log.Debug ("Stopping running task {0}", account.Server);
+				break;
+				
+			case NotificationType.RestartIndexing:
+				LoadDatabase ();
+				break;
+				
+			case NotificationType.UpdateAccountInformation:
+				account = (TB.Account) args.Data;
+				LoadDatabase ();
+				break;
+				
+			}
+		}
+
+		// Done should be set to true when there's no more objects to index. This will allow
+		// PostFlushHook to remove old objects from the index.
+		public bool Done {
+			get { return done; }
+			set { done = value; }
+		}
+		
+		public string DbFile {
+			get { return db_file; }
+			set { db_file = value; }
+		}
+		
+		public bool FullIndex {
+			get { return full_index; }
+			set { full_index = value; }
+		}
+		
+		// Realtive path to current mork_file
+		public string RelativePath {
+			get { return relative_path; }
+		}
+		
+		protected IEnumerator DbEnumerator {
+			get { return db_enumerator; }
+		}
+		
+		public string StatusName { 
+			get { return account.Server; }
+		}
+	}
+	
+	/////////////////////////////////////////////////////////////////////////////////////
+	
+	[AttributeUsage (AttributeTargets.Class, AllowMultiple = true)]
+	public class ThunderbirdIndexableGeneratorAttribute : System.Attribute {
+		private TB.AccountType type;
+		private string description;
+		private bool enabled;
+		
+		public ThunderbirdIndexableGeneratorAttribute (TB.AccountType type, string description, bool enabled)
+		{
+			this.type = type;
+			this.description = description;
+			this.enabled = enabled;
+		}
+		
+		public TB.AccountType Type {
+			get { return type; }
+			set { type = value; }
+		}
+		
+		public string Description {
+			get { return description; }
+			set { description = value; }
+		}
+		
+		public bool Enabled {
+			get { return enabled; }
+			set { enabled = value; }
+		}
+	}
+	
+	/////////////////////////////////////////////////////////////////////////////////////
+	
+	
+	public class UriRemovalIndexableGenerator : IIndexableGenerator {
+		private Uri[] uris;
+		
+		private IEnumerator enumerator;
+		
+		public UriRemovalIndexableGenerator (Uri[] uris)
+		{
+			this.uris = uris;
+			this.enumerator = this.uris.GetEnumerator ();
+		}
+		
+		public Indexable GetNextIndexable ()
+		{
+			return new Indexable (IndexableType.Remove, (Uri) enumerator.Current);
+		}
+		
+		public bool HasNextIndexable ()
+		{
+			while (enumerator == null || !enumerator.MoveNext ())
+				return false;
+			
+			return true;
+		}
+		
+		public string StatusName {
+			get { return String.Format ("Removing {0} uris", uris.Length); }
+		}
+		
+		public void PostFlushHook () { }
+	}
+}
Index: ./beagled/ThunderbirdQueryable/ThunderbirdIndexer.cs
===================================================================
RCS file: ./beagled/ThunderbirdQueryable/ThunderbirdIndexer.cs
diff -N ./beagled/ThunderbirdQueryable/ThunderbirdIndexer.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./beagled/ThunderbirdQueryable/ThunderbirdIndexer.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,462 @@
+//
+// ThunderbirdIndexer.cs: This class launches IndexableGenerators and makes sure instant-updates work
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	public class ThunderbirdIndexer {
+		private ThunderbirdQueryable queryable;
+
+		private bool init_phase, first_lap;
+		private string[] root_paths;
+		private Hashtable supported_types;
+		private ArrayList account_list;
+		private ThunderbirdInotify inotify;
+	
+		public delegate void NotificationEventHandler (object o, NotificationEventArgs args);
+	
+		public ThunderbirdIndexer (ThunderbirdQueryable queryable, string[] root_paths)
+		{
+			this.queryable = queryable;
+			this.root_paths = root_paths;
+			this.supported_types = new Hashtable ();
+			this.init_phase = true;
+			this.first_lap = true;
+			this.account_list = new ArrayList ();
+			this.inotify = new ThunderbirdInotify ();
+			
+			LoadSupportedTypes ();
+			
+			foreach (string path in root_paths) {
+				Inotify.Subscribe (path, OnInotifyEvent, 
+					Inotify.EventType.Delete | 
+					Inotify.EventType.MovedTo | 
+					Inotify.EventType.Modify |
+					Inotify.EventType.Create);
+			}
+			
+			inotify.InotifyEvent += OnInotifyEvent;
+		}
+		
+		// Loads all supported types, checks if they have a correct constructor and is enabled
+		private void LoadSupportedTypes ()
+		{
+			Assembly assembly = Assembly.GetCallingAssembly ();
+			
+			foreach (Type type in ReflectionFu.ScanAssemblyForInterface (assembly, typeof (IIndexableGenerator))) {
+			
+				foreach (ThunderbirdIndexableGeneratorAttribute attr in 
+					ReflectionFu.ScanTypeForAttribute (type, typeof (ThunderbirdIndexableGeneratorAttribute))) {
+
+					foreach (ConstructorInfo constructor in type.GetConstructors ()) {
+					
+						ParameterInfo[] parameters = constructor.GetParameters ();
+						if(parameters.Length != 3)
+							continue;
+						
+						if (parameters [0].ParameterType.Equals (typeof (ThunderbirdIndexer)) &&
+							parameters [1].ParameterType.Equals (typeof (TB.Account)) &&
+							parameters [2].ParameterType.Equals (typeof (string))) {
+							
+							// Make sure we should enable this type
+							if (attr.Enabled)
+								supported_types [attr.Type]  = type;
+							
+						} else
+							Logger.Log.Debug ("{0} has an invalid constructor!", type.ToString ());
+					}
+				}
+			}	
+		}
+		
+		public void Crawl ()
+		{
+			int launched = 0;
+			
+			foreach (string path in root_paths) {
+				foreach (TB.Account account in Thunderbird.ReadAccounts (path)) {
+					if (Shutdown.ShutdownRequested)
+						return;
+					
+					if (supported_types [account.Type] == null)
+						continue;
+					
+					IndexAccount (account);
+					launched++;
+				}
+			}
+			
+			init_phase = false;
+			Logger.Log.Info ("Indexing {0} ({1}) Thunderbird account(s) spread over {2} profile(s)", 
+				launched, account_list.Count, root_paths.Length);
+			
+			// Clean out old stuff in case no IndexableGenerator was launched
+			if (launched == 0)
+				ChildComplete ();
+		}
+		
+		public void IndexAccount (TB.Account account)
+		{
+			TB.Account stored_account = GetParentAccount (account.Path);
+
+			// We need to act upon changes made to accounts during Thunderbird runtime.
+			// The user might change from plain to SSL, which leads to a new port number
+			// that has to be taken in account for when indexing.
+			if (stored_account == null && Directory.Exists (account.Path) && supported_types [account.Type] != null) {
+				account_list.Add (account);
+				IndexDirectory (account.Path);
+				//Logger.Log.Info ("Indexing {0} account {1}", account.Type.ToString (), account.Server);
+			
+			} else if (stored_account == null && File.Exists (account.Path) && supported_types [account.Type] != null) {
+				account_list.Add (account);
+				IndexFile (account.Path);
+				//Logger.Log.Info ("Indexing {0} account {1}", account.Type.ToString (), account.Server);
+				
+			} else if (stored_account != null &&
+				(stored_account.Server != account.Server ||
+				stored_account.Port != account.Port ||
+				stored_account.Type != account.Type ||
+				stored_account.Delimiter != account.Delimiter)) {
+
+				account_list.Remove (stored_account);
+				account_list.Add (account);
+				
+				// Make sure all running indexables are aware of this since it can affect the way they index
+				NotificationEventArgs args;
+				args = new NotificationEventArgs (NotificationType.UpdateAccountInformation, stored_account);
+				args.Data = (object) account;
+				OnNotification (args);
+				
+				Logger.Log.Info ("Updated {0} with new account details", account.Server);
+			}
+		}
+		
+		public void IndexFile (string file)
+		{
+			TB.Account account = GetParentAccount (file);
+						
+			if (account == null || supported_types [account.Type] == null)
+				return;
+			
+			object[] param = new object[] {this, account, file};
+			ThunderbirdIndexableGenerator generator = Activator.CreateInstance (
+				(Type) supported_types [account.Type], param) as ThunderbirdIndexableGenerator;
+			
+			AddIIndexableTask (generator, file);
+		}
+		
+		private void IndexDirectory (string directory)
+		{
+			Queue pending = new Queue ();
+			
+			pending.Enqueue (directory);
+			while (pending.Count > 0) {
+				string dir = pending.Dequeue () as string;
+				
+				foreach (string subdir in DirectoryWalker.GetDirectories (dir)) {
+					if (Shutdown.ShutdownRequested)
+						return;
+						
+					pending.Enqueue (subdir);
+				}
+
+				if (Inotify.Enabled) {
+						inotify.Watch (dir, 
+						Inotify.EventType.Modify | 
+						Inotify.EventType.Create |
+						Inotify.EventType.Delete |
+						Inotify.EventType.MovedFrom |
+						Inotify.EventType.MovedTo);
+				}
+				
+				foreach (string file in DirectoryWalker.GetItems 
+					(dir, new DirectoryWalker.FileFilter (Thunderbird.IsMorkFile))) {
+					if (Shutdown.ShutdownRequested)
+						return;
+					
+					IndexFile (file);
+				}
+			}
+		}
+		
+		public void RemoveAccount (TB.Account account)
+		{
+			TB.Account acc = GetParentAccount (account.Path);
+			
+			if (acc == null)
+				return;
+			
+			ScheduleRemoval (Property.NewKeyword ("fixme:account", acc.Server), Scheduler.Priority.Delayed);
+			OnNotification (new NotificationEventArgs (NotificationType.StopIndexing, account));
+			account_list.Remove (acc);
+		}
+
+		private void AddIIndexableTask (IIndexableGenerator generator, string tag)
+		{
+			if (queryable.ThisScheduler.ContainsByTag (tag)) {
+				Logger.Log.Debug ("Not adding a Task for already running: {0}", tag);
+				return;
+			}
+
+			Scheduler.Task task = queryable.NewAddTask (generator);
+			task.Tag = tag;
+			queryable.ThisScheduler.Add (task);
+		}
+		
+		private void ScheduleRemoval (Property prop, Scheduler.Priority priority)
+		{
+			if (queryable.ThisScheduler.ContainsByTag (prop.ToString ())) {
+				Logger.Log.Debug ("Not adding a Task for already running: {0}", prop.ToString ());
+				return;
+			}
+			
+			Scheduler.Task task = queryable.NewRemoveByPropertyTask (prop);
+			task.Priority = priority;
+			task.SubPriority = 0;
+			queryable.ThisScheduler.Add (task);
+		}
+
+		public void ScheduleRemoval (Uri[] uris, string tag, Scheduler.Priority priority)
+		{
+			if (queryable.ThisScheduler.ContainsByTag (tag)) {
+				Logger.Log.Debug ("Not adding a Task for already running: {0}", tag);
+				return;
+			}
+
+			Scheduler.Task task = queryable.NewAddTask (new UriRemovalIndexableGenerator (uris));
+			task.Priority = priority;
+			task.SubPriority = 0;
+			queryable.ThisScheduler.Add (task);
+		}
+		
+		public void UpdateAccounts (string root_path)
+		{
+			ArrayList new_accounts;
+
+			try {
+				new_accounts = Thunderbird.ReadAccounts (root_path);
+			} catch (Exception e) {
+				Logger.Log.Warn ("Failed when reading Thunderbird accounts: {0}, account may have been added or removed", e);
+				return;
+			}
+
+			// Add all accounts again to make sure things are updated the way they should
+			foreach (TB.Account account in new_accounts)
+				IndexAccount (account);
+
+			// Remove non-existing accounts
+			foreach (TB.Account existing in account_list) {
+				bool found = false;
+			
+				foreach (TB.Account new_account in new_accounts) {
+					if (existing.Path == new_account.Path) {
+						found = true;
+						break;
+					}
+				}
+
+				if (!found) 
+						RemoveAccount (existing);
+			}
+		}
+		
+		public TB.Account GetParentAccount (string directory)
+		{
+			foreach (TB.Account acc in account_list) {
+				if (directory.StartsWith (acc.Path))
+					return acc;
+			}
+			
+			return null;
+		}
+
+		private void OnInotifyEvent (Inotify.Watch watch,
+						string path,
+						string subitem,
+						string srcpath,
+						Inotify.EventType type)
+		{
+			if (subitem == null)
+				return;
+				
+			string full_path = Path.Combine (path, subitem);
+
+			// If prefs.js is deleted... then we have nothing at all to index
+			if (((type & Inotify.EventType.MovedTo) != 0 && srcpath == Path.Combine (path, "prefs.js")) ||
+				((type & Inotify.EventType.Delete) != 0 && subitem == "prefs.js")) {
+				
+				foreach (TB.Account account in account_list)
+					RemoveAccount (account);
+				return;
+			}
+
+			// Update in case an account was removed or added
+			// Thunderbird saves prefs.js with a different name and then replacing the old one
+			// by "moving" it over the existing prefs.js. That's why MoveTo is used as inotfy type.
+			if ((((type & Inotify.EventType.Modify) != 0 || (type & Inotify.EventType.MovedTo) != 0 || 
+				(type & Inotify.EventType.Create) != 0) && subitem == "prefs.js")) {
+				
+				UpdateAccounts (path);
+				return;
+			}
+
+			// In case the address book file have been moved or deleted, we have to stop indexing it
+			if (((type & Inotify.EventType.MovedTo) != 0 && srcpath == Path.Combine (path, "abook.mab")) ||
+				((type & Inotify.EventType.Delete) != 0 && subitem == "abook.mab")) {
+				
+				TB.Account account = GetParentAccount (full_path);
+				
+				if (account != null)					
+					RemoveAccount (account);
+					
+				return;
+			}
+
+			// In case of a newly created addressbook, the current address book is modified or an old 
+			// address book is moved to where the address book can be found: either start indexing 
+			// or restart an already indexing IndeaxbleGenerator.
+			if ((((type & Inotify.EventType.Modify) != 0 || (type & Inotify.EventType.MovedTo) != 0 || 
+				(type & Inotify.EventType.Create) != 0) && subitem == "abook.mab")) {
+				
+				TB.Account account = GetParentAccount (full_path);
+				
+				if (account == null && File.Exists (full_path)) {
+					UpdateAccounts (path);
+					return;
+				} else if (account == null)
+					return;
+
+				// Tell any running indexable about this or start a new one
+				if (queryable.ThisScheduler.ContainsByTag (full_path))
+					OnNotification (new NotificationEventArgs (NotificationType.RestartIndexing, account));
+				else
+					IndexFile (full_path);
+				
+				return;
+			}
+			
+			// Re-index files when needed
+			if ((type & Inotify.EventType.Modify) != 0) {
+				TB.Account account = GetParentAccount (full_path);
+			
+				if (account == null || !Thunderbird.IsMorkFile (path, subitem))
+					return;
+				
+				// In case we have a running IndexableGenerator, tell it that we have a file that needs to 
+				// be re-indexed.
+				if (queryable.ThisScheduler.ContainsByTag (full_path))
+					OnNotification (new NotificationEventArgs (NotificationType.RestartIndexing, account));
+				else
+					IndexFile (full_path);
+					
+				return;
+			}
+			
+			// Index newly created directories
+			if ((type & Inotify.EventType.Create) != 0 && (type & Inotify.EventType.IsDirectory) != 0) {
+				if (GetParentAccount (full_path) != null && Inotify.Enabled)
+					Inotify.Subscribe (full_path, OnInotifyEvent, Inotify.EventType.All);
+					
+				return;
+			}
+		}
+
+		public void ChildComplete ()
+		{
+			if (NotificationEvent != null || init_phase || !first_lap)
+				return;
+			
+			if (Thunderbird.Debug)
+				Logger.Log.Debug ("Removing old Thunderbird objects");
+
+			Scheduler.Task task = queryable.NewRemoveTaskByDate (ThunderbirdQueryable.IndexingStart);
+			task.Priority = Scheduler.Priority.Idle;
+			task.Tag = "RemoveOldThunderbirdMails";
+			queryable.ThisScheduler.Add (task);
+			
+			// This makes sure that ChildComplete will only clean out all mails once in a lifetime
+			// (of the Thunderbird backend that is)
+			first_lap = false;
+		}
+
+		protected virtual void OnNotification(NotificationEventArgs args)
+		{
+			if (NotificationEvent != null)
+				NotificationEvent (this, args);
+		}
+
+		public event NotificationEventHandler NotificationEvent;
+		
+		public LuceneAccess Lucene {
+			get { return queryable.Lucene; }
+		}
+	}
+	
+	/////////////////////////////////////////////////////////////////////////////////////
+	
+	public enum NotificationType {
+		StopIndexing,
+		RestartIndexing,
+		UpdateAccountInformation
+	}
+	
+	/////////////////////////////////////////////////////////////////////////////////////
+	
+	public class NotificationEventArgs  : EventArgs
+	{
+		private NotificationType type;
+		private TB.Account account;
+		private object data;
+
+		public NotificationEventArgs (NotificationType type, TB.Account account)
+		{
+			this.type = type;
+			this.account = account;
+		}
+		
+		public NotificationType Type {
+			get { return type; }
+		}
+		
+		public TB.Account Account {
+			get { return account; }
+		}
+		
+		public object Data {
+			get { return data; }
+			set { data =value; }
+		}
+	}
+	
+}
Index: ./beagled/ThunderbirdQueryable/ThunderbirdInotify.cs
===================================================================
RCS file: ./beagled/ThunderbirdQueryable/ThunderbirdInotify.cs
diff -N ./beagled/ThunderbirdQueryable/ThunderbirdInotify.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./beagled/ThunderbirdQueryable/ThunderbirdInotify.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,160 @@
+//
+// ThunderbirdInotify.cs. This class will sumnarize inotify events and raise an event every 30 seconds (to prevent inotify hammering)
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+
+using Beagle;
+using Beagle.Util;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	public class ThunderbirdInotify {
+		protected struct Event {
+			public Inotify.Watch Watch;
+			public string Path;
+			public string Subitem;
+			public string Srcpath;
+			public Inotify.EventType Type;
+			public long OldFileSize;
+			public long CurrentFileSize;
+			
+			public Event (Inotify.Watch watch, string path, string subitem,
+				string srcpath, Inotify.EventType type, long old_size, long current_size)
+			{
+				this.Watch = watch;
+				this.Path = path;
+				this.Subitem = subitem;
+				this.Srcpath = srcpath;
+				this.Type = type;
+				this.OldFileSize = old_size;
+				this.CurrentFileSize = current_size;
+			}
+		}
+
+		private Queue queue;
+		
+		public ThunderbirdInotify ()
+		{
+			queue = new Queue ();
+			
+			GLib.Timeout.Add (30000, new GLib.TimeoutHandler (Process));
+		}
+		
+		public void Watch (string path, Inotify.EventType type)
+		{
+			Inotify.Subscribe (path, OnInotify, type);
+		}
+		
+		private void OnInotify (Inotify.Watch watch,
+					string path,
+					string subitem,
+					string srcpath,
+					Inotify.EventType type)
+		{
+			if (subitem == null)
+				return;
+
+			// Unsubscribe to directories that have been removed
+			if ((type & Inotify.EventType.Delete) != 0 && (type & Inotify.EventType.IsDirectory) != 0)
+				watch.Unsubscribe ();
+
+			lock (queue.SyncRoot) {
+				bool found = false;
+				for (int i = 0; i < queue.Count; i++) {
+					Event ev = (Event) queue.Dequeue ();
+					
+					if (ev.Path == path && ev.Subitem == subitem && ev.Srcpath == srcpath) {
+						found = true;
+						ev.Type = (ev.Type | type);
+						queue.Enqueue (ev);
+						break;
+					}
+					
+					queue.Enqueue (ev);
+				}
+				
+				if (!found) {
+					queue.Enqueue (new Event (watch, path, subitem, srcpath, 
+						type, -1, GetFileSize (Path.Combine (path, subitem))));
+				}
+			}
+		}
+			
+		private bool Process ()
+		{
+			Queue tmp = new Queue ();
+			
+			lock (queue.SyncRoot) {
+				while (queue.Count > 0) {
+					Event ev = (Event) queue.Dequeue();
+					long size = GetFileSize (Path.Combine (ev.Path, ev.Subitem));
+					
+					if (Thunderbird.Debug) {
+						Logger.Log.Debug ("EVENT: {0} ({1}) [{2}, {3}]", 
+							Path.Combine (ev.Path, ev.Subitem).ToString (), ev.Type, ev.CurrentFileSize, size);
+					}
+
+					if (size != ev.CurrentFileSize) {
+						ev.OldFileSize = ev.CurrentFileSize;
+						ev.CurrentFileSize = size;
+						tmp.Enqueue (ev);
+						continue;
+					}
+						
+					OnInotifyEvent (ev);
+				}
+
+				while (tmp.Count > 0)
+					queue.Enqueue (tmp.Dequeue ());
+			}
+		
+			return true;
+		}
+
+		private long GetFileSize (string filename)
+		{
+			long filesize = -1;
+			
+			try {
+				FileInfo file = new FileInfo (filename);
+				filesize = file.Length;
+			} catch (Exception e) { }
+				
+			return filesize;
+		}
+		
+		protected virtual void OnInotifyEvent (Event ev)
+		{
+			if (InotifyEvent != null)
+				InotifyEvent (ev.Watch, ev.Path, ev.Subitem, ev.Srcpath, ev.Type);
+		}
+		
+		public event Inotify.InotifyCallback InotifyEvent;
+	}
+
+}
Index: ./beagled/ThunderbirdQueryable/ThunderbirdQueryable.cs
===================================================================
RCS file: ./beagled/ThunderbirdQueryable/ThunderbirdQueryable.cs
diff -N ./beagled/ThunderbirdQueryable/ThunderbirdQueryable.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ ./beagled/ThunderbirdQueryable/ThunderbirdQueryable.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,180 @@
+//
+// ThunderbirdQueryable.cs: This is where all the magic begins!
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Collections;
+using System.Text.RegularExpressions;
+
+using Beagle;
+using Beagle.Util;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	[QueryableFlavor (Name = "Thunderbird", Domain = QueryDomain.Local, RequireInotify = false)]
+	public class ThunderbirdQueryable : LuceneQueryable {
+		private static DateTime indexing_start;
+		private ThunderbirdIndexer indexer;
+		
+		public ThunderbirdQueryable () :
+			base ("ThunderbirdIndex")
+		{
+			// Remove one second from the start time to make sure we don't run into any troubles
+			indexing_start = DateTime.UtcNow.Subtract (new TimeSpan (0, 0, 1));
+			indexer = null;
+			
+			GMime.Global.Init ();
+			
+			if (Environment.GetEnvironmentVariable ("BEAGLE_THUNDERBIRD_DEBUG") != null) {
+				Thunderbird.Debug = true;
+				Logger.Log.Debug ("Running Thunderbird backend in debug mode");
+			}
+		}
+		
+		public override void Start ()
+		{
+			base.Start ();
+			ExceptionHandlingThread.Start (new ThreadStart (StartWorker));
+		}
+		
+		private void StartWorker ()
+		{
+			Logger.Log.Info ("Starting Thunderbird backend");
+			Stopwatch watch = new Stopwatch ();
+			watch.Start ();
+			
+			string root_path = Thunderbird.GetRootPath ();
+			if (!Directory.Exists (root_path)) {
+				GLib.Timeout.Add (60000, new GLib.TimeoutHandler (IndexDataCheck));
+				Logger.Log.Info ("No data available for indexing in {0}", root_path);
+				return;
+			}
+			
+			State = QueryableState.Crawling;
+			indexer = new ThunderbirdIndexer (this, Thunderbird.GetProfilePaths (root_path));
+			indexer.Crawl ();
+			State = QueryableState.Idle;
+			
+			watch.Stop ();
+			Logger.Log.Info ("Thunderbird backend done in {0}s", watch.ElapsedTime);
+		}
+		
+		private bool IndexDataCheck ()
+		{
+			if (!Directory.Exists (Thunderbird.GetRootPath ()))
+				return true;
+				
+			StartWorker ();
+			return false;
+		}
+		
+		// We need this in order to perform custom queries to the lucene database
+		override protected LuceneQueryingDriver BuildLuceneQueryingDriver (string index_name,
+										   int    minor_version,
+										   bool   read_only_mode)
+		{
+			return new LuceneAccess (index_name, minor_version, read_only_mode);
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		public Scheduler.Task NewRemoveTaskByDate (DateTime end_date)
+		{
+			return NewAddTask (new DateIndexableGenerator (Driver, Lucene, end_date));
+		}
+		
+		// The purpose of this IndexableGenerator is to remove mails older than the
+		// specified date (when beagle began to index Thunderbird mails)
+		private class DateIndexableGenerator : IIndexableGenerator {
+			private LuceneQueryingDriver driver;
+			private LuceneAccess lucene;
+			private DateTime end_date;
+			
+			private Uri[] stored_uris;
+			private IEnumerator enumerator;
+			
+			public DateIndexableGenerator (LuceneQueryingDriver driver, LuceneAccess lucene, DateTime end_date)
+			{
+				this.driver = driver;
+				this.lucene = lucene;
+				this.end_date = end_date;
+				this.stored_uris = null;
+			}
+			
+			public Indexable GetNextIndexable ()
+			{
+				return new Indexable (IndexableType.Remove, (Uri) enumerator.Current);
+			}
+			
+			public bool HasNextIndexable ()
+			{
+				if (stored_uris == null) {
+					stored_uris = driver.PropertyQuery (Property.NewKeyword ("fixme:client", "thunderbird"));
+					enumerator = stored_uris.GetEnumerator ();
+				}
+				
+				do {
+					while (enumerator == null || !enumerator.MoveNext ())
+						return false;
+				} while (MatchesDate ((enumerator.Current as Uri)));
+				
+				return true;
+			}
+			
+			private bool MatchesDate (Uri uri)
+			{
+				LuceneAccess.StoredInfo info = lucene.GetStoredInfo (uri);
+				
+				try {
+					if (!info.Equals (end_date) && info.LastIndex.CompareTo (end_date) < 0)
+						return false;
+				} catch (Exception e) {}
+				
+				return true;
+			}
+			
+			public string StatusName {
+				get {
+					return String.Format ("Removing Thunderbird mails past {0}", end_date.ToString ()); 
+				}
+			}
+			
+			public void PostFlushHook () { }
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		public LuceneAccess Lucene {
+			get { return (LuceneAccess) Driver; }
+		}
+		
+		public static DateTime IndexingStart {
+			get { return indexing_start; }
+		}
+	}
+
+}
Index: ./beagled/Makefile.am
===================================================================
RCS file: /cvs/gnome/beagle/beagled/Makefile.am,v
retrieving revision 1.157
diff -u -1 -2 -r1.157 Makefile.am
--- ./beagled/Makefile.am	5 Apr 2006 20:10:07 -0000	1.157
+++ ./beagled/Makefile.am	23 Jun 2006 07:34:37 -0000
@@ -581,25 +581,58 @@
 DAEMON_LOCAL_ASSEMBLIES += 		\
 	BeagleDaemonPlugins.dll		\
 	../images/Images.dll		\
 	../Tiles/Tiles.dll		\
 	$(WEBSERVICES_BACKEND_TARGET) 	\
 	$(WEBSERVICES_FRONTEND_TARGET)	
 
 DAEMON_ASSEMBLIES += -r:System.Runtime.Remoting
 endif
 
 $(DAEMON_TARGET): $(DAEMON_CSFILES) $(DAEMON_LOCAL_ASSEMBLIES)
 	$(CSC) -out:$@ $(DAEMON_CSFLAGS) $(DAEMON_CSFILES) $(DAEMON_ASSEMBLIES)
+if ENABLE_THUNDERBIRD	
+THUNDERBIRD_DIR = ./ThunderbirdQueryable
 
+THUNDERBIRD_TARGET = $(THUNDERBIRD_DIR)/ThunderbirdBackends.dll
+
+THUNDERBIRD_CSFLAGS = -target:library
+
+
+THUNDERBIRD_CSFILES =						\
+	$(THUNDERBIRD_DIR)/LuceneAccess.cs			\
+	$(THUNDERBIRD_DIR)/ThunderbirdIndexer.cs   		\
+	$(THUNDERBIRD_DIR)/ThunderbirdIndexableGenerator.cs	\
+	$(THUNDERBIRD_DIR)/ThunderbirdQueryable.cs		\
+	$(THUNDERBIRD_DIR)/ThunderbirdInotify.cs		\
+	$(THUNDERBIRD_DIR)/Mail.cs				\
+	$(THUNDERBIRD_DIR)/Contact.cs				\
+	$(THUNDERBIRD_DIR)/RSS.cs				\
+	$(THUNDERBIRD_DIR)/NNTP.cs				\
+	$(THUNDERBIRD_DIR)/MoveMail.cs
+
+THUNDERBIRD_LOCAL_ASSEMBLIES =		\
+	../Util/Util.dll		\
+	../BeagleClient/Beagle.dll	\
+	BeagleDaemonLib.dll
+
+THUNDERBIRD_ASSEMBLIES =				\
+	-r:Mono.Posix				\
+	$(BEAGLED_LIBS)                 	\
+	$(THUNDERBIRD_LOCAL_ASSEMBLIES:%=-r:%)
+
+
+$(THUNDERBIRD_TARGET): $(THUNDERBIRD_CSFILES) $(THUNDERBIRD_LOCAL_ASSEMBLIES)
+	$(CSC) -out:$@ $(THUNDERBIRD_CSFLAGS) $(THUNDERBIRD_CSFILES) $(THUNDERBIRD_ASSEMBLIES)
+endif
 ############################################################
 ############################################################
 ############################################################
 
 # This next section is a very good example of why we need real mono support in
 # automake.  This is a massive wart on our development platform.
 
 INDEX_HELPER_WRAPPER = beagled-index-helper
 INDEX_HELPER_WRAPPER_IN = $(INDEX_HELPER_WRAPPER).in
 INDEX_HELPER_TARGET = IndexHelper.exe
 
 $(INDEX_HELPER_WRAPPER): $(srcdir)/$(INDEX_HELPER_WRAPPER_IN)
@@ -966,24 +999,28 @@
 	$(INDEX_WEB_CONTENT_WRAPPER)	\
 	$(EXTRACT_CONTENT_TARGET)	\
 	$(EXTRACT_CONTENT_WRAPPER)	\
 	$(DUMP_INDEX_TARGET)		\
 	$(SNIPPET_TEST_TARGET)		\
 	$(TERM_STATS_TARGET)		\
 	$(NAME_INDEX_TARGET)		\
 	$(MANAGE_INDEX_TARGET)		\
 	$(MANAGE_INDEX_WRAPPER)		\
 	$(BUILD_INDEX_TARGET)		\
 	$(BUILD_INDEX_WRAPPER)
 
+if ENABLE_THUNDERBIRD
+ALL_TARGETS += $(THUNDERBIRD_TARGET)
+endif
+
 if ENABLE_WEBSERVICES
 ALL_TARGETS += 				\
 	$(XSP_TARGET)			\
 	$(WEBSERVICES_FRONTEND_TARGET)	\
 	$(WEBSERVICES_BACKEND_TARGET)	
 endif
 
 if ENABLE_EVOLUTION
 ALL_TARGETS +=				\
 	$(EVO_DLL_TARGET)
 endif
 
@@ -1041,31 +1078,38 @@
 	$(INSTALL_DATA) $(BUILD_INDEX_TARGET).mdb        $(DESTDIR)$(pkglibdir)
 	$(INSTALL_DATA) $(MANAGE_INDEX_TARGET)           $(DESTDIR)$(pkglibdir)
 	$(INSTALL_DATA) $(MANAGE_INDEX_TARGET).mdb       $(DESTDIR)$(pkglibdir)
 if ENABLE_WEBSERVICES
 	$(INSTALL_DATA) $(WEBSERVICES_XSP_TARGET) $(DESTDIR)$(pkglibdir)
 	$(INSTALL_DATA) $(WEBSERVICES_BACKEND_TARGET) $(DESTDIR)$(pkglibdir)
 	$(INSTALL_DATA) $(WEBSERVICES_FRONTEND_TARGET) $(DESTDIR)$(XSPBIN_DIR)
 	$(INSTALL_DATA) $(WEBSERVICES_BACKEND_TARGET) $(DESTDIR)$(XSPBIN_DIR)
 	$(INSTALL_DATA)  $(WEBSERVICES_RESOURCES) $(DESTDIR)$(XSP_DIR)
 #	$(INSTALL_DATA) ../Util/Util.dll ../images/Images.dll ../BeagleClient/Beagle.dll ../Tiles/Tiles.dll BeagleDaemonPlugins.dll BeagleDaemonLib.dll $(DESTDIR)$(XSPBIN_DIR)
 
 endif
-
+if ENABLE_THUNDERBIRD
+	$(INSTALL_DATA) $(THUNDERBIRD_TARGET)                 $(DESTDIR)/$(BACKENDDIR)
+	$(INSTALL_DATA) $(THUNDERBIRD_TARGET).mdb             $(DESTDIR)/$(BACKENDDIR)
+endif
 uninstall-local:
 if ENABLE_WEBSERVICES
 	rm -f $(DESTDIR)$(pkglibdir)/$(WEBSERVICES_BACKEND_TARGET)
 	rm -f $(DESTDIR)$(pkglibdir)/$(WEBSERVICES_XSP_TARGET)
 	rm -rf $(DESTDIR)$(XSP_DIR)
 endif
+if ENABLE_THUNDERBIRD
+	$(INSTALL_DATA) $(THUNDERBIRD_TARGET)                 $(DESTDIR)/$(BACKENDDIR)
+	$(INSTALL_DATA) $(THUNDERBIRD_TARGET).mdb             $(DESTDIR)/$(BACKENDDIR)
+endif
 if ENABLE_EVOLUTION
 	rm -f $(DESTDIR)$(BACKENDDIR)/$(EVO_DLL_TARGET)
 	rm -f $(DESTDIR)$(BACKENDDIR)/$(EVO_DLL_TARGET).mdb
 endif
 
 	@for i in $(BIN_WRAPPERS); do				\
 		echo "rm -f $(DESTDIR)$(bindir)/$$i";		\
 		rm -f $(DESTDIR)$(bindir)/$$i || exit 1;	\
 	done
 
 	@for i in $(SBIN_WRAPPERS); do				\
 		echo "rm -f $(DESTDIR)$(sbindir)/$$i";		\
@@ -1176,14 +1220,15 @@
 	$(FS_SANITY_CHECK_WRAPPER)		\
 	$(MANAGE_INDEX_TARGET)			\
 	$(MANAGE_INDEX_TARGET).mdb		\
 	$(MANAGE_INDEX_WRAPPER)			\
 	$(BUILD_INDEX_TARGET)			\
 	$(BUILD_INDEX_TARGET).mdb		\
 	$(BUILD_INDEX_WRAPPER)			\
 	$(WEBSERVICES_BACKEND_TARGET)		\
 	$(WEBSERVICES_BACKEND_TARGET).mdb	\
 	$(WEBSERVICES_FRONTEND_TARGET)		\
 	$(WEBSERVICES_FRONTEND_TARGET).mdb	\
 	$(WEBSERVICES_XSP_TARGET)		\
-	$(WEBSERVICES_XSP_TARGET).mdb		
-
+	$(WEBSERVICES_XSP_TARGET).mdb		\
+	$(THUNDERBIRD_TARGET)			\
+	$(THUNDERBIRD_TARGET).mdb
Index: ./search/Tiles/MailAttachment.cs
===================================================================
RCS file: /cvs/gnome/beagle/search/Tiles/MailAttachment.cs,v
retrieving revision 1.5
diff -u -1 -2 -r1.5 MailAttachment.cs
--- ./search/Tiles/MailAttachment.cs	20 May 2006 15:53:01 -0000	1.5
+++ ./search/Tiles/MailAttachment.cs	23 Jun 2006 07:34:38 -0000
@@ -13,31 +13,31 @@
 		}
 
 		public override Tile BuildTile (Beagle.Hit hit, Beagle.Query query)
 		{
 			return new MailAttachment (hit, query);
 		}
 
 		public override bool Validate (Beagle.Hit hit)
 		{
 			if (! base.Validate (hit))
 				return false;
 
-			string str = hit ["parent:fixme:hasAttachments"];
-
+			string str = hit.GetFirstProperty ("parent:fixme:hasAttachments");
+			//Console.WriteLine (str);
 			if (hit.ParentUri == null || str == null || str == "false")
 				return false;
 
-			str = hit ["fixme:attachment_title"];
-
+			str = hit.GetFirstProperty ("fixme:attachment_title");
+			//Console.WriteLine (str);
 			if (str == null || str == "")
 				return false;
 
 			Weight += 1;
 			
 			return true;
 		}
 	}
 
 	public class MailAttachment : TileTemplate {
 
 		public MailAttachment (Beagle.Hit hit, Beagle.Query query) : base (hit, query)
@@ -60,34 +60,39 @@
 		{
 			// FIXME: We should handle this case better, but
 			// for now, if we match an attachment, we just want
 			// to display the properties for the parent message.
 			if (!IsAttachment (hit))
 				return hit [name];
 			else
 				return hit ["parent:" + name];
 		}
 
 		public override void Open ()
 		{
-			if (GetHitProperty (Hit, "fixme:client") != "evolution") {
+			SafeProcess p = MailMessage.GetClientProcess (GetHitProperty (Hit, "fixme:client"));
+
+			if (p == null) {
 				OpenFromMime (Hit);
 				return;
 			}
 
-			SafeProcess p = new SafeProcess ();
-			p.Arguments = new string [2];
-			p.Arguments [0] = "evolution";
-
 			if (Hit.ParentUriAsString != null)
-				p.Arguments [1] = Hit.ParentUriAsString;
+				p.Arguments [p.Arguments.Length-1] = Hit.ParentUriAsString;
 			else
-				p.Arguments [1] = Hit.UriAsString;
+				p.Arguments [p.Arguments.Length-1] = Hit.UriAsString;
 
 			try {
 				p.Start ();
 			} catch (SafeProcessException e) {
 				Console.WriteLine ("Unable to run {0}: {1}", p.Arguments [0], e.Message);
+				p.Arguments [0] = "mozilla-thunderbird";
+				try {
+					p.Start();
+				} catch (SafeProcessException e2) {
+					Console.WriteLine ("Unable to run {0}: {1}", p.Arguments [0], e2.Message);
+				}			
 			}
-		}	
+		}
+		
 	}
 }
Index: ./search/Tiles/MailMessage.cs
===================================================================
RCS file: /cvs/gnome/beagle/search/Tiles/MailMessage.cs,v
retrieving revision 1.15
diff -u -1 -2 -r1.15 MailMessage.cs
--- ./search/Tiles/MailMessage.cs	26 May 2006 11:14:31 -0000	1.15
+++ ./search/Tiles/MailMessage.cs	23 Jun 2006 07:34:38 -0000
@@ -1,14 +1,15 @@
 using System;
 using System.Diagnostics;
+using System.IO;
 using Mono.Unix;
 using Beagle.Util;
 
 namespace Search.Tiles {
 
 	public class MailMessageActivator : TileActivator {
 
 		public MailMessageActivator () : base ()
 		{
 			AddSupportedFlavor (new HitFlavor (null, null, "message/rfc822"));
 		}
 
@@ -88,44 +89,69 @@
 
 			string label = sent ? Catalog.GetString ("To:") : Catalog.GetString ("From:");
 			details.AddLabelPair (label, GetAddress (Hit));
 
 			label = sent ? Catalog.GetString ("Date Sent:") : Catalog.GetString ("Date Received:");
 			details.AddLabelPair (label, Utils.NiceLongDate (Timestamp));
 
 			details.AddSnippet ();
 
 			return details;
 		}
 
+		public static SafeProcess GetClientProcess (string client)
+		{
+			SafeProcess p = null;
+
+			if (client == "evolution") {
+				p = new SafeProcess ();
+				p.Arguments = new string [2];
+				p.Arguments [0] = "evolution";
+			} else if (client == "thunderbird") {
+				
+
+				p = new SafeProcess ();
+				p.Arguments = new string [3];
+				p.Arguments [0] = "thunderbird";
+				p.Arguments [1] = "-mail";
+			}
+
+			return p;
+		}
+
 		public override void Open ()
 		{
-			if (Hit.GetFirstProperty ("fixme:client") != "evolution") {
+			SafeProcess p = GetClientProcess (Hit.GetFirstProperty ("fixme:client"));
+
+			if (p == null) {
 				OpenFromMime (Hit);
 				return;
 			}
 
-			SafeProcess p = new SafeProcess ();
-			p.Arguments = new string [2];
-			p.Arguments [0] = "evolution";
 
 			if (Hit.ParentUriAsString != null)
-				p.Arguments [1] = Hit.ParentUriAsString;
+				p.Arguments [p.Arguments.Length-1] = Hit.ParentUriAsString;
 			else
-				p.Arguments [1] = Hit.UriAsString;
+				p.Arguments [p.Arguments.Length-1] = Hit.UriAsString;
 
 			try {
 				p.Start ();
 			} catch (SafeProcessException e) {
 				Console.WriteLine ("Unable to run {0}: {1}", p.Arguments [0], e.Message);
+				p.Arguments [0] = "mozilla-thunderbird";
+				try {
+					p.Start();
+				} catch (SafeProcessException e2) {
+					Console.WriteLine ("Unable to run {0}: {1}", p.Arguments [0], e2.Message);
+				}
 			}
 		}
 
 		public void SendInMail ()
 		{
 			SafeProcess p = new SafeProcess ();
 			p.Arguments = new string [] { "evolution", String.Format ("{0};forward=attached", Hit.Uri) };
 
 			try {
 				p.Start () ;
 			} catch (Exception e) {
 				Console.WriteLine ("Error launching Evolution composer: " + e.Message);
Index: Filters/FilterMail.cs
===================================================================
RCS file: /cvs/gnome/beagle/Filters/FilterMail.cs,v
retrieving revision 1.18
diff -u -1 -2 -r1.18 FilterMail.cs
--- Filters/FilterMail.cs	25 Apr 2006 22:56:04 -0000	1.18
+++ Filters/FilterMail.cs	23 Jun 2006 07:34:37 -0000
@@ -44,25 +44,27 @@
 	[PropertyKeywordMapping (Keyword="mailinglist",  PropertyName="fixme:mlist",        IsKeyword=true, Description="Mailing list id")]
 	public class FilterMail : Beagle.Daemon.Filter, IDisposable {
 
 		private static bool gmime_initialized = false;
 
 		private GMime.Message message;
 		private PartHandler handler;
 
 		public FilterMail ()
 		{
 			// 1: Make email addresses non-keyword, add sanitized version
 			//    for eaching for parts of an email address.
-			SetVersion (1);
+			// 2: Snippet all mail and attachments
+			SetVersion (2);
+			SnippetMode = true;
 
 			AddSupportedFlavor (FilterFlavor.NewFromMimeType ("message/rfc822"));
 		}
 
 		protected override void DoOpen (FileInfo info)
 		{
 			if (!gmime_initialized) {
 				try {
 					GMime.Global.Init ();
 					gmime_initialized = true;
 				} catch {
 					Error ();
@@ -72,26 +74,29 @@
 
 			int mail_fd = Mono.Unix.Native.Syscall.open (info.FullName, Mono.Unix.Native.OpenFlags.O_RDONLY);
 			
 			if (mail_fd == -1)
 				throw new IOException (String.Format ("Unable to read {0} for parsing mail", info.FullName));
 
 			GMime.StreamFs stream = new GMime.StreamFs (mail_fd);
 			GMime.Parser parser = new GMime.Parser (stream);
 			this.message = parser.ConstructMessage ();
 			stream.Dispose ();
 			parser.Dispose ();
 
-			if (this.message == null)
+			if (this.message == null){
+				Console.WriteLine ("NO MESSAGE!");
 				Error ();
+				
+			}
 		}
 
 		private bool HasAttachments (GMime.Object mime_part)
 		{
 			if (mime_part is GMime.MessagePart)
 				return true;
 
 			// Messages that are multipart/alternative shouldn't be considered as having
 			// attachments.  Unless of course they do.
 			if (mime_part is GMime.Multipart && mime_part.ContentType.Subtype.ToLower () != "alternative")
 				return true;
 
@@ -215,24 +220,25 @@
 		}
 
 		private class PartHandler {
 			private Beagle.Daemon.Filter filter;
 			private int count = 0; // parts handled so far
 			private int depth = 0; // part recursion depth
 			private ArrayList child_indexables = new ArrayList ();
 			private TextReader reader;
 
 			public PartHandler (Beagle.Daemon.Filter filter)
 			{
 				this.filter = filter;
+				this.filter.SnippetMode = true;
 			}
 
 			private bool IsMimeTypeHandled (string mime_type)
 			{
 				foreach (FilterFlavor flavor in FilterFlavor.Flavors) {
 					if (flavor.IsMatch (null, null, mime_type.ToLower ()))
 						return true;
 				}
 
 				return false;
 			}
 
@@ -306,25 +312,26 @@
 							no_child_needed = true;
 
 							this.reader = new StreamReader (stream);
 						}
 					}
 
 					if (!no_child_needed) {
 						string sub_uri = this.filter.Uri.ToString () + "#" + this.count;
 						Indexable child = new Indexable (new Uri (sub_uri));
 
 						child.HitType = "MailMessage";
 						child.MimeType = part.ContentType.ToString ();
-						child.CacheContent = false;
+						//child.CacheContent = false;
+					
 
 						child.AddProperty (Property.NewKeyword ("fixme:attachment_title", ((GMime.Part)part).Filename));
 
 						if (part.ContentType.Type.ToLower () == "text")
 							child.SetTextReader (new StreamReader (stream));
 						else
 							child.SetBinaryStream (stream);
 
 						this.child_indexables.Add (child);
 					}
 
 					this.count++;
Index: Util/Makefile.am
===================================================================
RCS file: /cvs/gnome/beagle/Util/Makefile.am,v
retrieving revision 1.94
diff -u -1 -2 -r1.94 Makefile.am
--- Util/Makefile.am	25 Apr 2006 22:56:05 -0000	1.94
+++ Util/Makefile.am	23 Jun 2006 07:34:37 -0000
@@ -105,24 +105,29 @@
 	$(srcdir)/SemWeb/RSquaryFilters.cs		\
 	$(srcdir)/SemWeb/Sparql.cs			\
 	$(srcdir)/SemWeb/SQLStore.cs			\
 	$(srcdir)/SemWeb/Statement.cs			\
 	$(srcdir)/SemWeb/Store.cs			\
 	$(srcdir)/SemWeb/UriMap.cs			\
 	$(srcdir)/SemWeb/Util.cs			\
 	$(srcdir)/SemWeb/XmlParser.cs			\
 	$(srcdir)/SemWeb/XmlWriter.cs			\
 	$(srcdir)/SemWeb/XPathSemWebNavigator.cs	\
 	$(srcdir)/PropertyKeywordAttribute.cs
 
+if ENABLE_THUNDERBIRD
+UTIL_CSFILES +=				\
+	$(srcdir)/Mork.cs		\
+	$(srcdir)/Thunderbird.cs
+endif
 
 if HAS_LIBCHM 
 UTIL_CSFILES +=					\
 	$(srcdir)/ChmFile.cs
 endif
 
 UTIL_ASSEMBLIES =				\
 	$(BEAGLED_LIBS)				\
 	-r:System.Web.Services			\
 	-r:System.Data				\
 	-r:Mono.Data.SqliteClient		\
 	-r:Mono.Posix				\
Index: Util/Mork.cs
===================================================================
RCS file: Util/Mork.cs
diff -N Util/Mork.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ Util/Mork.cs	23 Jun 2006 07:34:37 -0000
@@ -0,0 +1,349 @@
+//
+// Mork.cs: A parser for mork files (used by software such as Firefox and Thunderbird)
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Text;
+using System.Collections;
+using System.Text.RegularExpressions;
+
+namespace Beagle.Util
+{
+	public class MorkDatabase : IEnumerable {
+		protected string mork_file;
+		protected string enum_namespace;
+		
+		protected Hashtable dicts;
+		protected Hashtable metadicts;
+		protected Hashtable rows;
+		protected Hashtable tables;
+		
+		protected string regex_dict = @"(?<id>\w+)\s*=(?<value>(?>[^()]+|\((?<n>)|\) (?<-n>))*)\)(\(|>)";
+		protected string regex_row = @"(?<action>[-!+]?)\[(-|)(?<roid>[0-9A-Za-z:\^]+)(?<cells>(?>[^\[\]]+)?)\]";
+		protected string regex_cell = @"\^(?<key>[0-9A-Fa-f]+)(\^(?<pvalue>[0-9A-Fa-f]+)|=(?<value>[0-9A-Fa-f]+))";
+		protected string regex_table = @"{.*?:(?<ns>[0-9A-Fa-f\^]+) {\(k\^(?<tbl>[0-9A-Fa-f]+):c\)";
+
+		public MorkDatabase (string mork_file)
+		{
+			this.mork_file = mork_file;
+			this.dicts = new Hashtable ();
+			this.metadicts = new Hashtable ();
+			this.rows = new Hashtable ();
+			this.tables = new Hashtable ();
+		}
+		
+		public void Read ()
+		{
+			string content;
+			StreamReader reader;
+			
+			reader = new StreamReader (mork_file);
+			content  = reader.ReadToEnd ();
+			reader.Close ();
+			
+			Reset ();
+			Read (content);
+		}
+
+		protected void Read (string content)
+		{
+			int position = -1;
+
+			while (++position != content.Length) {
+				
+				if (content [position].Equals ('/') && content [position].Equals ('/'))
+					// Ignore comments
+					position = content.IndexOf ('\n');
+				else if (content [position].Equals ('<') && content [position+2].Equals ('<'))
+					// Parse metadict information
+					ParseDict (Read (content, ref position, "<(", ")>"), true);
+				else if (content [position].Equals ('<'))
+					// Parse dict information
+					ParseDict (Read (content, ref position, "<(", ")>"), false);
+				else if (content [position].Equals ('{')) {
+					// Parse table information
+					ParseTable (Read (content, ref position, "{", "}"));
+				 }else if (content [position].Equals ('[')) 
+					// Parse rows
+					ParseRows (Read (content, ref position, "[", "]"), null, null);
+				else if (content [position].Equals ('@') && content [position+1].Equals ('$'))
+					// Parse groups
+					ParseGroups (Read (content, ref position, "@$${", "@$$}"));
+			}
+		}
+		
+		protected string Read (string content, ref int position, string start, string end)
+		{
+			int tmp = position, start_position = position;
+			
+			do {
+				position = content.IndexOf (end, position+1);
+				if ((tmp = content.IndexOf (start, tmp+1)) < 0)
+					break;
+			} while (tmp < position);
+			
+			return content.Substring (start_position, position-start_position+1);
+		}
+		
+		protected virtual void ParseDict (string dict, bool metadict)
+		{
+			Regex reg = new Regex (@"(?<id>[0-9A-Fa-f]+)\s*=(?<value>(.*))", RegexOptions.Compiled);
+			
+			// This is sooo lame that, but it's an easy solution that works
+			foreach (string t in Regex.Replace (dict.Substring (2, dict.Length-3).Replace ("\n", ""), @"\)\s*\(", "\n").Split ('\n')) {
+				
+				Match m = reg.Match (t);
+				
+				if (metadict) 
+					metadicts [m.Result ("${id}")] = m.Result ("${value}");
+				else
+					dicts [m.Result ("${id}")] = Decode (m.Result ("${value}"), "iso-8859-1");;
+			}
+			
+		}
+		
+		protected virtual void ParseTable (string table)
+		{
+			int start = table.IndexOf ('}')+1;
+			Match m = new Regex (regex_table, RegexOptions.Compiled).Match (table);
+			
+			ParseRows (table.Substring (start, table.Length-start-1), m.Result ("${ns}"), m.Result ("${tbl}"));
+		}
+		
+		protected virtual void ParseRows (string rows, string ns, string table)
+		{
+			Regex reg = new Regex (regex_row, RegexOptions.Compiled);
+			
+			foreach (Match m in reg.Matches (Clean (rows))) {
+				// tmp [0] == id, tmp [1] == ns
+				string[] tmp = m.Result ("${roid}").Split (':');
+				
+				if (m.Result ("${action}") == "-" || m.Result ("${cells}") == string.Empty)
+					RemoveRow (tmp [0], (tmp.Length > 1 ? tmp [1] : ns));
+				else
+					AddRow (tmp [0], (tmp.Length > 1 ? tmp [1] : ns), table, m.Result ("${cells}"));
+			}
+		}
+		
+		protected virtual void ParseGroups (string groups)
+		{
+			int start = groups.IndexOf ("{@")+2;
+			
+			Read (groups.Substring (start, groups.Length-start-1));
+		}
+		
+		protected string Clean (string str)
+		{
+			return str.Replace ("\n", "").Replace (" ", "");
+		}
+		
+		public string ParseNamespace (string ns)
+		{
+			if (ns.StartsWith ("^"))
+				return ns;
+			else {
+				foreach (string key in metadicts.Keys)
+					if ((metadicts [key] as string) == ns)
+						return String.Format ("^{0}", key);
+			}
+			
+			return ns;
+		}
+		
+		public void AddRow (string id, string ns, string table, string cols)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			if (id == string.Empty || ns2 == string.Empty || cols == string.Empty)
+				return;
+			else if (!rows.ContainsKey (ns2))
+				rows [ns2] = new Hashtable ();
+			
+			// Add row with a twist... (aka hack)
+			if (!Exists (id, ns2))
+				(rows [ns2] as Hashtable).Add (id, cols);
+			else if (cols.Length >= GetCells (id, ns2).Length)
+				(rows [ns2] as Hashtable) [id] = cols;
+			else
+				(rows [ns2] as Hashtable) [id] = String.Format ("{0}{1}", (rows [ns2] as Hashtable) [id], cols);
+			
+			// Save table association
+			if (!tables.ContainsKey (id))
+				tables [id] = table;
+		}
+		
+		public void RemoveRow (string id, string ns)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			if (!rows.ContainsKey (ns2))
+				return;
+			
+			(rows [ns2] as Hashtable).Remove (id);
+			tables.Remove (id);
+		}
+		
+		public string GetCells (string id, string ns)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			return (ns2 != null ?(rows [ns2] as Hashtable) [id] as string : null);
+		}
+		
+		public Hashtable Compile (string id, string ns)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			if (!Exists (id, ns2))
+				return null;
+			
+			Hashtable tbl = new Hashtable ();
+			Regex reg = new Regex (regex_cell, RegexOptions.Compiled);
+			
+			foreach (Match m in reg.Matches (GetCells (id, ns2))) {
+				tbl [metadicts [m.Result ("${key}")]] = (m.Result ("${pvalue}") != string.Empty ? 
+					dicts [m.Result("${pvalue}")] : m.Result ("${value}"));
+			}
+			
+			tbl ["id"] = id;
+			tbl ["table"] = tables [id];
+			
+			return tbl;
+		}
+
+		public bool Exists (string id, string ns)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			return (ns2 != null ? (rows [ns] as Hashtable).ContainsKey (id) : false);
+		}
+		
+		public int GetRowCount (string ns)
+		{
+			string ns2 = ParseNamespace (ns);
+			
+			if (ns2 == null || rows [ns2] == null)
+				return -1;
+			
+			return (rows [ns2] as Hashtable).Count;
+		}
+		
+		public int GetRowCount (string ns, string table)
+		{
+			int count = 0;
+			string ns2 = ParseNamespace (ns);
+			
+			if (ns2 == null || rows [ns2] == null)
+				return -1;
+			
+			foreach (string id in (rows [ns2] as Hashtable).Keys) {
+				if ((string) tables [id] == table)
+					count++;
+			}
+			
+			return count;
+		}
+		
+		public IEnumerator GetEnumerator ()
+		{
+			string ns = ParseNamespace (EnumNamespace);
+			
+			if (ns == null || (rows [ns] as Hashtable) == null || Empty)
+				return null;
+			
+			return (rows [ns] as Hashtable).Keys.GetEnumerator ();
+		}
+		
+		public void Reset ()
+		{
+			dicts.Clear ();
+			metadicts.Clear ();
+			rows.Clear ();
+			tables.Clear ();
+		}
+		
+		// A _very_ hackish method to convert an UTF-8-morkish-encoded string to another encoding
+		// (see Encoding-property)
+		public static string Decode (string str, string encoding)
+		{
+			int start = 0, old;
+			string decoded = string.Empty;
+			
+			if (str == string.Empty || encoding == string.Empty || str == null || encoding == null)
+				return str;
+			
+			foreach (Match m in Regex.Matches (str, @"\$(?<1>[0-9A-Fa-f]{2})\$(?<2>[0-9A-Fa-f]{2})")) {
+				old = start;
+				start = m.Index+6;
+				
+				byte[] b = new byte [] { 
+					Convert.ToByte (Thunderbird.Hex2Dec (m.Result ("${1}"))), 
+					Convert.ToByte (Thunderbird.Hex2Dec (m.Result ("${2}"))) };
+				
+				byte[] tmp = System.Text.Encoding.Convert (
+					System.Text.Encoding.UTF8, 
+					System.Text.Encoding.GetEncoding (encoding), b);
+				
+				decoded = String.Concat (decoded, 
+					str.Substring (old, start-old-6), 
+					System.Text.Encoding.GetEncoding (encoding).GetString (tmp));
+			}
+			
+			if (start == 0 && decoded.Length == 0)
+				return str;
+			else if (start != decoded.Length)
+				decoded = String.Concat (decoded, str.Substring (start, str.Length-start));
+			
+			return decoded;
+		}
+		
+		public int Rows {
+			get {
+				int count = 0;
+				
+				foreach (Hashtable r in rows.Values)
+					count += r.Count;
+			
+				return count;
+			}
+		}
+		
+		public string EnumNamespace {
+			get { return enum_namespace; }
+			set { enum_namespace = value; }
+		}
+		
+		public string Filename {
+			get { return mork_file; }
+		}
+
+		// There will always exist an item with id 1 in namespace 80, which means
+		// that when there are less than two items in the database, it's empty
+		public bool Empty {
+			get { return (rows.Count > 1 ? false : true); }
+		}
+	}
+}
Index: Util/Thunderbird.cs
===================================================================
RCS file: Util/Thunderbird.cs
diff -N Util/Thunderbird.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ Util/Thunderbird.cs	23 Jun 2006 07:34:37 -0000
@@ -0,0 +1,836 @@
+//
+// Thunderbird.cs: A utility class with methods and classes that might be needed to parse Thunderbird data
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Text;
+using System.Collections;
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+using Beagle;
+using Beagle.Util;
+
+using GMime;
+
+namespace Beagle.Util {
+
+	public class Thunderbird {
+	
+		public static bool Debug = false;
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+	
+		public enum AccountType {
+			Pop3,
+			Imap,
+			Rss,
+			Nntp,
+			AddressBook,
+			MoveMail,
+			Invalid
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		public class Account {
+			private string server_string = null;
+			private string path = null;
+			private int server_port = -1;
+			private AccountType account_type;
+			private char delimiter;
+			
+			public Account (string server, string path, int port, AccountType type, char delim)
+			{
+				this.server_string = server;
+				this.path = path;
+				this.server_port = port;
+				this.account_type = type;
+				this.delimiter = delim;
+			}
+
+			public string Server {
+				get { return server_string; }
+			}
+			
+			public string Path {
+				get { return path; }
+			}
+			
+			public int Port {
+				get { return (server_port > 0 ? server_port : Thunderbird.ParsePort (Type)); }
+			}
+			
+			public AccountType Type {
+				get { return account_type; }
+			}
+			
+			public char Delimiter {
+				get { return (delimiter ==  char.MinValue ? '/' : delimiter); }
+			}
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		public class StorageBase {
+			protected Hashtable data;
+			protected System.Uri uri;
+			protected Account account;
+			
+			public StorageBase ()
+			{
+				data = new Hashtable ();
+			}
+			
+			public string GetString (string key)
+			{
+				return Convert.ToString (data [key]);
+			}
+			
+			public int GetInt (string key)
+			{
+				try {
+					if (!data.ContainsKey (key))
+						return -1;
+					
+					return Convert.ToInt32 (data [key]);
+				} catch (Exception e) {
+					return -1;
+				}
+			}
+			
+			public bool GetBool (string key)
+			{
+				try {
+					return Convert.ToBoolean (data [key]);
+				} catch (Exception e) {
+					return false;
+				}
+			}
+			
+			public object GetObject (string key)
+			{
+				return data [key];
+			}
+			
+			public void SetObject (string key, object value)
+			{
+				data [key] = value;
+			}
+			
+			public System.Uri Uri {
+				get { return uri; }
+				set { uri = value; }
+			}
+			
+			public Account Account {
+				get { return account; }
+			}
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		// String types:
+		// id, sender, subject, recipients, date, mailbox
+		// Integer types:
+		// size, msgOffset, offlineMsgSize
+		// Bool types:
+		// FullIndex
+		public class Mail : StorageBase {
+			private string workfile;
+		
+			public Mail (Account account, Hashtable data, string workfile)
+			{
+				foreach (string key in data.Keys) {
+					if (key == "id")
+						SetObject (key, data [key]);
+					else if (key == "sender")
+						SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
+					else if (key == "subject")
+						SetObject (key, Utils.HeaderDecodeText ((string) data [key]));
+					else if (key == "recipients")
+						SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
+					else if (key == "date")
+						SetObject (key, Thunderbird.HexDateToString ((string) data [key]));
+					else if (key == "size")
+						SetObject (key, Thunderbird.Hex2Dec ((string) data [key]));
+					else if (key == "msgOffset")
+						SetObject (key, Thunderbird.Hex2Dec ((string) data[key]));
+					else if (key == "offlineMsgSize")
+						SetObject (key, Thunderbird.Hex2Dec ((string) data [key]));
+					else if (key == "message-id")
+						SetObject (key, (string) data [key]);
+					else if (key == "references")
+						SetObject (key, (data [key] as string).Replace ("\\", ""));
+				}
+				
+				this.account = account;
+				this.workfile = workfile;
+				SetObject ("mailbox", Thunderbird.ConstructMailboxString (workfile, account));
+				this.uri = Thunderbird.NewUri (Account, GetString ("mailbox"), GetString ("id"));
+			}
+			
+			private GMime.Message ConstructMessage ()
+			{
+				GMime.Message message = null;
+				
+				// Try to fully index this mail by loading the entire mail into memory
+				if (GetBool ("FullIndex"))
+					message = FullMessage ();
+				
+				// Make sure we have the correct status set on this message, in case something went wrong
+				if (message == null || (message != null && message.Stream.Length <= 1)) {
+					SetObject ("FullIndex", (object) false);
+					return PartialMessage ();
+				} else 
+					return message;
+			}
+			
+			private GMime.Message PartialMessage ()
+			{
+				GMime.Message message = new GMime.Message (true);
+
+				message.Subject = GetString ("subject");
+				message.Sender = GetString ("sender");
+				message.MessageId = GetString ("message-id");
+				message.SetDate (DateTime.Parse (GetString ("date")), 0);
+				
+				// Add references
+				if (data.ContainsKey ("references")) {
+					foreach (Match m in Regex.Matches ((data ["references"] as string), @"\<(?>[^\<\>]+)\>"))
+						message.AddHeader ("References", m.Value);
+				}
+				
+				return message;
+			}
+
+			private GMime.Message FullMessage ()
+			{
+				int fd;
+				string file = Thunderbird.GetFullyIndexableFile (workfile);
+				GMime.Message message = null;
+
+				// gmime will go nuts and make the daemon "segmentation fault" in case the file doesn't exist!
+				if (!File.Exists (file))
+					return message;
+
+				try {
+					fd = Mono.Unix.Native.Syscall.open (file, Mono.Unix.Native.OpenFlags.O_RDONLY);
+					StreamFs stream = new StreamFs (fd, Offset, Offset + Size);
+					Parser parser = new Parser (stream);
+					message = parser.ConstructMessage ();
+				
+					stream.Dispose ();
+					parser.Dispose ();
+				} catch (Exception e) {}
+
+				return message;
+			}
+			
+			public int Offset {
+				get {
+					int msg_offset = GetInt ("msgOffset"); 
+					return (msg_offset >= 0 ? msg_offset : Thunderbird.Hex2Dec (GetString ("id"))); 
+				}
+			}
+			
+			public int Size {
+				get {
+					int msg_offline_size = GetInt ("offlineMsgSize");
+					return (msg_offline_size >= 0 ? msg_offline_size : GetInt ("size")); 
+				}
+			}
+
+			public GMime.Message Message {
+				get { return ConstructMessage (); }
+			}
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		// String types:
+		// id, FirstName, LastName, DisplayName, NickName, PrimaryEmail, SecondEmail,
+		// WorkPhone, FaxNumber, HomePhone, PagerNumber, CellularNumber, HomeAddress,
+		// HomeAddress2, HomeCity, HomeState, HomeZipCode,  HomeCountry, WorkAddress,
+		// WorkAddress2, WorkCity, WorkState, WorkZipCode, WorkCountry, JobTitle, Department,
+		// Company, _AimScreenName, FamilyName, WebPage1, WebPage2, BirthYear, BirthMonth
+		// , BirthDay, Custom1, Custom2
+		// Integer types:
+		// None
+		public class Contact : StorageBase {
+			private string workfile;
+		
+			public Contact (Account account, Hashtable data, string workfile)
+			{
+				this.account = account;
+				this.data = data;
+				this.workfile = workfile;
+				this.uri = NewUri (account, GetString ("PrimaryEmail"), GetString ("id"));
+			}
+			
+			public string Workfile {
+				get { return workfile; }
+			}
+		
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		// String types:
+		// id, subject, sender, date, message-id
+		// Integer types:
+		// size
+		public class RssFeed : StorageBase {
+			private string workfile;
+		
+			public RssFeed (Account account, Hashtable data, string workfile)
+			{
+				foreach (string key in data.Keys) {
+					if (key == "id")
+						SetObject (key, data [key]);
+					else if (key == "subject") // title
+						SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
+					else if (key == "sender") // publisher
+						SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
+					else if (key == "date") // date
+						SetObject (key, HexDateToString ((string) data [key]));
+					else if (key == "size") // size
+						SetObject (key, Hex2Dec ((string) data [key]));
+					else if (key == "message-id") { // links
+						string tmp = (string) data [key];
+						SetObject (key, Utils.HeaderDecodePhrase (tmp.Substring (0, tmp.LastIndexOf ("@"))));
+					}
+				}
+				
+				this.account = account;
+				this.workfile = workfile;
+				this.uri = NewUri (account, ConstructMailboxString (workfile, account), GetString ("id"));
+			}
+			
+			// FIXME: Make this a lot faster!
+			private StringReader ConstructContent ()
+			{
+				string content = null;
+				string file = GetFullyIndexableFile (workfile);
+				
+				if (!File.Exists (file))
+					return null;
+				
+				try {
+					StreamReader reader = new StreamReader (file);
+					
+					char[] tmp = new char [GetInt ("size")];
+					reader.BaseStream.Seek (Hex2Dec (GetString ("id")), SeekOrigin.Begin);
+					reader.Read (tmp, 0, tmp.Length);
+					
+					// We don't want to index all HTTP headers, so we cut 'em off
+					content = new string (tmp);
+					content = content.Substring (content.IndexOf ("<html>"));
+					
+					reader.Close ();
+				} catch (Exception e) { }
+				
+				return (content != null ? new StringReader (content) : null);
+			}
+			
+			public string Workfile {
+				get { return workfile; }
+			}
+			
+			public StringReader Content {
+				get { return ConstructContent (); }
+			}
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		// String types:
+		// id, subject, sender, date
+		// Integer types:
+		// size
+		// An NNTP message resambles a mail so very much...
+		public class NntpMessage : Mail {
+		
+			public NntpMessage (Account account, Hashtable data, string workfile)
+				: base (account, data, workfile)
+			{
+				foreach (string key in data.Keys) {
+					if (key == "id")
+						SetObject (key,  data [key]);
+					else if (key == "subject")
+						SetObject (key, Utils.HeaderDecodeText ((string) data [key]));
+					else if (key == "sender")
+						SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
+					else if (key == "date")
+						SetObject (key, Thunderbird.HexDateToString ((string) data [key]));
+					else if (key == "size")
+						SetObject (key, Thunderbird.Hex2Dec ((string) data [key]));
+				}
+				
+				Uri = NewUri (account, ConstructMailboxString (workfile, account), GetString ("id"));
+			}
+		
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		// Still just a stub, will be fixed later on
+		public class MoveMail : StorageBase {
+		
+			public MoveMail (Account account, Hashtable data, string workfile)
+			{
+				this.account = account;
+				this.data = data;
+				//this.workfile = workfile;
+				this.uri = NewUri (account, GetString ("tmp"), GetString ("id"));
+			}
+		
+		}
+
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		public class Database : IEnumerable {
+			private static MorkDatabase db;
+			private Account account;
+			private string file;
+			
+			private IEnumerator current = null;
+
+			public Database (Account account, string file)
+			{
+				this.account = account;
+				this.file = file;
+			}
+			
+			public void Load ()
+			{
+				db = new MorkDatabase (file);
+				db.Read();
+				
+				switch (account.Type) {
+				case AccountType.Pop3:
+				case AccountType.Imap:
+				case AccountType.Rss:
+				case AccountType.Nntp:
+				case AccountType.MoveMail:
+					db.EnumNamespace = "ns:msg:db:row:scope:msgs:all";
+					break;
+				case AccountType.AddressBook:
+					db.EnumNamespace = "ns:addrbk:db:row:scope:card:all";
+					break;
+				}
+				
+				current = db.GetEnumerator ();
+			}
+			
+			public Account Account {
+				get { return account; }
+			}
+			
+			public int Count {
+				get {
+					if (db == null)
+						return 0;
+						
+					return (account.Type == AccountType.AddressBook ? 
+						db.GetRowCount ("ns:addrbk:db:row:scope:card:all", "BF") : 
+						db.GetRowCount ("ns:msg:db:row:scope:msgs:all"));
+				}
+			}
+			
+			public string Filename {
+				get { return (db != null ? db.Filename : string.Empty); }
+			}
+			
+			public MorkDatabase Db {
+				get { return db; }
+			}
+			
+			public IEnumerator GetEnumerator ()
+			{
+				return new DatabaseEnumerator (db, account, current);
+			}
+			
+			public class DatabaseEnumerator : IEnumerator {
+				private MorkDatabase db;
+				private Account account;
+				private IEnumerator enumerator;
+
+				public DatabaseEnumerator (MorkDatabase db, Account account, IEnumerator enumerator)
+				{
+					this.db = db;
+					this.enumerator = enumerator;
+					this.account = account;
+				}
+				
+				public bool MoveNext ()
+				{
+					return (enumerator != null ? enumerator.MoveNext () : false);
+				}
+				
+				public void Reset ()
+				{
+					enumerator.Reset ();
+				}
+				
+				public object Current {
+					get { 
+						switch (account.Type) {
+						case AccountType.Pop3:
+						case AccountType.Imap:
+							return new Mail (account, db.Compile ((string) enumerator.Current, 
+								"ns:msg:db:row:scope:msgs:all"), db.Filename); 
+						case AccountType.AddressBook:
+							return new Contact (account, db.Compile ((string) enumerator.Current,
+								"ns:addrbk:db:row:scope:card:all"), db.Filename);
+						case AccountType.Rss:
+							return new RssFeed (account, db.Compile ((string) enumerator.Current, 
+								"ns:msg:db:row:scope:msgs:all"), db.Filename);
+						case AccountType.Nntp:
+							return new NntpMessage (account, db.Compile ((string) enumerator.Current, 
+								"ns:msg:db:row:scope:msgs:all"), db.Filename);
+						case AccountType.MoveMail:
+							return new MoveMail (account, db.Compile ((string) enumerator.Current,
+								"ns:msg:db:row:scope:msgs:all"), db.Filename);
+						}
+						
+						return null;
+					}
+				}
+			}
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+
+		public static string HexDateToString (string hex)
+		{
+			DateTime time = new DateTime (1970,1,1,0,0,0);
+
+			try {
+				time = time.AddSeconds (
+					Int32.Parse (hex, NumberStyles.HexNumber));
+			} catch (Exception e) { }
+			
+			return time.ToString ();
+		}
+		
+		public static int Hex2Dec (string hex)
+		{
+			int dec = -1;
+			
+			try {
+				dec = Convert.ToInt32 (hex, 16);
+			} catch (Exception e) { }
+			
+			return dec;
+		}
+		
+		public static int ParsePort (AccountType type)
+		{
+			int port = 0;
+			
+			switch (type) {
+			case AccountType.Pop3:
+				port = 110;
+				break;
+			case AccountType.Imap:
+				port = 143;
+				break;
+			}
+			
+			return port;
+		}
+		
+		public static AccountType ParseAccountType (string type_str)
+		{
+			AccountType type;
+
+			switch (type_str.ToLower ()) {
+			case "pop3":
+				type = AccountType.Pop3;
+				break;
+			case "imap":
+				type = AccountType.Imap;
+				break;
+			case "rss":
+				type = AccountType.Rss;
+				break;
+			case "nntp":
+				type= AccountType.Nntp;
+				break;
+			case "addressbook":
+				type = AccountType.AddressBook;
+				break;
+			case "movemail":
+				type = AccountType.MoveMail;
+				break;
+			default:
+				type = AccountType.Invalid;
+				break;
+			}
+			
+			return type;
+		}
+		
+		// A hack to extract a potential delimiter from a namespace-string
+		public static char GetDelimiter (string[] namespace_str)
+		{
+			MatchCollection matches = null;
+			Regex reg = new Regex (@"\\\""(.*)(?<delimiter>[^,])\\\""", RegexOptions.Compiled);
+			
+			if (namespace_str == null)
+				return char.MinValue;
+			
+			foreach (string str in namespace_str) {
+				try {
+					matches = reg.Matches (str);
+				} catch (Exception e) {
+					continue;
+				}
+
+				foreach (Match m in matches) {
+					char delim = Convert.ToChar (m.Result ("${delimiter}"));
+					if (delim != ' ')
+						return delim;
+				}
+			}
+			
+			return char.MinValue;
+		}
+		
+		public static Uri NewUri (Account account, string mailbox, string id)
+		{
+			Uri uri = null;
+
+			switch (account.Type) {
+				case AccountType.Pop3:
+				case AccountType.MoveMail:
+				case AccountType.Rss: // rss, movemail and pop3 share the same uri scheme
+					uri = new Uri (String.Format ("mailbox://{0}/{1}?number={2}", 
+						account.Path, mailbox, Convert.ToInt32 (id, 16))); 
+					break;
+				case AccountType.Imap:
+					uri = new Uri (String.Format ("imap://{0}:{1}/fetch%3EUID%3E{2}%3E{3}",
+						account.Server, account.Port, mailbox, Convert.ToInt32 (id, 16)));
+					break;
+				case AccountType.AddressBook:
+					uri = new Uri (String.Format ("mailto:{0}",mailbox));
+					break;
+				case AccountType.Nntp:
+					uri = new Uri (String.Format ( "news://{0}:{1}/{2}?number={3}" , 
+						new string[] {account.Server, account.Port.ToString(), mailbox, id} ));
+					break;
+				case AccountType.Invalid:
+					break;
+			}
+
+			return uri;
+		}
+		
+		public static  string GetFullyIndexableFile (string mork_file)
+		{
+			string mailbox_file = Path.Combine (
+				Path.GetDirectoryName (mork_file), 
+				Path.GetFileNameWithoutExtension (mork_file));
+			
+			return mailbox_file;
+		}
+		
+		// a generic way to determine where thunderbird is storing it's files
+		public static string GetRootPath ()
+		{
+			foreach (string dir in Directory.GetDirectories (PathFinder.HomeDir, ".*thunderbird*")) {
+				if (File.Exists (Path.Combine (dir, "profiles.ini")))
+					return dir;
+			}
+			
+			return null;
+		}
+		
+		public static string[] GetProfilePaths (string root)
+		{
+			string line;
+			StreamReader reader;
+			ArrayList profiles = new ArrayList ();
+			
+			try {
+				reader = new StreamReader (Path.Combine (root, "profiles.ini"));
+			} catch (Exception e) { 
+				return (string[]) profiles.ToArray ();
+			}
+			
+			// Read the profile path
+			while ((line = reader.ReadLine ()) != null) {
+				if (line.StartsWith ("Path=")) {
+					profiles.Add (String.Format ("{0}/{1}", root, line.Substring (5)));
+					continue;
+				}
+			}
+
+			return (string[]) profiles.ToArray (typeof (string));
+		}
+		
+		public static string GetRelativePath (string mork_file)
+		{
+			string path = null;
+			foreach (string root in Thunderbird.GetProfilePaths (Thunderbird.GetRootPath ())) {
+				if (!mork_file.StartsWith (root))
+					continue;
+
+				path = mork_file.Substring (root.Length+1);
+				break;
+			}
+			
+			return path;
+		}
+		
+		public static ArrayList ReadAccounts (string profile_dir)
+		{
+			string line = null;
+			Queue accounts = new Queue();
+			Hashtable tbl = new Hashtable ();
+			ArrayList account_list = new ArrayList ();
+			StreamReader reader;
+			Regex id_reg = new Regex (@"account.account(?<id>\d).server");
+			Regex reg = new Regex (@"user_pref\(""mail\.(?<key>.*)""\s*,\s*(""(?<value>.*)"" | (?<value>.*))\);",
+				RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
+
+			try {
+				reader = new StreamReader (Path.Combine (profile_dir, "prefs.js"));
+			} catch (Exception e) {
+				if (Debug)
+					Logger.Log.Debug ("Failed to open file {0}: {1}", Path.Combine (profile_dir , "prefs.js"), e.Message);
+
+				return account_list;
+			}
+
+			while ((line = reader.ReadLine()) != null) {
+				if (!line.StartsWith ("user_pref(\"mail."))
+					continue;
+
+				try {
+					string key = reg.Match (line).Result ("${key}");
+
+					if (key.StartsWith ("account.account")) {
+						if (Debug)
+							Logger.Log.Debug ("account.account: {0}", id_reg.Match (key).Result ("${id}"));
+
+						accounts.Enqueue (id_reg.Match (key).Result ("${id}"));
+					}
+
+					tbl [key] = reg.Match (line).Result ("${value}");
+				} catch (Exception e) { 
+					if (Debug)
+						Logger.Log.Debug ("ReadAccounts 1: {0}", e.Message);				
+				}
+			}
+			
+			if (Debug)
+				Logger.Log.Info ("ReadAccounts: {0} accounts", accounts.Count);
+
+			while (accounts.Count > 0) {
+				string id = "server.server" + (accounts.Dequeue() as string);
+				AccountType type = ParseAccountType ((string) tbl [id + ".type"]);
+				char delimiter = GetDelimiter (new string [] { (string) tbl [id + ".namespace.personal"], 
+					(string) tbl [id + ".namespace.public"], (string) tbl [id + ".namespace.other_users"]});
+				
+				if (type == AccountType.Invalid)
+					continue;
+				
+				if (Debug)
+					Logger.Log.Debug ("ReadAccounts 2: {0}", id);
+
+				try {
+					account_list.Add (new Account (
+						String.Format ("{0}@{1}", (string) tbl [id + ".userName"], (string) tbl [id + ".hostname"]), 
+						(string) tbl [id + ".directory"], Convert.ToInt32 ((string) tbl [id + ".port"]), type, delimiter));
+				} catch (Exception e) {
+					if (Debug)
+						Logger.Log.Debug ("ReadAccounts 3: {0}", e.Message);
+					continue;
+				}
+			}
+			
+			// In case the address book file exists, add it as well
+			if (File.Exists (Path.Combine (profile_dir, "abook.mab"))) {
+				account_list.Add (new Account (Path.GetFileName (profile_dir), 
+					Path.Combine (profile_dir, "abook.mab"), 0, AccountType.AddressBook, ' '));
+			}
+
+			return account_list;
+		}
+		
+		public static bool IsMorkFile (string path, string filename)
+		{
+			string full_path = Path.Combine (path, filename);
+			
+			if (Path.GetExtension (filename) == ".msf" && File.Exists (full_path))
+				return true;
+		
+			return false;
+		}
+		
+		public static bool IsFullyIndexable (string mork_file)
+		{
+			try {
+				FileInfo file_info = new FileInfo (GetFullyIndexableFile (mork_file));
+				if (file_info.Length > 0)
+					return true;
+			} catch (Exception e) {}
+			
+			return false;
+		}
+		
+		public static string ConstructMailboxString (string mork_file, Account account)
+		{
+			string mailbox = null;
+
+			switch (account.Type) {
+			case AccountType.Pop3:
+			case AccountType.Rss:
+			case AccountType.MoveMail:
+				mailbox = GetFullyIndexableFile (mork_file.Substring (account.Path.Length+1));
+				break;
+			case AccountType.Imap:
+				mailbox = mork_file.Substring (account.Path.Length+1);
+				mailbox = account.Delimiter + GetFullyIndexableFile (mailbox).Replace 
+					(".sbd/", Convert.ToString (account.Delimiter));
+				break;
+			case AccountType.AddressBook:
+			case AccountType.Nntp:
+				// Doesn't really matter what this is as long as it's unique (at least until I've figure the uri schemes)
+				mailbox = account.Server;
+				break;
+			case AccountType.Invalid:
+				mailbox = String.Format ("InvalidMailbox-{0}", mork_file);
+				break;
+			}
+
+			return mailbox;
+		}
+	}
+
+}
Index: beagled/Makefile.am
===================================================================
RCS file: /cvs/gnome/beagle/beagled/Makefile.am,v
retrieving revision 1.157
diff -u -1 -2 -r1.157 Makefile.am
--- beagled/Makefile.am	5 Apr 2006 20:10:07 -0000	1.157
+++ beagled/Makefile.am	23 Jun 2006 07:34:37 -0000
@@ -581,25 +581,58 @@
 DAEMON_LOCAL_ASSEMBLIES += 		\
 	BeagleDaemonPlugins.dll		\
 	../images/Images.dll		\
 	../Tiles/Tiles.dll		\
 	$(WEBSERVICES_BACKEND_TARGET) 	\
 	$(WEBSERVICES_FRONTEND_TARGET)	
 
 DAEMON_ASSEMBLIES += -r:System.Runtime.Remoting
 endif
 
 $(DAEMON_TARGET): $(DAEMON_CSFILES) $(DAEMON_LOCAL_ASSEMBLIES)
 	$(CSC) -out:$@ $(DAEMON_CSFLAGS) $(DAEMON_CSFILES) $(DAEMON_ASSEMBLIES)
+if ENABLE_THUNDERBIRD	
+THUNDERBIRD_DIR = ./ThunderbirdQueryable
 
+THUNDERBIRD_TARGET = $(THUNDERBIRD_DIR)/ThunderbirdBackends.dll
+
+THUNDERBIRD_CSFLAGS = -target:library
+
+
+THUNDERBIRD_CSFILES =						\
+	$(THUNDERBIRD_DIR)/LuceneAccess.cs			\
+	$(THUNDERBIRD_DIR)/ThunderbirdIndexer.cs   		\
+	$(THUNDERBIRD_DIR)/ThunderbirdIndexableGenerator.cs	\
+	$(THUNDERBIRD_DIR)/ThunderbirdQueryable.cs		\
+	$(THUNDERBIRD_DIR)/ThunderbirdInotify.cs		\
+	$(THUNDERBIRD_DIR)/Mail.cs				\
+	$(THUNDERBIRD_DIR)/Contact.cs				\
+	$(THUNDERBIRD_DIR)/RSS.cs				\
+	$(THUNDERBIRD_DIR)/NNTP.cs				\
+	$(THUNDERBIRD_DIR)/MoveMail.cs
+
+THUNDERBIRD_LOCAL_ASSEMBLIES =		\
+	../Util/Util.dll		\
+	../BeagleClient/Beagle.dll	\
+	BeagleDaemonLib.dll
+
+THUNDERBIRD_ASSEMBLIES =				\
+	-r:Mono.Posix				\
+	$(BEAGLED_LIBS)                 	\
+	$(THUNDERBIRD_LOCAL_ASSEMBLIES:%=-r:%)
+
+
+$(THUNDERBIRD_TARGET): $(THUNDERBIRD_CSFILES) $(THUNDERBIRD_LOCAL_ASSEMBLIES)
+	$(CSC) -out:$@ $(THUNDERBIRD_CSFLAGS) $(THUNDERBIRD_CSFILES) $(THUNDERBIRD_ASSEMBLIES)
+endif
 ############################################################
 ############################################################
 ############################################################
 
 # This next section is a very good example of why we need real mono support in
 # automake.  This is a massive wart on our development platform.
 
 INDEX_HELPER_WRAPPER = beagled-index-helper
 INDEX_HELPER_WRAPPER_IN = $(INDEX_HELPER_WRAPPER).in
 INDEX_HELPER_TARGET = IndexHelper.exe
 
 $(INDEX_HELPER_WRAPPER): $(srcdir)/$(INDEX_HELPER_WRAPPER_IN)
@@ -966,24 +999,28 @@
 	$(INDEX_WEB_CONTENT_WRAPPER)	\
 	$(EXTRACT_CONTENT_TARGET)	\
 	$(EXTRACT_CONTENT_WRAPPER)	\
 	$(DUMP_INDEX_TARGET)		\
 	$(SNIPPET_TEST_TARGET)		\
 	$(TERM_STATS_TARGET)		\
 	$(NAME_INDEX_TARGET)		\
 	$(MANAGE_INDEX_TARGET)		\
 	$(MANAGE_INDEX_WRAPPER)		\
 	$(BUILD_INDEX_TARGET)		\
 	$(BUILD_INDEX_WRAPPER)
 
+if ENABLE_THUNDERBIRD
+ALL_TARGETS += $(THUNDERBIRD_TARGET)
+endif
+
 if ENABLE_WEBSERVICES
 ALL_TARGETS += 				\
 	$(XSP_TARGET)			\
 	$(WEBSERVICES_FRONTEND_TARGET)	\
 	$(WEBSERVICES_BACKEND_TARGET)	
 endif
 
 if ENABLE_EVOLUTION
 ALL_TARGETS +=				\
 	$(EVO_DLL_TARGET)
 endif
 
@@ -1041,31 +1078,38 @@
 	$(INSTALL_DATA) $(BUILD_INDEX_TARGET).mdb        $(DESTDIR)$(pkglibdir)
 	$(INSTALL_DATA) $(MANAGE_INDEX_TARGET)           $(DESTDIR)$(pkglibdir)
 	$(INSTALL_DATA) $(MANAGE_INDEX_TARGET).mdb       $(DESTDIR)$(pkglibdir)
 if ENABLE_WEBSERVICES
 	$(INSTALL_DATA) $(WEBSERVICES_XSP_TARGET) $(DESTDIR)$(pkglibdir)
 	$(INSTALL_DATA) $(WEBSERVICES_BACKEND_TARGET) $(DESTDIR)$(pkglibdir)
 	$(INSTALL_DATA) $(WEBSERVICES_FRONTEND_TARGET) $(DESTDIR)$(XSPBIN_DIR)
 	$(INSTALL_DATA) $(WEBSERVICES_BACKEND_TARGET) $(DESTDIR)$(XSPBIN_DIR)
 	$(INSTALL_DATA)  $(WEBSERVICES_RESOURCES) $(DESTDIR)$(XSP_DIR)
 #	$(INSTALL_DATA) ../Util/Util.dll ../images/Images.dll ../BeagleClient/Beagle.dll ../Tiles/Tiles.dll BeagleDaemonPlugins.dll BeagleDaemonLib.dll $(DESTDIR)$(XSPBIN_DIR)
 
 endif
-
+if ENABLE_THUNDERBIRD
+	$(INSTALL_DATA) $(THUNDERBIRD_TARGET)                 $(DESTDIR)/$(BACKENDDIR)
+	$(INSTALL_DATA) $(THUNDERBIRD_TARGET).mdb             $(DESTDIR)/$(BACKENDDIR)
+endif
 uninstall-local:
 if ENABLE_WEBSERVICES
 	rm -f $(DESTDIR)$(pkglibdir)/$(WEBSERVICES_BACKEND_TARGET)
 	rm -f $(DESTDIR)$(pkglibdir)/$(WEBSERVICES_XSP_TARGET)
 	rm -rf $(DESTDIR)$(XSP_DIR)
 endif
+if ENABLE_THUNDERBIRD
+	$(INSTALL_DATA) $(THUNDERBIRD_TARGET)                 $(DESTDIR)/$(BACKENDDIR)
+	$(INSTALL_DATA) $(THUNDERBIRD_TARGET).mdb             $(DESTDIR)/$(BACKENDDIR)
+endif
 if ENABLE_EVOLUTION
 	rm -f $(DESTDIR)$(BACKENDDIR)/$(EVO_DLL_TARGET)
 	rm -f $(DESTDIR)$(BACKENDDIR)/$(EVO_DLL_TARGET).mdb
 endif
 
 	@for i in $(BIN_WRAPPERS); do				\
 		echo "rm -f $(DESTDIR)$(bindir)/$$i";		\
 		rm -f $(DESTDIR)$(bindir)/$$i || exit 1;	\
 	done
 
 	@for i in $(SBIN_WRAPPERS); do				\
 		echo "rm -f $(DESTDIR)$(sbindir)/$$i";		\
@@ -1176,14 +1220,15 @@
 	$(FS_SANITY_CHECK_WRAPPER)		\
 	$(MANAGE_INDEX_TARGET)			\
 	$(MANAGE_INDEX_TARGET).mdb		\
 	$(MANAGE_INDEX_WRAPPER)			\
 	$(BUILD_INDEX_TARGET)			\
 	$(BUILD_INDEX_TARGET).mdb		\
 	$(BUILD_INDEX_WRAPPER)			\
 	$(WEBSERVICES_BACKEND_TARGET)		\
 	$(WEBSERVICES_BACKEND_TARGET).mdb	\
 	$(WEBSERVICES_FRONTEND_TARGET)		\
 	$(WEBSERVICES_FRONTEND_TARGET).mdb	\
 	$(WEBSERVICES_XSP_TARGET)		\
-	$(WEBSERVICES_XSP_TARGET).mdb		
-
+	$(WEBSERVICES_XSP_TARGET).mdb		\
+	$(THUNDERBIRD_TARGET)			\
+	$(THUNDERBIRD_TARGET).mdb
Index: beagled/ThunderbirdQueryable/Contact.cs
===================================================================
RCS file: beagled/ThunderbirdQueryable/Contact.cs
diff -N beagled/ThunderbirdQueryable/Contact.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ beagled/ThunderbirdQueryable/Contact.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,147 @@
+//
+// Contact.cs: Adds address book indexing support to the Thunderbird backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+using GMime;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {	
+	
+	[ThunderbirdIndexableGenerator (TB.AccountType.AddressBook, "Address book support", true)]
+	public class ContactIndexableGenerator : ThunderbirdIndexableGenerator {
+
+		public ContactIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string abook_file)
+			: base (indexer, account, abook_file)
+		{
+		}
+		
+		public override bool HasNextIndexable ()
+		{
+			do {
+				if (DbEnumerator == null || !DbEnumerator.MoveNext ()) {
+					Done = true;
+					indexer.NotificationEvent -= OnNotification;
+					indexer.ChildComplete ();
+					return false;
+				}
+			} while ((DbEnumerator.Current as TB.Contact).GetString ("table") != "BF" ||
+				IsUpToDate ((DbEnumerator.Current as TB.Contact).Uri));
+			
+			return true;
+		}
+		
+		public override Indexable GetNextIndexable ()
+		{
+			return ContactToIndexable (DbEnumerator.Current as TB.Contact);
+		}
+		
+		public override void LoadDatabase ()
+		{
+			try {
+				db = new TB.Database (account, DbFile);
+				db.Load ();
+			} catch (Exception e) {
+				Logger.Log.Debug ("Failed to load {0}: {1}", DbFile, e.Message);
+				return;
+			}
+			
+			if (db.Count <= 0)
+				return;
+			
+			ResetEnumerator ();
+			Logger.Log.Info ("Indexing address book containing {0} contact(s) ({1})", db.Count, RelativePath);
+		}
+
+		private Indexable ContactToIndexable (TB.Contact contact)
+		{
+			Indexable indexable = NewIndexable (contact.Uri, DateTime.Now.ToUniversalTime (), "Contact");
+			
+			indexable.AddProperty (Property.New ("fixme:FirstName", contact.GetString ("FirstName")));
+			indexable.AddProperty (Property.New ("fixme:LastName", contact.GetString ("LastName")));
+			indexable.AddProperty (Property.New ("fixme:DisplayName", contact.GetString ("LastName")));
+			indexable.AddProperty (Property.New ("fixme:NickName", contact.GetString ("NickName")));
+			indexable.AddProperty (Property.NewKeyword ("fixme:PrimaryEmail", contact.GetString ("PrimaryEmail")));
+			indexable.AddProperty (Property.NewKeyword ("fixme:SecondEmail", contact.GetString ("SecondEmail")));
+			indexable.AddProperty (Property.New ("fixme:WorkPhone", contact.GetString ("WorkPhone")));
+			indexable.AddProperty (Property.New ("fixme:FaxNumber", contact.GetString ("FaxNumber")));
+			indexable.AddProperty (Property.New ("fixme:HomePhone", contact.GetString ("HomePhone")));
+			indexable.AddProperty (Property.New ("fixme:PagerNumber", contact.GetString ("PagerNumber")));
+			indexable.AddProperty (Property.New ("fixme:CellularNumber", contact.GetString ("CellularNumber")));
+			indexable.AddProperty (Property.New ("fixme:HomeAddress", contact.GetString ("HomeAddress")));
+			indexable.AddProperty (Property.New ("fixme:HomeAddress2", contact.GetString ("HomeAddress2")));
+			indexable.AddProperty (Property.New ("fixme:HomeCity", contact.GetString ("HomeCity")));
+			indexable.AddProperty (Property.New ("fixme:HomeState", contact.GetString ("HomeState")));
+			indexable.AddProperty (Property.New ("fixme:HomeZipCode", contact.GetString("HomeZipCode")));
+			indexable.AddProperty (Property.New ("fixme:HomeCountry", contact.GetString ("HomeCountry")));
+			indexable.AddProperty (Property.New ("fixme:WorkAddress", contact.GetString ("WorkAddress")));
+			indexable.AddProperty (Property.New ("fixme:WorkAddress2", contact.GetString ("WorkAddress2")));
+			indexable.AddProperty (Property.New ("fixme:WorkCity", contact.GetString ("WorkCity")));
+			indexable.AddProperty (Property.New ("fixme:WorkState", contact.GetString ("WorkState")));
+			indexable.AddProperty (Property.New ("fixme:WorkZipCode", contact.GetString ("WorkZipCode")));
+			indexable.AddProperty (Property.New ("fixme:WorkCountry", contact.GetString ("WorkCountry")));
+			indexable.AddProperty (Property.New ("fixme:JobTitle", contact.GetString ("JobTitle")));
+			indexable.AddProperty (Property.New ("fixme:Department", contact.GetString ("Department")));
+			indexable.AddProperty (Property.New ("fixme:Company", contact.GetString ("Company")));
+			indexable.AddProperty (Property.New ("fixme:_AimScreenName", contact.GetString ("_AimScreenName")));
+			indexable.AddProperty (Property.New ("fixme:FamilyName", contact.GetString ("FamilyName")));
+			indexable.AddProperty (Property.NewKeyword ("fixme:WebPage1", contact.GetString ("WebPage1")));
+			indexable.AddProperty (Property.NewKeyword ("fixme:WebPage2", contact.GetString ("WebPage2")));
+			indexable.AddProperty (Property.New ("fixme:BirthYear", contact.GetString ("BirthYear")));
+			indexable.AddProperty (Property.New ("fixme:BirthMonth", contact.GetString ("BirthMonth")));
+			indexable.AddProperty (Property.New ("fixme:BirthDay", contact.GetString ("BirthDay")));
+			indexable.AddProperty (Property.New ("fixme:Custom1", contact.GetString ("Custom1")));
+			indexable.AddProperty (Property.New ("fixme:Custom2", contact.GetString ("Custom2")));
+			
+			indexable.AddProperty (Property.NewKeyword ("fixme:Email", contact.GetString ("PrimaryEmail")));
+			indexable.AddProperty (Property.New ("fixme:Name", contact.GetString ("DisplayName")));
+			
+			return indexable;
+		}
+		
+		// Why? Because it's very likely that the user will sometimes change contact details. Current IsUpToDate
+		// (in ThunderbirdIndexableGenerator-class) only checks the "fullyIndexed" property and when the contact
+		// was indexed, thus if the user changes an email address it won't be updated until beagle is restarted.
+		// By always returning false here, we make sure that beagle always re-index contacts when something
+		// happens. It's a really fast and not a very cpu intensive task, so it doesn't really matter.
+		protected new bool IsUpToDate (Uri uri)
+		{
+			// Remove this uri from the cache
+			if (stored_cache != null)
+				stored_cache.Remove (uri.ToString ());
+			
+			return false;
+		}
+	}
+}
+	
Index: beagled/ThunderbirdQueryable/LuceneAccess.cs
===================================================================
RCS file: beagled/ThunderbirdQueryable/LuceneAccess.cs
diff -N beagled/ThunderbirdQueryable/LuceneAccess.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ beagled/ThunderbirdQueryable/LuceneAccess.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,136 @@
+//
+// LuceneAccess.cs: Provides low level access to the underlying Lucene database
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.Collections;
+
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using LNS = Lucene.Net.Search;
+
+using Beagle.Daemon;
+using Beagle.Util;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	public class LuceneAccess : LuceneQueryingDriver {
+		public class StoredInfo 
+		{
+			public DateTime LastIndex;
+			public bool FullyIndexed;
+			public Uri Uri;
+		}
+		
+		public LuceneAccess (string index_name, int minor_version, bool read_only)
+			: base (index_name, minor_version, read_only)
+		{
+		
+		}
+		
+		public StoredInfo DocumentToStoredInfo (Document doc)
+		{
+			int count = 0;
+			StoredInfo info = new StoredInfo ();
+
+			info.Uri = GetUriFromDocument (doc);
+
+			foreach (Field f in doc.Fields ()) {
+				Property prop = GetPropertyFromDocument (f, doc, false);
+				if (prop == null)
+					continue;
+
+				switch (prop.Key) {
+				case "fixme:indexDateTime":
+					info.LastIndex = StringFu.StringToDateTime (prop.Value);
+					count++;
+					break;
+				case "fixme:fullyIndexed":
+					info.FullyIndexed = Convert.ToBoolean (prop.Value);
+					count++;
+					break;
+				}
+				
+				if (count == 2)
+					break;
+			}
+
+			return info;
+		}
+		
+		private class SingletonCollector : LNS.HitCollector
+		{
+			public int MatchId = -1;
+			
+			public override void Collect (int id, float score)
+			{
+				MatchId = id;
+			}
+		}
+		
+		public StoredInfo GetStoredInfo (Uri uri)
+		{
+			StoredInfo info = new StoredInfo ();
+
+			LNS.Query query = UriQuery ("Uri", uri);
+			SingletonCollector collector = new SingletonCollector ();
+			
+			LNS.IndexSearcher searcher = LuceneCommon.GetSearcher (PrimaryStore);
+			searcher.Search (query, null, collector);
+			
+			if (collector.MatchId != -1) { 
+				Document doc = searcher.Doc (collector.MatchId);
+				info = DocumentToStoredInfo (doc);
+			}
+			
+			LuceneCommon.ReleaseSearcher (searcher);
+			
+			return info;
+		}
+		
+		public Hashtable GetStoredUriStrings (string server, string file)
+		{
+			Hashtable uris = new Hashtable ();
+
+			Term term = new Term (PropertyToFieldName (PropertyType.Keyword, "fixme:file"), file);
+			LNS.QueryFilter filter = new LNS.QueryFilter (new LNS.TermQuery (term));
+			
+			term = new Term (PropertyToFieldName (PropertyType.Keyword, "fixme:account"), server);
+			LNS.TermQuery query = new LNS.TermQuery (term);
+			
+			LNS.IndexSearcher searcher = LuceneCommon.GetSearcher (PrimaryStore);
+			LNS.Hits hits = searcher.Search (query, filter);
+			
+			for (int i = 0; i < hits.Length (); i++) {
+				StoredInfo info = DocumentToStoredInfo (hits.Doc (i));	
+				uris.Add (info.Uri.ToString (), info.FullyIndexed);
+			}
+
+			LuceneCommon.ReleaseSearcher (searcher);
+			
+			return uris;
+		}
+	}
+}
Index: beagled/ThunderbirdQueryable/Mail.cs
===================================================================
RCS file: beagled/ThunderbirdQueryable/Mail.cs
diff -N beagled/ThunderbirdQueryable/Mail.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ beagled/ThunderbirdQueryable/Mail.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,172 @@
+//
+// Mail.cs: Adds POP3 and IMAP indexing support to the Thunderbird backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+using GMime;
+	
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	[ThunderbirdIndexableGenerator (TB.AccountType.Pop3, "POP3 Support", true)]
+	[ThunderbirdIndexableGenerator (TB.AccountType.Imap, "IMAP Support", true)]
+	public class MailIndexableGenerator : ThunderbirdIndexableGenerator {
+
+		public MailIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string mork_file)
+			: base (indexer, account, mork_file)
+		{
+		}
+		
+		public override bool HasNextIndexable ()
+		{
+			do {
+				while (DbEnumerator == null || !DbEnumerator.MoveNext ()) {
+					Done = true;
+					indexer.NotificationEvent -= OnNotification;
+					indexer.ChildComplete ();
+					return false;
+				}
+			} while (IsUpToDate ((DbEnumerator.Current as TB.Mail).Uri));
+			
+			return true;
+		}
+		
+		public override Indexable GetNextIndexable ()
+		{
+			TB.Mail mail = DbEnumerator.Current as TB.Mail;
+
+			// If status is different, than something happend when loading this mail and we dont'
+			// want to change it's status.
+			if (mail.GetObject ("FullIndex") == null)
+				mail.SetObject ("FullIndex", (object) FullIndex);
+			
+			return MailToIndexable (mail);
+		}
+
+		public override void LoadDatabase ()
+		{
+			try {
+				db = new TB.Database (account, DbFile);
+				db.Load ();
+			} catch (Exception e) {
+				Logger.Log.Warn ("Failed to load {0}: {1}", DbFile, e.Message);
+				return;
+			}
+			
+			if (db.Count <= 0)
+				return;
+			
+			FullIndex = (Thunderbird.IsFullyIndexable (DbFile) ? true : false);
+			ResetEnumerator ();
+			Logger.Log.Info ("Indexing {0} containing {1} mails ({2})", RelativePath, db.Count, (FullIndex ? "Full" : "Partial"));
+		}
+
+		private Indexable MailToIndexable (TB.Mail mail)
+		{
+			Indexable indexable;
+			GMime.Message message = mail.Message;
+			FullIndex = mail.GetBool ("FullIndex"); // Make sure this is up to date
+			
+			indexable = NewIndexable (mail.Uri, message.Date.ToUniversalTime (), "MailMessage");
+			indexable.MimeType = "message/rfc822";
+			indexable.CacheContent = true;
+			indexable.AddProperty (Property.NewKeyword ("fixme:folder", mail.GetString ("mailbox")));
+			if (mail.GetBool ("FullIndex"))
+				indexable.ContentUri = UriFu.PathToFileUri (Thunderbird.GetFullyIndexableFile (DbFile));
+			else
+				indexable.NoContent = true;
+
+			string subject = GMime.Utils.HeaderDecodePhrase (message.Subject);
+			indexable.AddProperty (Property.New ("dc:title", subject));
+
+			indexable.AddProperty (Property.NewDate ("fixme:date", message.Date.ToUniversalTime ()));
+
+			GMime.InternetAddressList addrs;
+			addrs = message.GetRecipients (GMime.Message.RecipientType.To);
+			foreach (GMime.InternetAddress ia in addrs) {
+				indexable.AddProperty (Property.NewUnsearched ("fixme:to", ia.ToString (false)));
+				if (ia.AddressType != GMime.InternetAddressType.Group) {
+					indexable.AddProperty (Property.New ("fixme:to_address", ia.Addr));
+					indexable.AddProperty (Property.NewUnstored ("fixme:to_sanitized", StringFu.SanitizeEmail (ia.Addr)));
+				}
+				indexable.AddProperty (Property.New ("fixme:to_name", ia.Name));
+			}
+			addrs.Dispose ();
+
+			addrs = message.GetRecipients (GMime.Message.RecipientType.Cc);
+			foreach (GMime.InternetAddress ia in addrs) {
+				indexable.AddProperty (Property.NewUnsearched ("fixme:cc", ia.ToString (false)));
+				if (ia.AddressType != GMime.InternetAddressType.Group) {
+					indexable.AddProperty (Property.New ("fixme:cc_address", ia.Addr));
+					indexable.AddProperty (Property.NewUnstored ("fixme:cc_sanitized", StringFu.SanitizeEmail (ia.Addr)));
+				}
+				indexable.AddProperty (Property.New ("fixme:cc_name", ia.Name));
+			}
+			addrs.Dispose ();
+
+			addrs = GMime.InternetAddressList.ParseString (GMime.Utils.HeaderDecodePhrase (message.Sender));
+			foreach (GMime.InternetAddress ia in addrs) {
+				indexable.AddProperty (Property.NewUnsearched ("fixme:from", ia.ToString (false)));
+				if (ia.AddressType != GMime.InternetAddressType.Group) {
+					indexable.AddProperty (Property.New ("fixme:from_address", ia.Addr));
+					indexable.AddProperty (Property.NewUnstored ("fixme:from_sanitized", StringFu.SanitizeEmail (ia.Addr)));
+				}
+				indexable.AddProperty (Property.New ("fixme:from_name", ia.Name));
+			}
+			addrs.Dispose ();
+
+			if (HasAttachments (message.MimePart))
+				indexable.AddProperty (Property.NewFlag ("fixme:hasAttachments"));
+
+			if (message.ContentType.ToString ().ToLower () == "text/plain")
+				indexable.SetTextReader (new StreamReader (message.Stream));
+			indexable.SetBinaryStream (message.Stream);
+			
+			return indexable;
+		}
+		
+		private bool HasAttachments (GMime.Object mime_part)
+		{
+			if (mime_part is GMime.MessagePart)
+				return true;
+
+			// Messages that are multipart/alternative shouldn't be considered as having
+			// attachments.  Unless of course they do.
+			if (mime_part is GMime.Multipart && mime_part.ContentType.Subtype.ToLower () != "alternative")
+				return true;
+
+			return false;
+		}
+	}
+}
+	
Index: beagled/ThunderbirdQueryable/MoveMail.cs
===================================================================
RCS file: beagled/ThunderbirdQueryable/MoveMail.cs
diff -N beagled/ThunderbirdQueryable/MoveMail.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ beagled/ThunderbirdQueryable/MoveMail.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,107 @@
+//
+// MoveMail: Adds Unix Mailspool (MoveMail) indexing support to the Thunderbird backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+using GMime;
+	
+namespace Beagle.Daemon.ThunderbirdQueryable {	
+	
+	[ThunderbirdIndexableGenerator (TB.AccountType.MoveMail, "Movemail Support", false)]
+	public class MoveMailIndexableGenerator : ThunderbirdIndexableGenerator {
+	
+		public MoveMailIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string file)
+			: base (indexer, account, file)
+		{
+		}
+		
+		public override bool HasNextIndexable ()
+		{
+			do {
+				if (DbEnumerator == null || !DbEnumerator.MoveNext ()) {
+					Done = true;
+					indexer.NotificationEvent -= OnNotification;
+					indexer.ChildComplete ();
+					return false;
+				}
+			} while (IsUpToDate ((DbEnumerator.Current as TB.MoveMail).Uri));
+			
+			return true;
+		}
+		
+		public override Indexable GetNextIndexable ()
+		{
+			TB.MoveMail message = DbEnumerator.Current as TB.MoveMail;
+			
+			// If status is different, than something happend when loading this mail and we dont'
+			// want to change it's status.
+			if (message.GetObject ("FullIndex") == null)
+				message.SetObject ("FullIndex", (object) FullIndex);
+			
+			return MoveMailToIndexable (message);
+		}
+		
+		public override void LoadDatabase ()
+		{
+			try {
+				db = new TB.Database (account, DbFile);
+				db.Load ();
+			} catch (Exception e) {
+				Logger.Log.Debug ("Failed to load {0}: {1}", DbFile, e.Message);
+				return;
+			}
+			
+			if (db.Count <= 0)
+				return;
+			
+			FullIndex = (Thunderbird.IsFullyIndexable (DbFile) ? true : false);
+			ResetEnumerator ();
+			Logger.Log.Info ("Indexing {0} Movemails ({1})", db.Count, RelativePath);
+		}
+		
+		// FIXME: This need some more info
+		private Indexable MoveMailToIndexable (TB.MoveMail mail)
+		{
+			Indexable indexable;
+
+			indexable = NewIndexable (mail.Uri, DateTime.UtcNow, "MailMessage");
+			indexable.MimeType = "message/rfc822";
+			
+			string subject = GMime.Utils.HeaderDecodePhrase (mail.GetString ("subject"));
+			indexable.AddProperty (Property.New ("dc:title", subject));
+			
+			return indexable;
+		}
+	}
+}
Index: beagled/ThunderbirdQueryable/NNTP.cs
===================================================================
RCS file: beagled/ThunderbirdQueryable/NNTP.cs
diff -N beagled/ThunderbirdQueryable/NNTP.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ beagled/ThunderbirdQueryable/NNTP.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,113 @@
+//
+// NNTP.cs: Adds NNTP indexing support to the Thunderbird backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+using GMime;
+	
+namespace Beagle.Daemon.ThunderbirdQueryable {	
+	
+	[ThunderbirdIndexableGenerator (TB.AccountType.Nntp, "NNTP Support", false)]
+	public class NNTPIndexableGenerator : ThunderbirdIndexableGenerator {
+	
+		public NNTPIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string file)
+			: base (indexer, account, file)
+		{
+		}
+		
+		public override bool HasNextIndexable ()
+		{
+			do {
+				if (DbEnumerator == null || !DbEnumerator.MoveNext ()) {
+					Done = true;
+					indexer.NotificationEvent -= OnNotification;
+					indexer.ChildComplete ();
+					return false;
+				}
+			} while (IsUpToDate ((DbEnumerator.Current as TB.NntpMessage).Uri));
+			
+			return true;
+		}
+		
+		public override Indexable GetNextIndexable ()
+		{
+			TB.NntpMessage message = DbEnumerator.Current as TB.NntpMessage;
+			
+			// If status is different, than something happend when loading this mail and we dont'
+			// want to change it's status.
+			if (message.GetObject ("FullIndex") == null)
+				message.SetObject ("FullIndex", (object) FullIndex);
+			
+			return NntpMessageToIndexable (message);
+		}
+		
+		public override void LoadDatabase ()
+		{
+			try {
+				db = new TB.Database (account, DbFile);
+				db.Load ();
+			} catch (Exception e) {
+				Logger.Log.Debug ("Failed to load {0}: {1}", DbFile, e.Message);
+				return;
+			}
+			
+			if (db.Count <= 0)
+				return;
+			
+			FullIndex = (Thunderbird.IsFullyIndexable (DbFile) ? true : false);
+			ResetEnumerator ();
+			Logger.Log.Info ("Indexing {0} NNTP messages", db.Count);
+		}
+		
+		// FIXME: This need some more info
+		private Indexable NntpMessageToIndexable (TB.NntpMessage message)
+		{
+			Indexable indexable;
+			
+			indexable = new Indexable (message.Uri);
+			indexable.HitType = "MailMessage";
+			indexable.MimeType = "message/rfc822";
+			indexable.Timestamp = DateTime.Parse (message.GetString ("date")).ToUniversalTime ();
+			
+			indexable.AddProperty (Property.NewKeyword ("fixme:client", "thunderbird"));
+			indexable.AddProperty (Property.NewUnsearched ("fixme:fullyIndexed", message.GetBool ("FullIndex")));
+			indexable.AddProperty (Property.NewDate ("fixme:indexDateTime", DateTime.UtcNow));
+			
+			string subject = GMime.Utils.HeaderDecodePhrase (message.GetString ("subject"));
+			indexable.AddProperty (Property.New ("dc:title", subject));
+			
+			return indexable;
+		}
+	}
+}
Index: beagled/ThunderbirdQueryable/RSS.cs
===================================================================
RCS file: beagled/ThunderbirdQueryable/RSS.cs
diff -N beagled/ThunderbirdQueryable/RSS.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ beagled/ThunderbirdQueryable/RSS.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,122 @@
+//
+// RSS.cs: Adds RSS feed indexing support to the Thunderbird backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {	
+
+	[ThunderbirdIndexableGenerator (TB.AccountType.Rss, "RSS Support", true)]
+	public class RSSIndexableGenerator : ThunderbirdIndexableGenerator {
+		private string feed_url;
+		
+		public RSSIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string mork_file)
+			: base (indexer, account, mork_file)
+		{
+		}
+			
+		public override bool HasNextIndexable ()
+		{
+			do {
+				if (DbEnumerator == null || !DbEnumerator.MoveNext ()) {
+					Done = true;
+					indexer.NotificationEvent -= OnNotification;
+					indexer.ChildComplete ();
+					return false;
+				}
+			} while (IsUpToDate ((DbEnumerator.Current as TB.RssFeed).Uri));
+			
+			return true;
+		}
+		
+		public override Indexable GetNextIndexable ()
+		{
+			TB.RssFeed feed = DbEnumerator.Current as TB.RssFeed;
+			
+			// If status is different, than something happend when loading this mail and we dont'
+			// want to change it's status.
+			if (feed.GetObject ("FullIndex") == null)
+				feed.SetObject ("FullIndex", (object) FullIndex);
+			
+			return RssFeedToIndexable (feed);
+		}
+		
+		public override void LoadDatabase ()
+		{
+			string folder_name = null;
+			
+			try {
+				db = new TB.Database (account, DbFile);
+				db.Load ();
+				
+				Hashtable tbl = db.Db.Compile ("1", "ns:msg:db:row:scope:dbfolderinfo:all");
+				feed_url = tbl ["feedUrl"] as string;
+				folder_name = tbl ["folderName"] as string;
+			} catch (Exception e) {
+				Logger.Log.Debug ("Failed to load {0}: {1}", DbFile, e.Message);
+				return;
+			}
+			
+			if (db.Count <= 0)
+				return;
+			
+			ResetEnumerator ();
+			Logger.Log.Info ("Indexing \"{0}\" RSS feed containing {1} entries ({2})", folder_name, db.Count, RelativePath);
+		}
+		
+		private Indexable RssFeedToIndexable (TB.RssFeed feed)
+		{
+			Indexable indexable;
+			StringReader content = feed.Content;
+			
+			indexable = new Indexable (feed.Uri);
+			indexable.MimeType = "text/html";
+			indexable.HitType = "FeedItem";
+			indexable.Timestamp = DateTime.Parse (feed.GetString ("date")).ToUniversalTime ();
+
+			indexable.AddProperty (Property.NewKeyword ("fixme:client", "thunderbird"));
+			indexable.AddProperty (Property.NewUnsearched ("fixme:fullyIndexed", feed.GetBool ("FullIndex")));
+			indexable.AddProperty (Property.NewDate ("fixme:indexDateTime", DateTime.UtcNow));
+			
+			indexable.AddProperty (Property.NewKeyword ("dc:identifier", feed.GetString ("message-id")));
+			indexable.AddProperty (Property.NewKeyword ("dc:source", feed_url));
+			indexable.AddProperty (Property.New ("dc:publisher", feed.GetString ("sender")));
+			if (content == null)
+				indexable.AddProperty (Property.New ("dc:title", feed.GetString ("subject")));
+			
+			indexable.SetTextReader (feed.Content);
+			
+			return indexable;
+		}
+	}
+}
Index: beagled/ThunderbirdQueryable/ThunderbirdIndexableGenerator.cs
===================================================================
RCS file: beagled/ThunderbirdQueryable/ThunderbirdIndexableGenerator.cs
diff -N beagled/ThunderbirdQueryable/ThunderbirdIndexableGenerator.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ beagled/ThunderbirdQueryable/ThunderbirdIndexableGenerator.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,258 @@
+//
+// ThunderbirdIndexableGenerator.cs: A helper class that makes it very easy to add new features to this backend
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+using System.Text.RegularExpressions;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+using GMime;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	// This is a generic IndexableGenerator-class and should be used to index mork files only!
+	public abstract class ThunderbirdIndexableGenerator : IIndexableGenerator {
+		protected ThunderbirdIndexer indexer;
+		protected TB.Database db;
+		protected TB.Account account;
+		protected IEnumerator db_enumerator;
+		
+		private bool full_index;
+		private string db_file;
+		private bool done;
+		private string relative_path;
+		protected Hashtable stored_cache;
+		
+		public ThunderbirdIndexableGenerator (ThunderbirdIndexer indexer, TB.Account account, string db_file)
+		{
+			this.indexer = indexer;
+			this.indexer.NotificationEvent += OnNotification;
+			this.account = account;
+			this.full_index = true;
+			this.db_file = db_file;
+			this.done = false;
+			this.relative_path = Thunderbird.GetRelativePath (db_file);
+			
+			// Load the database and make sure the enumerator is up to date. Otherwise we will
+			// get lots of null exceptions when enumerating the database.
+			LoadDatabase ();
+			ResetEnumerator ();
+
+			// Fetch all already stored uris in the index. This way we can remove one uri at the time
+			// while we are indexing and thus in the end now which mails that doesn't exist anymore.
+			stored_cache = indexer.Lucene.GetStoredUriStrings (account.Server, relative_path);
+		}
+		
+		public abstract bool HasNextIndexable ();
+		public abstract Indexable GetNextIndexable ();
+		public abstract void LoadDatabase ();
+		
+		public virtual bool IsUpToDate (Uri uri)
+		{
+			if (uri == null)
+				return false;
+			
+			LuceneAccess.StoredInfo info = indexer.Lucene.GetStoredInfo (uri);
+
+			// Remove this uri from the cache
+			if (stored_cache != null)
+				stored_cache.Remove (uri.ToString ());
+			
+			// Check if this time is "older" than the time we began to index and if the index
+			// status has changed (partial vs. full indexing)
+			if (info != null && ThunderbirdQueryable.IndexingStart.CompareTo (info.LastIndex) < 0 && 
+				FullIndex == info.FullyIndexed) {
+				return true;
+			}
+
+			return false;
+		}
+		
+		public virtual void PostFlushHook ()
+		{
+			if (!Done || (stored_cache == null) || (Done && stored_cache.Count == 0))
+				return;
+			
+			if (Thunderbird.Debug)
+				Logger.Log.Debug ("Cleaning out old objects in {0} ({1})", RelativePath, stored_cache.Count);
+		
+			ArrayList uris = new ArrayList ();
+			foreach (string uri_str in stored_cache.Keys)
+				uris.Add (new Uri (uri_str));
+				
+			indexer.ScheduleRemoval ((Uri[]) uris.ToArray (typeof (Uri)), 
+				String.Format ("PostFlushHook-{0}", RelativePath), Scheduler.Priority.Delayed);
+		}
+		
+		protected virtual Indexable NewIndexable (Uri uri, DateTime timestamp, string hit_type)
+		{
+			Indexable indexable;
+			
+			indexable = new Indexable (uri);
+			indexable.HitType = hit_type;
+			indexable.Timestamp = timestamp;
+			
+			indexable.AddProperty (Property.NewKeyword ("fixme:account", account.Server));
+			indexable.AddProperty (Property.NewKeyword ("fixme:client", "thunderbird"));
+			indexable.AddProperty (Property.NewUnsearched ("fixme:fullyIndexed", full_index));
+			indexable.AddProperty (Property.NewUnsearched ("fixme:file", RelativePath));
+			indexable.AddProperty (Property.NewDate ("fixme:indexDateTime", DateTime.UtcNow));
+			
+			return indexable;
+		}
+		
+		protected virtual void ResetEnumerator ()
+		{
+			if (db != null && db.Count > 0)
+				db_enumerator = db.GetEnumerator ();
+			else
+				db_enumerator = null;
+		}
+		
+		protected virtual void OnNotification (object o, NotificationEventArgs args)
+		{
+			if (args.Account != account)
+				return;
+			
+			switch (args.Type) {
+			case NotificationType.StopIndexing:
+				indexer.NotificationEvent -= OnNotification;
+				Logger.Log.Debug ("Stopping running task {0}", account.Server);
+				break;
+				
+			case NotificationType.RestartIndexing:
+				LoadDatabase ();
+				break;
+				
+			case NotificationType.UpdateAccountInformation:
+				account = (TB.Account) args.Data;
+				LoadDatabase ();
+				break;
+				
+			}
+		}
+
+		// Done should be set to true when there's no more objects to index. This will allow
+		// PostFlushHook to remove old objects from the index.
+		public bool Done {
+			get { return done; }
+			set { done = value; }
+		}
+		
+		public string DbFile {
+			get { return db_file; }
+			set { db_file = value; }
+		}
+		
+		public bool FullIndex {
+			get { return full_index; }
+			set { full_index = value; }
+		}
+		
+		// Realtive path to current mork_file
+		public string RelativePath {
+			get { return relative_path; }
+		}
+		
+		protected IEnumerator DbEnumerator {
+			get { return db_enumerator; }
+		}
+		
+		public string StatusName { 
+			get { return account.Server; }
+		}
+	}
+	
+	/////////////////////////////////////////////////////////////////////////////////////
+	
+	[AttributeUsage (AttributeTargets.Class, AllowMultiple = true)]
+	public class ThunderbirdIndexableGeneratorAttribute : System.Attribute {
+		private TB.AccountType type;
+		private string description;
+		private bool enabled;
+		
+		public ThunderbirdIndexableGeneratorAttribute (TB.AccountType type, string description, bool enabled)
+		{
+			this.type = type;
+			this.description = description;
+			this.enabled = enabled;
+		}
+		
+		public TB.AccountType Type {
+			get { return type; }
+			set { type = value; }
+		}
+		
+		public string Description {
+			get { return description; }
+			set { description = value; }
+		}
+		
+		public bool Enabled {
+			get { return enabled; }
+			set { enabled = value; }
+		}
+	}
+	
+	/////////////////////////////////////////////////////////////////////////////////////
+	
+	
+	public class UriRemovalIndexableGenerator : IIndexableGenerator {
+		private Uri[] uris;
+		
+		private IEnumerator enumerator;
+		
+		public UriRemovalIndexableGenerator (Uri[] uris)
+		{
+			this.uris = uris;
+			this.enumerator = this.uris.GetEnumerator ();
+		}
+		
+		public Indexable GetNextIndexable ()
+		{
+			return new Indexable (IndexableType.Remove, (Uri) enumerator.Current);
+		}
+		
+		public bool HasNextIndexable ()
+		{
+			while (enumerator == null || !enumerator.MoveNext ())
+				return false;
+			
+			return true;
+		}
+		
+		public string StatusName {
+			get { return String.Format ("Removing {0} uris", uris.Length); }
+		}
+		
+		public void PostFlushHook () { }
+	}
+}
Index: beagled/ThunderbirdQueryable/ThunderbirdIndexer.cs
===================================================================
RCS file: beagled/ThunderbirdQueryable/ThunderbirdIndexer.cs
diff -N beagled/ThunderbirdQueryable/ThunderbirdIndexer.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ beagled/ThunderbirdQueryable/ThunderbirdIndexer.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,462 @@
+//
+// ThunderbirdIndexer.cs: This class launches IndexableGenerators and makes sure instant-updates work
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Reflection;
+
+using Beagle.Util;
+using Beagle.Daemon;
+using TB = Beagle.Util.Thunderbird;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	public class ThunderbirdIndexer {
+		private ThunderbirdQueryable queryable;
+
+		private bool init_phase, first_lap;
+		private string[] root_paths;
+		private Hashtable supported_types;
+		private ArrayList account_list;
+		private ThunderbirdInotify inotify;
+	
+		public delegate void NotificationEventHandler (object o, NotificationEventArgs args);
+	
+		public ThunderbirdIndexer (ThunderbirdQueryable queryable, string[] root_paths)
+		{
+			this.queryable = queryable;
+			this.root_paths = root_paths;
+			this.supported_types = new Hashtable ();
+			this.init_phase = true;
+			this.first_lap = true;
+			this.account_list = new ArrayList ();
+			this.inotify = new ThunderbirdInotify ();
+			
+			LoadSupportedTypes ();
+			
+			foreach (string path in root_paths) {
+				Inotify.Subscribe (path, OnInotifyEvent, 
+					Inotify.EventType.Delete | 
+					Inotify.EventType.MovedTo | 
+					Inotify.EventType.Modify |
+					Inotify.EventType.Create);
+			}
+			
+			inotify.InotifyEvent += OnInotifyEvent;
+		}
+		
+		// Loads all supported types, checks if they have a correct constructor and is enabled
+		private void LoadSupportedTypes ()
+		{
+			Assembly assembly = Assembly.GetCallingAssembly ();
+			
+			foreach (Type type in ReflectionFu.ScanAssemblyForInterface (assembly, typeof (IIndexableGenerator))) {
+			
+				foreach (ThunderbirdIndexableGeneratorAttribute attr in 
+					ReflectionFu.ScanTypeForAttribute (type, typeof (ThunderbirdIndexableGeneratorAttribute))) {
+
+					foreach (ConstructorInfo constructor in type.GetConstructors ()) {
+					
+						ParameterInfo[] parameters = constructor.GetParameters ();
+						if(parameters.Length != 3)
+							continue;
+						
+						if (parameters [0].ParameterType.Equals (typeof (ThunderbirdIndexer)) &&
+							parameters [1].ParameterType.Equals (typeof (TB.Account)) &&
+							parameters [2].ParameterType.Equals (typeof (string))) {
+							
+							// Make sure we should enable this type
+							if (attr.Enabled)
+								supported_types [attr.Type]  = type;
+							
+						} else
+							Logger.Log.Debug ("{0} has an invalid constructor!", type.ToString ());
+					}
+				}
+			}	
+		}
+		
+		public void Crawl ()
+		{
+			int launched = 0;
+			
+			foreach (string path in root_paths) {
+				foreach (TB.Account account in Thunderbird.ReadAccounts (path)) {
+					if (Shutdown.ShutdownRequested)
+						return;
+					
+					if (supported_types [account.Type] == null)
+						continue;
+					
+					IndexAccount (account);
+					launched++;
+				}
+			}
+			
+			init_phase = false;
+			Logger.Log.Info ("Indexing {0} ({1}) Thunderbird account(s) spread over {2} profile(s)", 
+				launched, account_list.Count, root_paths.Length);
+			
+			// Clean out old stuff in case no IndexableGenerator was launched
+			if (launched == 0)
+				ChildComplete ();
+		}
+		
+		public void IndexAccount (TB.Account account)
+		{
+			TB.Account stored_account = GetParentAccount (account.Path);
+
+			// We need to act upon changes made to accounts during Thunderbird runtime.
+			// The user might change from plain to SSL, which leads to a new port number
+			// that has to be taken in account for when indexing.
+			if (stored_account == null && Directory.Exists (account.Path) && supported_types [account.Type] != null) {
+				account_list.Add (account);
+				IndexDirectory (account.Path);
+				//Logger.Log.Info ("Indexing {0} account {1}", account.Type.ToString (), account.Server);
+			
+			} else if (stored_account == null && File.Exists (account.Path) && supported_types [account.Type] != null) {
+				account_list.Add (account);
+				IndexFile (account.Path);
+				//Logger.Log.Info ("Indexing {0} account {1}", account.Type.ToString (), account.Server);
+				
+			} else if (stored_account != null &&
+				(stored_account.Server != account.Server ||
+				stored_account.Port != account.Port ||
+				stored_account.Type != account.Type ||
+				stored_account.Delimiter != account.Delimiter)) {
+
+				account_list.Remove (stored_account);
+				account_list.Add (account);
+				
+				// Make sure all running indexables are aware of this since it can affect the way they index
+				NotificationEventArgs args;
+				args = new NotificationEventArgs (NotificationType.UpdateAccountInformation, stored_account);
+				args.Data = (object) account;
+				OnNotification (args);
+				
+				Logger.Log.Info ("Updated {0} with new account details", account.Server);
+			}
+		}
+		
+		public void IndexFile (string file)
+		{
+			TB.Account account = GetParentAccount (file);
+						
+			if (account == null || supported_types [account.Type] == null)
+				return;
+			
+			object[] param = new object[] {this, account, file};
+			ThunderbirdIndexableGenerator generator = Activator.CreateInstance (
+				(Type) supported_types [account.Type], param) as ThunderbirdIndexableGenerator;
+			
+			AddIIndexableTask (generator, file);
+		}
+		
+		private void IndexDirectory (string directory)
+		{
+			Queue pending = new Queue ();
+			
+			pending.Enqueue (directory);
+			while (pending.Count > 0) {
+				string dir = pending.Dequeue () as string;
+				
+				foreach (string subdir in DirectoryWalker.GetDirectories (dir)) {
+					if (Shutdown.ShutdownRequested)
+						return;
+						
+					pending.Enqueue (subdir);
+				}
+
+				if (Inotify.Enabled) {
+						inotify.Watch (dir, 
+						Inotify.EventType.Modify | 
+						Inotify.EventType.Create |
+						Inotify.EventType.Delete |
+						Inotify.EventType.MovedFrom |
+						Inotify.EventType.MovedTo);
+				}
+				
+				foreach (string file in DirectoryWalker.GetItems 
+					(dir, new DirectoryWalker.FileFilter (Thunderbird.IsMorkFile))) {
+					if (Shutdown.ShutdownRequested)
+						return;
+					
+					IndexFile (file);
+				}
+			}
+		}
+		
+		public void RemoveAccount (TB.Account account)
+		{
+			TB.Account acc = GetParentAccount (account.Path);
+			
+			if (acc == null)
+				return;
+			
+			ScheduleRemoval (Property.NewKeyword ("fixme:account", acc.Server), Scheduler.Priority.Delayed);
+			OnNotification (new NotificationEventArgs (NotificationType.StopIndexing, account));
+			account_list.Remove (acc);
+		}
+
+		private void AddIIndexableTask (IIndexableGenerator generator, string tag)
+		{
+			if (queryable.ThisScheduler.ContainsByTag (tag)) {
+				Logger.Log.Debug ("Not adding a Task for already running: {0}", tag);
+				return;
+			}
+
+			Scheduler.Task task = queryable.NewAddTask (generator);
+			task.Tag = tag;
+			queryable.ThisScheduler.Add (task);
+		}
+		
+		private void ScheduleRemoval (Property prop, Scheduler.Priority priority)
+		{
+			if (queryable.ThisScheduler.ContainsByTag (prop.ToString ())) {
+				Logger.Log.Debug ("Not adding a Task for already running: {0}", prop.ToString ());
+				return;
+			}
+			
+			Scheduler.Task task = queryable.NewRemoveByPropertyTask (prop);
+			task.Priority = priority;
+			task.SubPriority = 0;
+			queryable.ThisScheduler.Add (task);
+		}
+
+		public void ScheduleRemoval (Uri[] uris, string tag, Scheduler.Priority priority)
+		{
+			if (queryable.ThisScheduler.ContainsByTag (tag)) {
+				Logger.Log.Debug ("Not adding a Task for already running: {0}", tag);
+				return;
+			}
+
+			Scheduler.Task task = queryable.NewAddTask (new UriRemovalIndexableGenerator (uris));
+			task.Priority = priority;
+			task.SubPriority = 0;
+			queryable.ThisScheduler.Add (task);
+		}
+		
+		public void UpdateAccounts (string root_path)
+		{
+			ArrayList new_accounts;
+
+			try {
+				new_accounts = Thunderbird.ReadAccounts (root_path);
+			} catch (Exception e) {
+				Logger.Log.Warn ("Failed when reading Thunderbird accounts: {0}, account may have been added or removed", e);
+				return;
+			}
+
+			// Add all accounts again to make sure things are updated the way they should
+			foreach (TB.Account account in new_accounts)
+				IndexAccount (account);
+
+			// Remove non-existing accounts
+			foreach (TB.Account existing in account_list) {
+				bool found = false;
+			
+				foreach (TB.Account new_account in new_accounts) {
+					if (existing.Path == new_account.Path) {
+						found = true;
+						break;
+					}
+				}
+
+				if (!found) 
+						RemoveAccount (existing);
+			}
+		}
+		
+		public TB.Account GetParentAccount (string directory)
+		{
+			foreach (TB.Account acc in account_list) {
+				if (directory.StartsWith (acc.Path))
+					return acc;
+			}
+			
+			return null;
+		}
+
+		private void OnInotifyEvent (Inotify.Watch watch,
+						string path,
+						string subitem,
+						string srcpath,
+						Inotify.EventType type)
+		{
+			if (subitem == null)
+				return;
+				
+			string full_path = Path.Combine (path, subitem);
+
+			// If prefs.js is deleted... then we have nothing at all to index
+			if (((type & Inotify.EventType.MovedTo) != 0 && srcpath == Path.Combine (path, "prefs.js")) ||
+				((type & Inotify.EventType.Delete) != 0 && subitem == "prefs.js")) {
+				
+				foreach (TB.Account account in account_list)
+					RemoveAccount (account);
+				return;
+			}
+
+			// Update in case an account was removed or added
+			// Thunderbird saves prefs.js with a different name and then replacing the old one
+			// by "moving" it over the existing prefs.js. That's why MoveTo is used as inotfy type.
+			if ((((type & Inotify.EventType.Modify) != 0 || (type & Inotify.EventType.MovedTo) != 0 || 
+				(type & Inotify.EventType.Create) != 0) && subitem == "prefs.js")) {
+				
+				UpdateAccounts (path);
+				return;
+			}
+
+			// In case the address book file have been moved or deleted, we have to stop indexing it
+			if (((type & Inotify.EventType.MovedTo) != 0 && srcpath == Path.Combine (path, "abook.mab")) ||
+				((type & Inotify.EventType.Delete) != 0 && subitem == "abook.mab")) {
+				
+				TB.Account account = GetParentAccount (full_path);
+				
+				if (account != null)					
+					RemoveAccount (account);
+					
+				return;
+			}
+
+			// In case of a newly created addressbook, the current address book is modified or an old 
+			// address book is moved to where the address book can be found: either start indexing 
+			// or restart an already indexing IndeaxbleGenerator.
+			if ((((type & Inotify.EventType.Modify) != 0 || (type & Inotify.EventType.MovedTo) != 0 || 
+				(type & Inotify.EventType.Create) != 0) && subitem == "abook.mab")) {
+				
+				TB.Account account = GetParentAccount (full_path);
+				
+				if (account == null && File.Exists (full_path)) {
+					UpdateAccounts (path);
+					return;
+				} else if (account == null)
+					return;
+
+				// Tell any running indexable about this or start a new one
+				if (queryable.ThisScheduler.ContainsByTag (full_path))
+					OnNotification (new NotificationEventArgs (NotificationType.RestartIndexing, account));
+				else
+					IndexFile (full_path);
+				
+				return;
+			}
+			
+			// Re-index files when needed
+			if ((type & Inotify.EventType.Modify) != 0) {
+				TB.Account account = GetParentAccount (full_path);
+			
+				if (account == null || !Thunderbird.IsMorkFile (path, subitem))
+					return;
+				
+				// In case we have a running IndexableGenerator, tell it that we have a file that needs to 
+				// be re-indexed.
+				if (queryable.ThisScheduler.ContainsByTag (full_path))
+					OnNotification (new NotificationEventArgs (NotificationType.RestartIndexing, account));
+				else
+					IndexFile (full_path);
+					
+				return;
+			}
+			
+			// Index newly created directories
+			if ((type & Inotify.EventType.Create) != 0 && (type & Inotify.EventType.IsDirectory) != 0) {
+				if (GetParentAccount (full_path) != null && Inotify.Enabled)
+					Inotify.Subscribe (full_path, OnInotifyEvent, Inotify.EventType.All);
+					
+				return;
+			}
+		}
+
+		public void ChildComplete ()
+		{
+			if (NotificationEvent != null || init_phase || !first_lap)
+				return;
+			
+			if (Thunderbird.Debug)
+				Logger.Log.Debug ("Removing old Thunderbird objects");
+
+			Scheduler.Task task = queryable.NewRemoveTaskByDate (ThunderbirdQueryable.IndexingStart);
+			task.Priority = Scheduler.Priority.Idle;
+			task.Tag = "RemoveOldThunderbirdMails";
+			queryable.ThisScheduler.Add (task);
+			
+			// This makes sure that ChildComplete will only clean out all mails once in a lifetime
+			// (of the Thunderbird backend that is)
+			first_lap = false;
+		}
+
+		protected virtual void OnNotification(NotificationEventArgs args)
+		{
+			if (NotificationEvent != null)
+				NotificationEvent (this, args);
+		}
+
+		public event NotificationEventHandler NotificationEvent;
+		
+		public LuceneAccess Lucene {
+			get { return queryable.Lucene; }
+		}
+	}
+	
+	/////////////////////////////////////////////////////////////////////////////////////
+	
+	public enum NotificationType {
+		StopIndexing,
+		RestartIndexing,
+		UpdateAccountInformation
+	}
+	
+	/////////////////////////////////////////////////////////////////////////////////////
+	
+	public class NotificationEventArgs  : EventArgs
+	{
+		private NotificationType type;
+		private TB.Account account;
+		private object data;
+
+		public NotificationEventArgs (NotificationType type, TB.Account account)
+		{
+			this.type = type;
+			this.account = account;
+		}
+		
+		public NotificationType Type {
+			get { return type; }
+		}
+		
+		public TB.Account Account {
+			get { return account; }
+		}
+		
+		public object Data {
+			get { return data; }
+			set { data =value; }
+		}
+	}
+	
+}
Index: beagled/ThunderbirdQueryable/ThunderbirdInotify.cs
===================================================================
RCS file: beagled/ThunderbirdQueryable/ThunderbirdInotify.cs
diff -N beagled/ThunderbirdQueryable/ThunderbirdInotify.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ beagled/ThunderbirdQueryable/ThunderbirdInotify.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,160 @@
+//
+// ThunderbirdInotify.cs. This class will sumnarize inotify events and raise an event every 30 seconds (to prevent inotify hammering)
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Collections;
+
+using Beagle;
+using Beagle.Util;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	public class ThunderbirdInotify {
+		protected struct Event {
+			public Inotify.Watch Watch;
+			public string Path;
+			public string Subitem;
+			public string Srcpath;
+			public Inotify.EventType Type;
+			public long OldFileSize;
+			public long CurrentFileSize;
+			
+			public Event (Inotify.Watch watch, string path, string subitem,
+				string srcpath, Inotify.EventType type, long old_size, long current_size)
+			{
+				this.Watch = watch;
+				this.Path = path;
+				this.Subitem = subitem;
+				this.Srcpath = srcpath;
+				this.Type = type;
+				this.OldFileSize = old_size;
+				this.CurrentFileSize = current_size;
+			}
+		}
+
+		private Queue queue;
+		
+		public ThunderbirdInotify ()
+		{
+			queue = new Queue ();
+			
+			GLib.Timeout.Add (30000, new GLib.TimeoutHandler (Process));
+		}
+		
+		public void Watch (string path, Inotify.EventType type)
+		{
+			Inotify.Subscribe (path, OnInotify, type);
+		}
+		
+		private void OnInotify (Inotify.Watch watch,
+					string path,
+					string subitem,
+					string srcpath,
+					Inotify.EventType type)
+		{
+			if (subitem == null)
+				return;
+
+			// Unsubscribe to directories that have been removed
+			if ((type & Inotify.EventType.Delete) != 0 && (type & Inotify.EventType.IsDirectory) != 0)
+				watch.Unsubscribe ();
+
+			lock (queue.SyncRoot) {
+				bool found = false;
+				for (int i = 0; i < queue.Count; i++) {
+					Event ev = (Event) queue.Dequeue ();
+					
+					if (ev.Path == path && ev.Subitem == subitem && ev.Srcpath == srcpath) {
+						found = true;
+						ev.Type = (ev.Type | type);
+						queue.Enqueue (ev);
+						break;
+					}
+					
+					queue.Enqueue (ev);
+				}
+				
+				if (!found) {
+					queue.Enqueue (new Event (watch, path, subitem, srcpath, 
+						type, -1, GetFileSize (Path.Combine (path, subitem))));
+				}
+			}
+		}
+			
+		private bool Process ()
+		{
+			Queue tmp = new Queue ();
+			
+			lock (queue.SyncRoot) {
+				while (queue.Count > 0) {
+					Event ev = (Event) queue.Dequeue();
+					long size = GetFileSize (Path.Combine (ev.Path, ev.Subitem));
+					
+					if (Thunderbird.Debug) {
+						Logger.Log.Debug ("EVENT: {0} ({1}) [{2}, {3}]", 
+							Path.Combine (ev.Path, ev.Subitem).ToString (), ev.Type, ev.CurrentFileSize, size);
+					}
+
+					if (size != ev.CurrentFileSize) {
+						ev.OldFileSize = ev.CurrentFileSize;
+						ev.CurrentFileSize = size;
+						tmp.Enqueue (ev);
+						continue;
+					}
+						
+					OnInotifyEvent (ev);
+				}
+
+				while (tmp.Count > 0)
+					queue.Enqueue (tmp.Dequeue ());
+			}
+		
+			return true;
+		}
+
+		private long GetFileSize (string filename)
+		{
+			long filesize = -1;
+			
+			try {
+				FileInfo file = new FileInfo (filename);
+				filesize = file.Length;
+			} catch (Exception e) { }
+				
+			return filesize;
+		}
+		
+		protected virtual void OnInotifyEvent (Event ev)
+		{
+			if (InotifyEvent != null)
+				InotifyEvent (ev.Watch, ev.Path, ev.Subitem, ev.Srcpath, ev.Type);
+		}
+		
+		public event Inotify.InotifyCallback InotifyEvent;
+	}
+
+}
Index: beagled/ThunderbirdQueryable/ThunderbirdQueryable.cs
===================================================================
RCS file: beagled/ThunderbirdQueryable/ThunderbirdQueryable.cs
diff -N beagled/ThunderbirdQueryable/ThunderbirdQueryable.cs
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ beagled/ThunderbirdQueryable/ThunderbirdQueryable.cs	23 Jun 2006 07:34:36 -0000
@@ -0,0 +1,180 @@
+//
+// ThunderbirdQueryable.cs: This is where all the magic begins!
+//
+// Copyright (C) 2006 Pierre Östlund
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Collections;
+using System.Text.RegularExpressions;
+
+using Beagle;
+using Beagle.Util;
+
+namespace Beagle.Daemon.ThunderbirdQueryable {
+
+	[QueryableFlavor (Name = "Thunderbird", Domain = QueryDomain.Local, RequireInotify = false)]
+	public class ThunderbirdQueryable : LuceneQueryable {
+		private static DateTime indexing_start;
+		private ThunderbirdIndexer indexer;
+		
+		public ThunderbirdQueryable () :
+			base ("ThunderbirdIndex")
+		{
+			// Remove one second from the start time to make sure we don't run into any troubles
+			indexing_start = DateTime.UtcNow.Subtract (new TimeSpan (0, 0, 1));
+			indexer = null;
+			
+			GMime.Global.Init ();
+			
+			if (Environment.GetEnvironmentVariable ("BEAGLE_THUNDERBIRD_DEBUG") != null) {
+				Thunderbird.Debug = true;
+				Logger.Log.Debug ("Running Thunderbird backend in debug mode");
+			}
+		}
+		
+		public override void Start ()
+		{
+			base.Start ();
+			ExceptionHandlingThread.Start (new ThreadStart (StartWorker));
+		}
+		
+		private void StartWorker ()
+		{
+			Logger.Log.Info ("Starting Thunderbird backend");
+			Stopwatch watch = new Stopwatch ();
+			watch.Start ();
+			
+			string root_path = Thunderbird.GetRootPath ();
+			if (!Directory.Exists (root_path)) {
+				GLib.Timeout.Add (60000, new GLib.TimeoutHandler (IndexDataCheck));
+				Logger.Log.Info ("No data available for indexing in {0}", root_path);
+				return;
+			}
+			
+			State = QueryableState.Crawling;
+			indexer = new ThunderbirdIndexer (this, Thunderbird.GetProfilePaths (root_path));
+			indexer.Crawl ();
+			State = QueryableState.Idle;
+			
+			watch.Stop ();
+			Logger.Log.Info ("Thunderbird backend done in {0}s", watch.ElapsedTime);
+		}
+		
+		private bool IndexDataCheck ()
+		{
+			if (!Directory.Exists (Thunderbird.GetRootPath ()))
+				return true;
+				
+			StartWorker ();
+			return false;
+		}
+		
+		// We need this in order to perform custom queries to the lucene database
+		override protected LuceneQueryingDriver BuildLuceneQueryingDriver (string index_name,
+										   int    minor_version,
+										   bool   read_only_mode)
+		{
+			return new LuceneAccess (index_name, minor_version, read_only_mode);
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		public Scheduler.Task NewRemoveTaskByDate (DateTime end_date)
+		{
+			return NewAddTask (new DateIndexableGenerator (Driver, Lucene, end_date));
+		}
+		
+		// The purpose of this IndexableGenerator is to remove mails older than the
+		// specified date (when beagle began to index Thunderbird mails)
+		private class DateIndexableGenerator : IIndexableGenerator {
+			private LuceneQueryingDriver driver;
+			private LuceneAccess lucene;
+			private DateTime end_date;
+			
+			private Uri[] stored_uris;
+			private IEnumerator enumerator;
+			
+			public DateIndexableGenerator (LuceneQueryingDriver driver, LuceneAccess lucene, DateTime end_date)
+			{
+				this.driver = driver;
+				this.lucene = lucene;
+				this.end_date = end_date;
+				this.stored_uris = null;
+			}
+			
+			public Indexable GetNextIndexable ()
+			{
+				return new Indexable (IndexableType.Remove, (Uri) enumerator.Current);
+			}
+			
+			public bool HasNextIndexable ()
+			{
+				if (stored_uris == null) {
+					stored_uris = driver.PropertyQuery (Property.NewKeyword ("fixme:client", "thunderbird"));
+					enumerator = stored_uris.GetEnumerator ();
+				}
+				
+				do {
+					while (enumerator == null || !enumerator.MoveNext ())
+						return false;
+				} while (MatchesDate ((enumerator.Current as Uri)));
+				
+				return true;
+			}
+			
+			private bool MatchesDate (Uri uri)
+			{
+				LuceneAccess.StoredInfo info = lucene.GetStoredInfo (uri);
+				
+				try {
+					if (!info.Equals (end_date) && info.LastIndex.CompareTo (end_date) < 0)
+						return false;
+				} catch (Exception e) {}
+				
+				return true;
+			}
+			
+			public string StatusName {
+				get {
+					return String.Format ("Removing Thunderbird mails past {0}", end_date.ToString ()); 
+				}
+			}
+			
+			public void PostFlushHook () { }
+		}
+		
+		/////////////////////////////////////////////////////////////////////////////////////
+		
+		public LuceneAccess Lucene {
+			get { return (LuceneAccess) Driver; }
+		}
+		
+		public static DateTime IndexingStart {
+			get { return indexing_start; }
+		}
+	}
+
+}
Index: search/Tiles/MailAttachment.cs
===================================================================
RCS file: /cvs/gnome/beagle/search/Tiles/MailAttachment.cs,v
retrieving revision 1.5
diff -u -1 -2 -r1.5 MailAttachment.cs
--- search/Tiles/MailAttachment.cs	20 May 2006 15:53:01 -0000	1.5
+++ search/Tiles/MailAttachment.cs	23 Jun 2006 07:34:38 -0000
@@ -13,31 +13,31 @@
 		}
 
 		public override Tile BuildTile (Beagle.Hit hit, Beagle.Query query)
 		{
 			return new MailAttachment (hit, query);
 		}
 
 		public override bool Validate (Beagle.Hit hit)
 		{
 			if (! base.Validate (hit))
 				return false;
 
-			string str = hit ["parent:fixme:hasAttachments"];
-
+			string str = hit.GetFirstProperty ("parent:fixme:hasAttachments");
+			//Console.WriteLine (str);
 			if (hit.ParentUri == null || str == null || str == "false")
 				return false;
 
-			str = hit ["fixme:attachment_title"];
-
+			str = hit.GetFirstProperty ("fixme:attachment_title");
+			//Console.WriteLine (str);
 			if (str == null || str == "")
 				return false;
 
 			Weight += 1;
 			
 			return true;
 		}
 	}
 
 	public class MailAttachment : TileTemplate {
 
 		public MailAttachment (Beagle.Hit hit, Beagle.Query query) : base (hit, query)
@@ -60,34 +60,39 @@
 		{
 			// FIXME: We should handle this case better, but
 			// for now, if we match an attachment, we just want
 			// to display the properties for the parent message.
 			if (!IsAttachment (hit))
 				return hit [name];
 			else
 				return hit ["parent:" + name];
 		}
 
 		public override void Open ()
 		{
-			if (GetHitProperty (Hit, "fixme:client") != "evolution") {
+			SafeProcess p = MailMessage.GetClientProcess (GetHitProperty (Hit, "fixme:client"));
+
+			if (p == null) {
 				OpenFromMime (Hit);
 				return;
 			}
 
-			SafeProcess p = new SafeProcess ();
-			p.Arguments = new string [2];
-			p.Arguments [0] = "evolution";
-
 			if (Hit.ParentUriAsString != null)
-				p.Arguments [1] = Hit.ParentUriAsString;
+				p.Arguments [p.Arguments.Length-1] = Hit.ParentUriAsString;
 			else
-				p.Arguments [1] = Hit.UriAsString;
+				p.Arguments [p.Arguments.Length-1] = Hit.UriAsString;
 
 			try {
 				p.Start ();
 			} catch (SafeProcessException e) {
 				Console.WriteLine ("Unable to run {0}: {1}", p.Arguments [0], e.Message);
+				p.Arguments [0] = "mozilla-thunderbird";
+				try {
+					p.Start();
+				} catch (SafeProcessException e2) {
+					Console.WriteLine ("Unable to run {0}: {1}", p.Arguments [0], e2.Message);
+				}			
 			}
-		}	
+		}
+		
 	}
 }
Index: search/Tiles/MailMessage.cs
===================================================================
RCS file: /cvs/gnome/beagle/search/Tiles/MailMessage.cs,v
retrieving revision 1.15
diff -u -1 -2 -r1.15 MailMessage.cs
--- search/Tiles/MailMessage.cs	26 May 2006 11:14:31 -0000	1.15
+++ search/Tiles/MailMessage.cs	23 Jun 2006 07:34:38 -0000
@@ -1,14 +1,15 @@
 using System;
 using System.Diagnostics;
+using System.IO;
 using Mono.Unix;
 using Beagle.Util;
 
 namespace Search.Tiles {
 
 	public class MailMessageActivator : TileActivator {
 
 		public MailMessageActivator () : base ()
 		{
 			AddSupportedFlavor (new HitFlavor (null, null, "message/rfc822"));
 		}
 
@@ -88,44 +89,69 @@
 
 			string label = sent ? Catalog.GetString ("To:") : Catalog.GetString ("From:");
 			details.AddLabelPair (label, GetAddress (Hit));
 
 			label = sent ? Catalog.GetString ("Date Sent:") : Catalog.GetString ("Date Received:");
 			details.AddLabelPair (label, Utils.NiceLongDate (Timestamp));
 
 			details.AddSnippet ();
 
 			return details;
 		}
 
+		public static SafeProcess GetClientProcess (string client)
+		{
+			SafeProcess p = null;
+
+			if (client == "evolution") {
+				p = new SafeProcess ();
+				p.Arguments = new string [2];
+				p.Arguments [0] = "evolution";
+			} else if (client == "thunderbird") {
+				
+
+				p = new SafeProcess ();
+				p.Arguments = new string [3];
+				p.Arguments [0] = "thunderbird";
+				p.Arguments [1] = "-mail";
+			}
+
+			return p;
+		}
+
 		public override void Open ()
 		{
-			if (Hit.GetFirstProperty ("fixme:client") != "evolution") {
+			SafeProcess p = GetClientProcess (Hit.GetFirstProperty ("fixme:client"));
+
+			if (p == null) {
 				OpenFromMime (Hit);
 				return;
 			}
 
-			SafeProcess p = new SafeProcess ();
-			p.Arguments = new string [2];
-			p.Arguments [0] = "evolution";
 
 			if (Hit.ParentUriAsString != null)
-				p.Arguments [1] = Hit.ParentUriAsString;
+				p.Arguments [p.Arguments.Length-1] = Hit.ParentUriAsString;
 			else
-				p.Arguments [1] = Hit.UriAsString;
+				p.Arguments [p.Arguments.Length-1] = Hit.UriAsString;
 
 			try {
 				p.Start ();
 			} catch (SafeProcessException e) {
 				Console.WriteLine ("Unable to run {0}: {1}", p.Arguments [0], e.Message);
+				p.Arguments [0] = "mozilla-thunderbird";
+				try {
+					p.Start();
+				} catch (SafeProcessException e2) {
+					Console.WriteLine ("Unable to run {0}: {1}", p.Arguments [0], e2.Message);
+				}
 			}
 		}
 
 		public void SendInMail ()
 		{
 			SafeProcess p = new SafeProcess ();
 			p.Arguments = new string [] { "evolution", String.Format ("{0};forward=attached", Hit.Uri) };
 
 			try {
 				p.Start () ;
 			} catch (Exception e) {
 				Console.WriteLine ("Error launching Evolution composer: " + e.Message);
