From 94c24365d93d421cd731f19c4b95072525e4835f Mon Sep 17 00:00:00 2001
From: Gabriel Burt <gabriel.burt@gmail.com>
Date: Tue, 20 Oct 2009 15:27:56 -0700
Subject: [PATCH] Index Evolution's cached GroupWise e-mails

---
 .../EvolutionMailIndexableGenerator.cs             |  411 +++++++++++---------
 .../EvolutionMailQueryable.cs                      |   25 ++-
 .../EvolutionMailQueryable/GroupWiseGenerator.cs   |  240 ++++++++++++
 .../beagled/EvolutionMailQueryable/MailCrawler.cs  |   10 +
 beagle/beagled/Makefile.am                         |    1 +
 5 files changed, 494 insertions(+), 193 deletions(-)
 create mode 100644 beagle/beagled/EvolutionMailQueryable/GroupWiseGenerator.cs

Index: beagled/EvolutionMailQueryable/EvolutionMailIndexableGenerator.cs
===================================================================
--- beagled/EvolutionMailQueryable/EvolutionMailIndexableGenerator.cs.orig	2008-04-25 12:17:04.000000000 -0600
+++ beagled/EvolutionMailQueryable/EvolutionMailIndexableGenerator.cs	2011-07-01 11:34:33.799000148 -0600
@@ -75,6 +75,128 @@ namespace Beagle.Daemon.EvolutionMailQue
 		public abstract Indexable GetNextIndexable ();
 		public abstract void Checkpoint ();
 
+		protected Indexable GetNextIndexable (GMime.Message message, long offset, string path)
+		{
+			using (message) {
+				// Work around what I think is a bug in GMime: If you
+				// have a zero-byte file or seek to the end of a
+				// file, parser.Eos () will return true until it
+				// actually tries to read something off the wire.
+				// Since parser.ConstructMessage() always returns a
+				// message (which may also be a bug), we'll often get
+				// one empty message which we need to deal with here.
+				//
+				// Check if its empty by seeing if the Headers
+				// property is null or empty.
+				if (message == null || message.Headers == null || message.Headers == "")
+					return null;
+				
+				++this.count;
+
+				string uid;
+				uint flags;
+				if (!TryGetMessageInfo (message, offset, path, out uid, out flags)) {
+					return null;
+				}
+
+				Indexable indexable = this.GMimeMessageToIndexable (uid, message, flags);
+
+				if (Debug) {
+					Logger.Log.Debug ("Constructed message {0} with uid {1}, flags {2}.  Indexable {3} null",
+							  this.count, uid, flags, indexable == null ? "" : "not");
+				}
+				
+				if (indexable == null)
+					return null;
+
+				++this.indexed_count;
+
+				return indexable;
+			}
+		}
+
+		protected virtual bool TryGetMessageInfo (GMime.Message message, long offset, string path, out string uid, out uint flags)
+		{
+			uid = null;
+			flags = 0;
+			return false;
+		}
+
+		private Indexable GMimeMessageToIndexable (string uid, GMime.Message message, uint flags)
+		{
+			// Don't index messages flagged as junk
+			if (CheckFlags (flags, B_U_Camel.CamelFlags.Junk))
+				return null;
+
+			System.Uri uri = EvolutionMailQueryable.EmailUri (this.account_name, this.folder_name, uid);
+			Indexable indexable = new Indexable (uri);
+
+			indexable.Timestamp = message.Date.ToUniversalTime ();
+			indexable.HitType = "MailMessage";
+			indexable.MimeType = "message/rfc822";
+			indexable.CacheContent = true;
+
+			indexable.AddProperty (Property.NewUnsearched ("fixme:client", "evolution"));
+			indexable.AddProperty (Property.NewUnsearched ("fixme:account", "Local"));
+                        indexable.AddProperty (Property.NewUnsearched ("fixme:folder", this.folder_name));
+
+			GMime.InternetAddressList addrs;
+
+			addrs = message.GetRecipients (GMime.Message.RecipientType.To);
+			foreach (GMime.InternetAddress ia in addrs) {
+				if (this.folder_name == "Sent" && ia.AddressType != GMime.InternetAddressType.Group)
+					indexable.AddProperty (Property.NewUnsearched ("fixme:sentTo", ia.Addr));
+			}
+			addrs.Dispose ();
+
+			addrs = message.GetRecipients (GMime.Message.RecipientType.Cc);
+			foreach (GMime.InternetAddress ia in addrs) {
+				if (this.folder_name == "Sent"  && ia.AddressType != GMime.InternetAddressType.Group)
+					indexable.AddProperty (Property.NewUnsearched ("fixme:sentTo", ia.Addr));
+			}
+			addrs.Dispose ();
+
+			addrs = GMime.InternetAddressList.ParseString (GMime.Utils.HeaderDecodePhrase (message.Sender));
+			foreach (GMime.InternetAddress ia in addrs) {
+				if (this.folder_name != "Sent"  && ia.AddressType != GMime.InternetAddressType.Group)
+					indexable.AddProperty (Property.NewUnsearched ("fixme:gotFrom", ia.Addr));
+			}
+			addrs.Dispose ();
+
+			if (this.folder_name == "Sent")
+				indexable.AddProperty (Property.NewFlag ("fixme:isSent"));
+
+	                Property flag_prop = Property.NewUnsearched ("fixme:flags", flags);
+			flag_prop.IsMutable = true;
+			indexable.AddProperty (flag_prop);
+
+			if (CheckFlags (flags, B_U_Camel.CamelFlags.Answered))
+				indexable.AddProperty (Property.NewFlag ("fixme:isAnswered"));
+
+			if (CheckFlags (flags, B_U_Camel.CamelFlags.Deleted))
+				indexable.AddProperty (Property.NewFlag ("fixme:isDeleted"));
+
+			if (CheckFlags (flags, B_U_Camel.CamelFlags.Draft))
+				indexable.AddProperty (Property.NewFlag ("fixme:isDraft"));
+
+			if (CheckFlags (flags, B_U_Camel.CamelFlags.Flagged))
+				indexable.AddProperty (Property.NewFlag ("fixme:isFlagged"));
+
+			if (CheckFlags (flags, B_U_Camel.CamelFlags.Seen))
+				indexable.AddProperty (Property.NewFlag ("fixme:isSeen"));
+
+			if (CheckFlags (flags, B_U_Camel.CamelFlags.AnsweredAll))
+				indexable.AddProperty (Property.NewFlag ("fixme:isAnsweredAll"));
+
+			indexable.SetBinaryStream (message.Stream);
+
+			return indexable;
+		}
+
+		private static bool CheckFlags (uint flags, B_U_Camel.CamelFlags test)
+		{
+			return (flags & (uint) test) == (uint) test;
+		}
 
 		protected double progress_percent;
 		public double ProgressPercent {
@@ -94,6 +216,84 @@ namespace Beagle.Daemon.EvolutionMailQue
 			}
 		}
 
+		protected static string GetGConfAccountName (string backend_prefix, string name)
+		{
+			string account_name = null;
+			ICollection accounts = null;
+
+			try {
+				accounts = (ICollection) GConfThreadHelper.Get ("/apps/evolution/mail/accounts");
+			} catch (Exception ex) {
+				Logger.Log.Warn ("Caught exception in Setup(): " + ex.Message);
+				Logger.Log.Warn ("There are no configured evolution accounts, ignoring {0}", name);
+				return null;
+			}
+
+			// This should only happen if we shut down while waiting for the GConf results to come back.
+			if (accounts == null)
+				return null;
+
+			foreach (string xml in accounts) {
+				XmlDocument xmlDoc = new XmlDocument ();
+
+				xmlDoc.LoadXml (xml);
+				
+				XmlNode account = xmlDoc.SelectSingleNode ("//account");
+
+				if (account == null)
+					continue;
+
+				string uid = null;
+
+				foreach (XmlAttribute attr in account.Attributes) {
+					if (attr.Name == "uid") {
+						uid = attr.InnerText;
+						break;
+					}
+				}
+
+				if (uid == null)
+					continue;
+
+				XmlNode imap_url_node = xmlDoc.SelectSingleNode ("//source/url");
+
+				if (imap_url_node == null)
+					continue;
+
+				string imap_url = imap_url_node.InnerText;
+				// If there is a semicolon in the username part of the URL, it
+				// indicates that there's an auth scheme there.  We don't care
+				// about that, so remove it.
+				int user_end = imap_url.IndexOf ('@');
+				int semicolon = imap_url.IndexOf (';', 0, user_end + 1);
+
+				if (semicolon != -1)
+					imap_url = imap_url.Substring (0, semicolon) + imap_url.Substring (user_end);
+
+				if (imap_url.StartsWith (backend_prefix + "://" + name + "/")) {
+					account_name = uid;
+					break;
+				}
+			}
+
+			return account_name;
+		}
+
+		protected static string EscapeName (string name)
+		{
+			// Escape backslashes, which frequently appear when using IMAP against Exchange servers
+			name = name.Replace ("\\", "%5c");
+
+			// Escape out additional @s in the name.  I hate the class libs so much.
+			int lastIdx = name.LastIndexOf ('@');
+			if (name.IndexOf ('@') != lastIdx) {
+				string toEscape = name.Substring (0, lastIdx);
+				name = toEscape.Replace ("@", "%40") + name.Substring (lastIdx);
+			}
+
+			return name;
+		}
+
 		protected bool IsSpamFolder (string name)
 		{
 			if (name.ToLower () == "spam" || name.ToLower () == "junk")
@@ -261,128 +461,28 @@ namespace Beagle.Daemon.EvolutionMailQue
 
 		public override Indexable GetNextIndexable ()
 		{
-			using (GMime.Message message = this.mbox_parser.ConstructMessage ()) {
-				// Work around what I think is a bug in GMime: If you
-				// have a zero-byte file or seek to the end of a
-				// file, parser.Eos () will return true until it
-				// actually tries to read something off the wire.
-				// Since parser.ConstructMessage() always returns a
-				// message (which may also be a bug), we'll often get
-				// one empty message which we need to deal with here.
-				//
-				// Check if its empty by seeing if the Headers
-				// property is null or empty.
-				if (message == null || message.Headers == null || message.Headers == "")
-					return null;
-				
-				++this.count;
-
-				string x_evolution = message.GetHeader ("X-Evolution");
-				if (x_evolution == null || x_evolution == "") {
-					Logger.Log.Info ("{0}: Message at offset {1} has no X-Evolution header!",
-							 this.folder_name, this.mbox_parser.FromOffset);
-					return null;
-				}
-				
-				// This extracts the UID and flags from the X-Evolution header.
-				// It may also contain user-defined flags and tags, but we don't
-				// support those right now.
-				int separator_idx = x_evolution.IndexOf ('-');
-
-				string uid_str = x_evolution.Substring (0, separator_idx);
-				string uid = Convert.ToUInt32 (uid_str, 16).ToString (); // ugh.
-				uint flags = Convert.ToUInt32 (x_evolution.Substring (separator_idx + 1, 4), 16);
-
-				Indexable indexable = this.GMimeMessageToIndexable (uid, message, flags);
-
-				if (Debug) {
-					Logger.Log.Debug ("Constructed message {0} with uid {1}, flags {2}.  Indexable {3} null",
-							  this.count, uid, flags, indexable == null ? "" : "not");
-				}
-				
-				if (indexable == null)
-					return null;
-
-				++this.indexed_count;
-
-				return indexable;
-			}
-		}
-
-		private static bool CheckFlags (uint flags, B_U_Camel.CamelFlags test)
-		{
-			return (flags & (uint) test) == (uint) test;
+			return GetNextIndexable (this.mbox_parser.ConstructMessage (), this.mbox_parser.FromOffset, null);
 		}
 
-		private Indexable GMimeMessageToIndexable (string uid, GMime.Message message, uint flags)
+		protected override bool TryGetMessageInfo (GMime.Message message, long offset, string path, out string uid, out uint flags)
 		{
-			// Don't index messages flagged as junk
-			if (CheckFlags (flags, B_U_Camel.CamelFlags.Junk))
-				return null;
-
-			System.Uri uri = EvolutionMailQueryable.EmailUri (this.account_name, this.folder_name, uid);
-			Indexable indexable = new Indexable (uri);
-
-			indexable.Timestamp = message.Date.ToUniversalTime ();
-			indexable.HitType = "MailMessage";
-			indexable.MimeType = "message/rfc822";
-			indexable.CacheContent = true;
-
-			indexable.AddProperty (Property.NewUnsearched ("fixme:client", "evolution"));
-			indexable.AddProperty (Property.NewUnsearched ("fixme:account", "Local"));
-                        indexable.AddProperty (Property.NewUnsearched ("fixme:folder", this.folder_name));
-
-			GMime.InternetAddressList addrs;
-
-			addrs = message.GetRecipients (GMime.Message.RecipientType.To);
-			foreach (GMime.InternetAddress ia in addrs) {
-				if (this.folder_name == "Sent" && ia.AddressType != GMime.InternetAddressType.Group)
-					indexable.AddProperty (Property.NewUnsearched ("fixme:sentTo", ia.Addr));
-			}
-			addrs.Dispose ();
-
-			addrs = message.GetRecipients (GMime.Message.RecipientType.Cc);
-			foreach (GMime.InternetAddress ia in addrs) {
-				if (this.folder_name == "Sent"  && ia.AddressType != GMime.InternetAddressType.Group)
-					indexable.AddProperty (Property.NewUnsearched ("fixme:sentTo", ia.Addr));
-			}
-			addrs.Dispose ();
-
-			addrs = GMime.InternetAddressList.ParseString (GMime.Utils.HeaderDecodePhrase (message.Sender));
-			foreach (GMime.InternetAddress ia in addrs) {
-				if (this.folder_name != "Sent"  && ia.AddressType != GMime.InternetAddressType.Group)
-					indexable.AddProperty (Property.NewUnsearched ("fixme:gotFrom", ia.Addr));
+			string x_evolution = message.GetHeader ("X-Evolution");
+			if (x_evolution == null || x_evolution == "") {
+				Logger.Log.Info ("{0}: Message at offset {1} has no X-Evolution header!",
+						 this.folder_name, offset);
+				uid = null; flags = 0;
+				return false;
 			}
-			addrs.Dispose ();
-
-			if (this.folder_name == "Sent")
-				indexable.AddProperty (Property.NewFlag ("fixme:isSent"));
-
-	                Property flag_prop = Property.NewUnsearched ("fixme:flags", flags);
-			flag_prop.IsMutable = true;
-			indexable.AddProperty (flag_prop);
-
-			if (CheckFlags (flags, B_U_Camel.CamelFlags.Answered))
-				indexable.AddProperty (Property.NewFlag ("fixme:isAnswered"));
-
-			if (CheckFlags (flags, B_U_Camel.CamelFlags.Deleted))
-				indexable.AddProperty (Property.NewFlag ("fixme:isDeleted"));
-
-			if (CheckFlags (flags, B_U_Camel.CamelFlags.Draft))
-				indexable.AddProperty (Property.NewFlag ("fixme:isDraft"));
-
-			if (CheckFlags (flags, B_U_Camel.CamelFlags.Flagged))
-				indexable.AddProperty (Property.NewFlag ("fixme:isFlagged"));
-
-			if (CheckFlags (flags, B_U_Camel.CamelFlags.Seen))
-				indexable.AddProperty (Property.NewFlag ("fixme:isSeen"));
-
-			if (CheckFlags (flags, B_U_Camel.CamelFlags.AnsweredAll))
-				indexable.AddProperty (Property.NewFlag ("fixme:isAnsweredAll"));
-
-			indexable.SetBinaryStream (message.Stream);
-
-			return indexable;
+			
+			// This extracts the UID and flags from the X-Evolution header.
+			// It may also contain user-defined flags and tags, but we don't
+			// support those right now.
+			int separator_idx = x_evolution.IndexOf ('-');
+
+			string uid_str = x_evolution.Substring (0, separator_idx);
+			uid = Convert.ToUInt32 (uid_str, 16).ToString (); // ugh.
+			flags = Convert.ToUInt32 (x_evolution.Substring (separator_idx + 1, 4), 16);
+			return true;
 		}
 
 		public override void Checkpoint ()
@@ -477,79 +577,8 @@ namespace Beagle.Daemon.EvolutionMailQue
 			string imap_start = dir_name.Substring (imap_start_idx);
 			this.imap_name = imap_start.Substring (0, imap_start.IndexOf ('/'));
 
-			ICollection accounts = null;
-
-			try {
-				accounts = (ICollection) GConfThreadHelper.Get ("/apps/evolution/mail/accounts");
-			} catch (Exception ex) {
-				Logger.Log.Warn ("Caught exception in Setup(): " + ex.Message);
-				Logger.Log.Warn ("There are no configured evolution accounts, ignoring {0}", this.imap_name);
-				return false;
-			}
-
-			// This should only happen if we shut down while waiting for the GConf results to come back.
-			if (accounts == null)
-				return false;
-
-			foreach (string xml in accounts) {
-				XmlDocument xmlDoc = new XmlDocument ();
-
-				xmlDoc.LoadXml (xml);
-				
-				XmlNode account = xmlDoc.SelectSingleNode ("//account");
-
-				if (account == null)
-					continue;
-
-				string uid = null;
-
-				foreach (XmlAttribute attr in account.Attributes) {
-					if (attr.Name == "uid") {
-						uid = attr.InnerText;
-						break;
-					}
-				}
-
-				if (uid == null)
-					continue;
-
-				XmlNode imap_url_node = xmlDoc.SelectSingleNode ("//source/url");
-
-				if (imap_url_node == null)
-					continue;
-
-				string imap_url = imap_url_node.InnerText;
-				// If there is a semicolon in the username part of the URL, it
-				// indicates that there's an auth scheme there.  We don't care
-				// about that, so remove it.
-				int user_end = imap_url.IndexOf ('@');
-				int semicolon = imap_url.IndexOf (';', 0, user_end + 1);
-
-				if (semicolon != -1)
-					imap_url = imap_url.Substring (0, semicolon) + imap_url.Substring (user_end);
-
-				// Escape backslashes, which frequently appear when using IMAP against Exchange servers
-				this.imap_name = this.imap_name.Replace ("\\", "%5c");
-
-				// Escape out additional @s in the name.  I hate the class libs so much.
-				int lastIdx = this.imap_name.LastIndexOf ('@');
-				if (this.imap_name.IndexOf ('@') != lastIdx) {
-					string toEscape = this.imap_name.Substring (0, lastIdx);
-					this.imap_name = toEscape.Replace ("@", "%40") + this.imap_name.Substring (lastIdx);
-				}
-
-				string backend_url_prefix;
-				if (this.backend_type == ImapBackendType.Imap)
-					backend_url_prefix = "imap";
-				else
-					backend_url_prefix = "imap4";
-
-				if (imap_url.StartsWith (backend_url_prefix + "://" + this.imap_name + "/")) {
-					this.account_name = uid;
-					break;
-				}
-			}
-
+			this.imap_name = EscapeName (this.imap_name);
+			this.account_name = GetGConfAccountName (this.backend_type == ImapBackendType.Imap ? "imap" : "imap4", this.imap_name);
 			if (this.account_name == null) {
 				Logger.Log.Info ("Unable to determine account name for {0}", this.imap_name);
 				return false;
Index: beagled/EvolutionMailQueryable/EvolutionMailQueryable.cs
===================================================================
--- beagled/EvolutionMailQueryable/EvolutionMailQueryable.cs.orig	2008-04-25 12:17:04.000000000 -0600
+++ beagled/EvolutionMailQueryable/EvolutionMailQueryable.cs	2011-07-01 11:34:33.799000148 -0600
@@ -39,7 +39,7 @@ namespace Beagle.Daemon.EvolutionMailQue
 
 		public int polling_interval_in_seconds = 60;
 
-		private string local_path, imap_path, imap4_path;
+		private string local_path, imap_path, imap4_path, groupwise_path;
 
 		private MailCrawler crawler;
 
@@ -62,6 +62,7 @@ namespace Beagle.Daemon.EvolutionMailQue
 			this.local_path = Path.Combine (PathFinder.HomeDir, ".evolution/mail/local");
 			this.imap_path = Path.Combine (PathFinder.HomeDir, ".evolution/mail/imap");
 			this.imap4_path = Path.Combine (PathFinder.HomeDir, ".evolution/mail/imap4");
+			this.groupwise_path = Path.Combine (PathFinder.HomeDir, ".evolution/mail/groupwise");
 		}
 
 		private void CrawlHook (Scheduler.Task task)
@@ -89,9 +90,10 @@ namespace Beagle.Daemon.EvolutionMailQue
 			}
 
 			Logger.Log.Debug ("Starting mail crawl");
-			crawler = new MailCrawler (this.local_path, this.imap_path, this.imap4_path);
+			crawler = new MailCrawler (this.local_path, this.imap_path, this.imap4_path, this.groupwise_path);
 			crawler.MboxAddedEvent += IndexMbox;
 			crawler.SummaryAddedEvent += IndexSummary;
+			crawler.GroupWiseAddedEvent += IndexGroupWise;
 			crawler.Crawl ();
 			Logger.Log.Debug ("Mail crawl finished");
 
@@ -143,6 +145,25 @@ namespace Beagle.Daemon.EvolutionMailQue
 			Scheduler.Task task;
 			task = NewAddTask (generator);
 			task.Tag = summaryInfo.FullName;
+			ThisScheduler.Add (task);
+
+			AddGenerator (generator, inotify_event);
+		}
+
+		public void IndexGroupWise (FileInfo summaryInfo, bool inotify_event)
+		{
+			// If there's already a task running for this folder,
+			// don't interrupt it.
+			if (ThisScheduler.ContainsByTag (summaryInfo.FullName)) {
+				Logger.Log.Debug ("Not adding task for already running task: {0}", summaryInfo.FullName);
+				return;
+			}
+
+			Logger.Log.Debug ("Will index summary {0}", summaryInfo.FullName);
+			GroupWiseGenerator generator = new GroupWiseGenerator (this, summaryInfo);
+			Scheduler.Task task;
+			task = NewAddTask (generator);
+			task.Tag = summaryInfo.FullName;
 			ThisScheduler.Add (task);
 
 			AddGenerator (generator, inotify_event);
Index: beagled/EvolutionMailQueryable/GroupWiseGenerator.cs
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ beagled/EvolutionMailQueryable/GroupWiseGenerator.cs	2011-07-01 11:34:33.807000148 -0600
@@ -0,0 +1,240 @@
+//
+// GroupWiseGenerator.cs
+//
+// Copyright (C) 2009 Novell, Inc.
+//
+//
+//
+// 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 System.Collections.Generic;
+using System.IO;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Threading;
+using System.Xml;
+
+using Beagle.Util;
+using Beagle.Daemon;
+
+namespace Beagle.Daemon.EvolutionMailQueryable
+{
+	public class GroupWiseGenerator : EvolutionMailIndexableGenerator
+	{
+		private const string GW_DIR = ".evolution/mail/groupwise/";
+
+		private FileInfo cmeta;
+		private DirectoryInfo cache_dir;
+		private IEnumerator<string> enumerator;
+
+		private EvolutionSummaryTracker tracker;
+		private DateTime start_crawl_time;
+		private ArrayList deleted_list;
+		private bool delete_mode;
+		private int delete_count;
+
+		public GroupWiseGenerator (EvolutionMailQueryable queryable, FileInfo cmeta) : base (queryable)
+		{
+			this.cmeta = cmeta;
+		}
+
+		protected override string GetFolderName (FileSystemInfo info)
+		{
+			if (folder_name == null) {
+				folder_name = "";
+
+				DirectoryInfo dir_info = cmeta.Directory;
+				while (dir_info != null && dir_info.Name != "folders") {
+					folder_name = Path.Combine (dir_info.Name, folder_name);
+					dir_info = dir_info.Parent;
+				}
+			}
+
+			return folder_name;
+		}
+
+		protected override bool Setup ()
+		{
+			// cmeta is something like /home/foo/.evolution/mail/groupwise/foo\@bar.com/folders/Mailbox/cmeta
+			string dir = cmeta.DirectoryName;
+
+			// Get the account directory name out of it
+			string account = dir.Substring (dir.IndexOf (GW_DIR) + GW_DIR.Length);
+			account = EscapeName (account.Substring (0, account.IndexOf ('/')));
+
+			// Make sure this account is configured in GConf
+			account_name = GetGConfAccountName ("groupwise", account);
+			if (account_name == null) {
+				Logger.Log.Info ("Unable to determine account name for {0}", account);
+				return false;
+			}
+
+			// Need to check the directory on disk to see if it's a junk/spam folder,
+			// since the folder name will be "foo/spam" and not match the check below.
+			DirectoryInfo dir_info = cmeta.Directory;
+			if (IsSpamFolder (dir_info.Name))
+				return false;
+
+			// Check if the folder is listed in the configuration as to be excluded from indexing
+			if (IgnoreFolder (dir_info.FullName))
+				return false;
+					
+			cache_dir = new DirectoryInfo (Path.Combine (dir, "cache"));
+			if (!cache_dir.Exists)
+				return false;
+
+			GetFolderName (dir_info);
+
+			return true;
+		}
+
+		public override bool HasNextIndexable ()
+		{
+			if (this.account_name == null) {
+				if (!Setup ()) {
+					this.queryable.RemoveGenerator (this);
+					return false;
+				}
+			}
+
+			if (this.tracker == null) {
+				if (this.queryable.FileAttributesStore.IsUpToDate (this.CrawlFile.FullName)) {
+					Logger.Log.Debug ("{0}: summary has not been updated; crawl unncessary", this.folder_name);
+					this.queryable.RemoveGenerator (this);
+					return false;
+				}
+
+				this.tracker = new EvolutionSummaryTracker (this.queryable.IndexDirectory, this.account_name, this.folder_name);
+				this.start_crawl_time = DateTime.UtcNow;
+			}
+
+			if (this.queryable.FileAttributesStore.IsUpToDate (cmeta.FullName)) {
+				return false;
+			}
+
+			if (enumerator == null) {
+				enumerator = CacheFileEnumerable ().GetEnumerator ();
+			}
+
+			if (enumerator.MoveNext ())
+				return true;
+
+			if (this.deleted_list == null) {
+				this.deleted_list = this.tracker.GetOlderThan (this.start_crawl_time);
+			}
+
+			if (this.deleted_list.Count > 0) {
+				this.delete_mode = true;
+				return true;
+			}
+
+			this.delete_mode = false;
+			this.deleted_list = null;
+
+			Logger.Log.Debug ("{0}: Finished indexing {1} messages, {2} messages deleted", this.folder_name, this.indexed_count, this.delete_count);
+
+			this.tracker.Close ();
+			this.tracker = null;
+			this.CrawlFinished ();
+
+			return false;
+		}
+
+		public override Indexable GetNextIndexable ()
+		{
+			// No more new messages to index, so start on the removals.
+			if (this.delete_mode) {
+				string uid = (string) this.deleted_list [0];
+				Uri uri = EvolutionMailQueryable.EmailUri (this.account_name, this.folder_name, uid);
+				
+				Indexable indexable = new Indexable (IndexableType.Remove, uri);
+
+				this.deleted_list.RemoveAt (0);
+				this.tracker.Remove (uid);
+
+				this.delete_count++;
+
+				return indexable;
+			}
+
+			string path = enumerator.Current;
+			if (path != null) {
+				Indexable indexable = GetNextIndexable (GetMessage (path), 0, path);
+				this.tracker.Update (Path.GetFileName (path), 0);
+				return indexable;
+			}
+
+			return null;
+		}
+
+		protected override bool TryGetMessageInfo (GMime.Message message, long offset, string path, out string uid, out uint flags)
+		{
+			uid = Path.GetFileName (path);
+			flags = 0;
+			return true;
+		}
+
+		private IEnumerable<string> CacheFileEnumerable ()
+		{
+			foreach (string subdir in DirectoryWalker.GetDirectories (cache_dir)) {
+				foreach (string path in DirectoryWalker.GetItems (subdir, null)) {
+					yield return path;
+				}
+			}
+		}
+
+		public override void Checkpoint ()
+		{
+			if (this.enumerator != null) {
+				Logger.Log.Debug ("{0}: indexed {1} messages", this.folder_name, this.indexed_count);
+			}
+			
+			if (this.tracker != null)
+				this.tracker.Checkpoint ();
+		}
+
+		protected override void CrawlFinished ()
+		{
+			this.queryable.FileAttributesStore.AttachLastWriteTime (this.CrawlFile.FullName, this.start_crawl_time);
+			base.CrawlFinished ();
+		}
+
+		public override string GetTarget ()
+		{
+			return "mbox-cache-directory:" + cache_dir.FullName;
+		}
+
+		protected override FileInfo CrawlFile {
+			get { return cmeta; }
+		}
+
+		private static GMime.Message GetMessage (string path)
+		{
+			InitializeGMime ();
+			var fd = Mono.Unix.Native.Syscall.open (path, Mono.Unix.Native.OpenFlags.O_RDONLY);
+			using (var stream = new GMime.StreamFs (fd)) {
+				using (var parser = new GMime.Parser (stream)) {
+					return parser.ConstructMessage ();
+				}
+			}
+		}
+	}
+}
Index: beagled/EvolutionMailQueryable/MailCrawler.cs
===================================================================
--- beagled/EvolutionMailQueryable/MailCrawler.cs.orig	2007-11-26 18:50:01.000000000 -0700
+++ beagled/EvolutionMailQueryable/MailCrawler.cs	2011-07-01 11:34:33.808000148 -0600
@@ -45,6 +45,7 @@ namespace Beagle.Daemon.EvolutionMailQue
 
 		public ItemAddedHandler MboxAddedEvent;
 		public ItemAddedHandler SummaryAddedEvent;
+		public ItemAddedHandler GroupWiseAddedEvent;
 		
 		public MailCrawler (params string[] paths)
 		{
@@ -96,6 +97,9 @@ namespace Beagle.Daemon.EvolutionMailQue
 						Logger.Log.Info ("Reindexing updated IMAP summary: {0}", full_path);
 						SummaryAddedEvent (new FileInfo (full_path), true);
 					}
+				} else if (subitem == "cmeta" && path.Contains ("gropwise")) {
+					if (GroupWiseAddedEvent != null && File.Exists (full_path))
+						GroupWiseAddedEvent (new FileInfo (full_path), true);
 				} else if (Path.GetExtension (full_path) == ".ev-summary") {
 					// mbox summary
 					string mbox_file = Path.ChangeExtension (full_path, null);
@@ -144,6 +148,9 @@ namespace Beagle.Daemon.EvolutionMailQue
 					if (file.Name == "summary") {
 						if (SummaryAddedEvent != null && FileIsInteresting (file))
 							SummaryAddedEvent (file, false);
+					} else if (file.Name == "cmeta") {
+						if (GroupWiseAddedEvent != null && FileIsInteresting (file))
+							GroupWiseAddedEvent (file, false);
 					} else if (file.Extension == ".ev-summary") {
 						string mbox_name = Path.Combine (file.DirectoryName,
 										 Path.GetFileNameWithoutExtension (file.Name));
@@ -163,6 +170,9 @@ namespace Beagle.Daemon.EvolutionMailQue
 			if (name == "summary")
 				return true;
 
+			if (path.Contains ("groupwise") && name == "cmeta")
+				return true;
+
 			if (Path.GetExtension (name) == ".ev-summary")
 				return true;
 
Index: beagled/Makefile.am
===================================================================
--- beagled/Makefile.am.orig	2011-07-01 11:34:24.946000149 -0600
+++ beagled/Makefile.am	2011-07-01 11:34:33.809000148 -0600
@@ -356,6 +356,7 @@ EVO_MAIL_QUERYABLE_CSFILES =					\
 	$(evomailqueryable)/EvolutionMailIndexableGenerator.cs	\
 	$(evomailqueryable)/EvolutionSummaryTracker.cs		\
 	$(evomailqueryable)/GConfThreadHelper.cs		\
+	$(evomailqueryable)/GroupWiseGenerator.cs	\
 	$(evomailqueryable)/MailCrawler.cs
 
 EVO_DLL_TARGET = EvolutionBackends.dll
