QueryResponses.cs, DumpIndex.cs, IQueryResult.cs, QueryExecutor.cs, QueryResult.cs...
[beagle.git] / beagled / EvolutionMailDriver / EvolutionMailIndexableGenerator.cs
blobaee1d47b9ea5012c7bdf7c5cd5daaed137f340d5
2 //
3 // EvolutionMailIndexableGenerator.cs
4 //
5 // Copyright (C) 2004 Novell, Inc.
6 //
7 //
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining a
10 // copy of this software and associated documentation files (the "Software"),
11 // to deal in the Software without restriction, including without limitation
12 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
13 // and/or sell copies of the Software, and to permit persons to whom the
14 // Software is furnished to do so, subject to the following conditions:
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 // DEALINGS IN THE SOFTWARE.
28 using System;
29 using System.Collections;
30 using System.IO;
31 using System.Runtime.Serialization.Formatters.Binary;
32 using System.Threading;
33 using System.Xml;
35 using Beagle.Util;
36 using Beagle.Daemon;
38 using Camel = Beagle.Util.Camel;
40 namespace Beagle.Daemon.EvolutionMailDriver {
42 public abstract class EvolutionMailIndexableGenerator : IIndexableGenerator {
44 protected static bool Debug = false;
46 private static bool gmime_initialized = false;
48 private static ArrayList excludes = new ArrayList ();
50 static EvolutionMailIndexableGenerator () {
51 foreach (ExcludeItem exclude in Conf.Indexing.Excludes)
52 if (exclude.Type == ExcludeType.MailFolder)
53 excludes.Add (exclude);
56 protected EvolutionMailQueryable queryable;
58 protected string account_name, folder_name;
59 protected int count, indexed_count;
61 protected EvolutionMailIndexableGenerator (EvolutionMailQueryable queryable)
63 this.queryable = queryable;
66 protected abstract string GetFolderName (FileSystemInfo info);
67 protected abstract bool Setup ();
68 protected abstract FileInfo CrawlFile { get; }
69 public abstract string GetTarget ();
70 public abstract bool HasNextIndexable ();
71 public abstract Indexable GetNextIndexable ();
72 public abstract void Checkpoint ();
74 public void PostFlushHook ()
76 Checkpoint ();
79 protected static void InitializeGMime ()
81 if (!gmime_initialized) {
82 GMime.Global.Init ();
83 gmime_initialized = true;
87 protected bool IsSpamFolder (string name)
89 if (name.ToLower () == "spam" || name.ToLower () == "junk")
90 return true;
91 else
92 return false;
95 protected bool IgnoreFolder (string path)
97 // FIXME: Use System.IO.Path
98 foreach (ExcludeItem exclude in excludes) {
99 if (exclude.IsMatch (path))
100 return true;
102 return false;
105 protected void CrawlFinished ()
107 // FIXME: This is a little sketchy
108 this.queryable.FileAttributesStore.AttachLastWriteTime (this.CrawlFile.FullName, DateTime.UtcNow);
110 this.queryable.SetGeneratorProgress (this, 100);
113 public string StatusName {
114 get { return this.CrawlFile.FullName; }
117 public override bool Equals (object o)
119 EvolutionMailIndexableGenerator generator = o as EvolutionMailIndexableGenerator;
121 if (generator == null)
122 return false;
124 if (Object.ReferenceEquals (this, generator))
125 return true;
127 if (this.CrawlFile.FullName == generator.CrawlFile.FullName)
128 return true;
129 else
130 return false;
133 public override int GetHashCode ()
135 return this.CrawlFile.FullName.GetHashCode ();
139 public class EvolutionMailIndexableGeneratorMbox : EvolutionMailIndexableGenerator {
140 private FileInfo mbox_info;
141 private int mbox_fd = -1;
142 private GMime.StreamFs mbox_stream;
143 private GMime.Parser mbox_parser;
144 private long file_size;
146 public EvolutionMailIndexableGeneratorMbox (EvolutionMailQueryable queryable, FileInfo mbox_info) : base (queryable)
148 this.mbox_info = mbox_info;
151 protected override string GetFolderName (FileSystemInfo info)
153 FileInfo file_info = (FileInfo) info;
154 DirectoryInfo di;
155 string folder_name = "";
157 di = file_info.Directory;
158 while (di != null) {
159 // Evo uses ".sbd" as the extension on a folder
160 if (di.Extension == ".sbd")
161 folder_name = Path.Combine (Path.GetFileNameWithoutExtension (di.Name), folder_name);
162 else
163 break;
165 di = di.Parent;
168 return Path.Combine (folder_name, file_info.Name);
171 protected override bool Setup ()
173 this.account_name = "local@local";
174 this.folder_name = this.GetFolderName (this.mbox_info);
176 if (this.IsSpamFolder (this.folder_name))
177 return false;
179 if (this.IgnoreFolder (this.mbox_info.FullName))
180 return false;
182 return true;
185 private long MboxLastOffset {
186 get {
187 string offset_str = this.queryable.ReadDataLine ("offset-" + this.folder_name.Replace ('/', '-'));
188 long offset = Convert.ToInt64 (offset_str);
190 return offset;
193 set {
194 this.queryable.WriteDataLine ("offset-" + this.folder_name.Replace ('/', '-'), value.ToString ());
198 public override bool HasNextIndexable ()
200 if (this.account_name == null) {
201 if (!Setup ()) {
202 this.queryable.RemoveGeneratorProgress (this);
203 return false;
207 if (this.mbox_fd < 0) {
208 Logger.Log.Debug ("Opening mbox {0}", this.mbox_info.Name);
210 try {
211 InitializeGMime ();
212 } catch (Exception e) {
213 Logger.Log.Warn (e, "Caught exception trying to initalize gmime:");
214 return false;
217 this.mbox_fd = Mono.Unix.Native.Syscall.open (this.mbox_info.FullName, Mono.Unix.Native.OpenFlags.O_RDONLY);
219 if (this.mbox_fd < 0) {
220 Log.Error ("Unable to open {0}: {1}", this.mbox_info.FullName, Mono.Unix.Native.Stdlib.strerror (Mono.Unix.Native.Stdlib.GetLastError ()));
221 return false;
224 this.mbox_stream = new GMime.StreamFs (this.mbox_fd);
225 this.mbox_stream.Seek ((int) this.MboxLastOffset);
226 this.mbox_parser = new GMime.Parser (this.mbox_stream);
227 this.mbox_parser.ScanFrom = true;
229 FileInfo info = new FileInfo (this.mbox_info.FullName);
230 this.file_size = info.Length;
233 if (this.mbox_parser.Eos ()) {
234 long offset = this.mbox_parser.FromOffset;
236 this.mbox_stream.Close ();
238 this.mbox_fd = -1;
239 this.mbox_stream.Dispose ();
240 this.mbox_stream = null;
241 this.mbox_parser.Dispose ();
242 this.mbox_parser = null;
244 Logger.Log.Debug ("{0}: Finished indexing {1} messages", this.folder_name, this.indexed_count);
246 if (offset >= 0)
247 this.MboxLastOffset = offset;
248 this.CrawlFinished ();
250 return false;
251 } else
252 return true;
255 public override Indexable GetNextIndexable ()
257 using (GMime.Message message = this.mbox_parser.ConstructMessage ()) {
258 // Work around what I think is a bug in GMime: If you
259 // have a zero-byte file or seek to the end of a
260 // file, parser.Eos () will return true until it
261 // actually tries to read something off the wire.
262 // Since parser.ConstructMessage() always returns a
263 // message (which may also be a bug), we'll often get
264 // one empty message which we need to deal with here.
266 // Check if its empty by seeing if the Headers
267 // property is null or empty.
268 if (message == null || message.Headers == null || message.Headers == "")
269 return null;
271 ++this.count;
273 string x_evolution = message.GetHeader ("X-Evolution");
274 if (x_evolution == null || x_evolution == "") {
275 Logger.Log.Info ("{0}: Message at offset {1} has no X-Evolution header!",
276 this.folder_name, this.mbox_parser.FromOffset);
277 return null;
280 // This extracts the UID and flags from the X-Evolution header.
281 // It may also contain user-defined flags and tags, but we don't
282 // support those right now.
283 int separator_idx = x_evolution.IndexOf ('-');
285 string uid_str = x_evolution.Substring (0, separator_idx);
286 string uid = Convert.ToUInt32 (uid_str, 16).ToString (); // ugh.
287 uint flags = Convert.ToUInt32 (x_evolution.Substring (separator_idx + 1, 4), 16);
289 Indexable indexable = this.GMimeMessageToIndexable (uid, message, flags);
291 if (Debug) {
292 Logger.Log.Debug ("Constructed message {0} with uid {1}, flags {2}. Indexable {3} null",
293 this.count, uid, flags, indexable == null ? "" : "not");
296 if (indexable == null)
297 return null;
299 ++this.indexed_count;
301 return indexable;
305 private static bool CheckFlags (uint flags, Camel.CamelFlags test)
307 return (flags & (uint) test) == (uint) test;
310 private Indexable GMimeMessageToIndexable (string uid, GMime.Message message, uint flags)
312 // Don't index messages flagged as junk
313 if (CheckFlags (flags, Camel.CamelFlags.Junk))
314 return null;
316 System.Uri uri = EvolutionMailQueryable.EmailUri (this.account_name, this.folder_name, uid);
317 Indexable indexable = new Indexable (uri);
319 indexable.Timestamp = message.Date.ToUniversalTime ();
320 indexable.HitType = "MailMessage";
321 indexable.MimeType = "message/rfc822";
322 indexable.CacheContent = false;
324 indexable.AddProperty (Property.NewUnsearched ("fixme:client", "evolution"));
325 indexable.AddProperty (Property.NewUnsearched ("fixme:account", "Local"));
326 indexable.AddProperty (Property.NewUnsearched ("fixme:folder", this.folder_name));
328 GMime.InternetAddressList addrs;
330 addrs = message.GetRecipients (GMime.Message.RecipientType.To);
331 foreach (GMime.InternetAddress ia in addrs) {
332 if (this.folder_name == "Sent" && ia.AddressType != GMime.InternetAddressType.Group)
333 indexable.AddProperty (Property.NewUnsearched ("fixme:sentTo", ia.Addr));
335 addrs.Dispose ();
337 addrs = message.GetRecipients (GMime.Message.RecipientType.Cc);
338 foreach (GMime.InternetAddress ia in addrs) {
339 if (this.folder_name == "Sent" && ia.AddressType != GMime.InternetAddressType.Group)
340 indexable.AddProperty (Property.NewUnsearched ("fixme:sentTo", ia.Addr));
342 addrs.Dispose ();
344 addrs = GMime.InternetAddressList.ParseString (GMime.Utils.HeaderDecodePhrase (message.Sender));
345 foreach (GMime.InternetAddress ia in addrs) {
346 if (this.folder_name != "Sent" && ia.AddressType != GMime.InternetAddressType.Group)
347 indexable.AddProperty (Property.NewUnsearched ("fixme:gotFrom", ia.Addr));
349 addrs.Dispose ();
351 if (this.folder_name == "Sent")
352 indexable.AddProperty (Property.NewFlag ("fixme:isSent"));
354 Property flag_prop = Property.NewUnsearched ("fixme:flags", flags);
355 flag_prop.IsMutable = true;
356 indexable.AddProperty (flag_prop);
358 if (CheckFlags (flags, Camel.CamelFlags.Answered))
359 indexable.AddProperty (Property.NewFlag ("fixme:isAnswered"));
361 if (CheckFlags (flags, Camel.CamelFlags.Deleted))
362 indexable.AddProperty (Property.NewFlag ("fixme:isDeleted"));
364 if (CheckFlags (flags, Camel.CamelFlags.Draft))
365 indexable.AddProperty (Property.NewFlag ("fixme:isDraft"));
367 if (CheckFlags (flags, Camel.CamelFlags.Flagged))
368 indexable.AddProperty (Property.NewFlag ("fixme:isFlagged"));
370 if (CheckFlags (flags, Camel.CamelFlags.Seen))
371 indexable.AddProperty (Property.NewFlag ("fixme:isSeen"));
373 if (CheckFlags (flags, Camel.CamelFlags.AnsweredAll))
374 indexable.AddProperty (Property.NewFlag ("fixme:isAnsweredAll"));
376 indexable.SetBinaryStream (message.Stream);
378 return indexable;
381 public override void Checkpoint ()
383 long offset = -1;
385 if (this.mbox_parser != null) {
386 offset = this.mbox_parser.FromOffset;
388 if (offset >= 0)
389 this.MboxLastOffset = offset;
392 string progress = "";
393 if (this.file_size > 0 && offset > 0) {
394 progress = String.Format (" ({0}/{1} bytes {2:###.0}%)",
395 offset, this.file_size, 100.0 * offset / this.file_size);
397 this.queryable.SetGeneratorProgress (this, (int) (100.0 * offset / this.file_size));
400 Logger.Log.Debug ("{0}: indexed {1} messages{2}",
401 this.folder_name, this.indexed_count, progress);
404 public override string GetTarget ()
406 return "mbox-file:" + mbox_info.FullName;
409 protected override FileInfo CrawlFile {
410 get { return this.mbox_info; }
414 public class EvolutionMailIndexableGeneratorImap : EvolutionMailIndexableGenerator {
415 private enum ImapBackendType {
416 Imap,
417 Imap4
420 private FileInfo summary_info;
421 private string imap_name;
422 private ImapBackendType backend_type;
423 private Camel.Summary summary;
424 private IEnumerator summary_enumerator;
425 private ICollection accounts;
426 private string folder_cache_name;
427 private Hashtable mapping;
428 private ArrayList deleted_list;
429 private bool delete_mode;
430 private int delete_count;
432 public EvolutionMailIndexableGeneratorImap (EvolutionMailQueryable queryable, FileInfo summary_info) : base (queryable)
434 this.summary_info = summary_info;
437 protected override string GetFolderName (FileSystemInfo info)
439 DirectoryInfo dir_info = (DirectoryInfo) info;
440 string folder_name = "";
442 while (dir_info != null) {
443 folder_name = Path.Combine (dir_info.Name, folder_name);
445 dir_info = dir_info.Parent;
447 if (dir_info.Name != "subfolders")
448 break;
449 else
450 dir_info = dir_info.Parent;
453 return folder_name;
456 protected override bool Setup ()
458 string dir_name = summary_info.DirectoryName;
459 int imap_start_idx;
461 int idx = dir_name.IndexOf (".evolution/mail/imap4/");
463 if (idx >= 0) {
464 this.backend_type = ImapBackendType.Imap4;
465 imap_start_idx = idx + 22;
466 } else {
467 this.backend_type = ImapBackendType.Imap;
468 imap_start_idx = dir_name.IndexOf (".evolution/mail/imap/") + 21;
471 string imap_start = dir_name.Substring (imap_start_idx);
472 this.imap_name = imap_start.Substring (0, imap_start.IndexOf ('/'));
474 try {
475 this.accounts = (ICollection) GConfThreadHelper.Get ("/apps/evolution/mail/accounts");
476 } catch (Exception ex) {
477 Logger.Log.Warn ("Caught exception in Setup(): " + ex.Message);
478 Logger.Log.Warn ("There are no configured evolution accounts, ignoring {0}", this.imap_name);
479 return false;
482 // This should only happen if we shut down while waiting for the GConf results to come back.
483 if (this.accounts == null)
484 return false;
486 foreach (string xml in this.accounts) {
487 XmlDocument xmlDoc = new XmlDocument ();
489 xmlDoc.LoadXml (xml);
491 XmlNode account = xmlDoc.SelectSingleNode ("//account");
493 if (account == null)
494 continue;
496 string uid = null;
498 foreach (XmlAttribute attr in account.Attributes) {
499 if (attr.Name == "uid") {
500 uid = attr.InnerText;
501 break;
505 if (uid == null)
506 continue;
508 XmlNode imap_url_node = xmlDoc.SelectSingleNode ("//source/url");
510 if (imap_url_node == null)
511 continue;
513 string imap_url = imap_url_node.InnerText;
514 // If there is a semicolon in the username part of the URL, it
515 // indicates that there's an auth scheme there. We don't care
516 // about that, so remove it.
517 int user_end = imap_url.IndexOf ('@');
518 int semicolon = imap_url.IndexOf (';', 0, user_end + 1);
520 if (semicolon != -1)
521 imap_url = imap_url.Substring (0, semicolon) + imap_url.Substring (user_end);
523 // Escape backslashes, which frequently appear when using IMAP against Exchange servers
524 this.imap_name = this.imap_name.Replace ("\\", "%5c");
526 // Escape out additional @s in the name. I hate the class libs so much.
527 int lastIdx = this.imap_name.LastIndexOf ('@');
528 if (this.imap_name.IndexOf ('@') != lastIdx) {
529 string toEscape = this.imap_name.Substring (0, lastIdx);
530 this.imap_name = toEscape.Replace ("@", "%40") + this.imap_name.Substring (lastIdx);
533 string backend_url_prefix;
534 if (this.backend_type == ImapBackendType.Imap)
535 backend_url_prefix = "imap";
536 else
537 backend_url_prefix = "imap4";
539 if (imap_url.StartsWith (backend_url_prefix + "://" + this.imap_name + "/")) {
540 this.account_name = uid;
541 break;
545 if (this.account_name == null) {
546 Logger.Log.Info ("Unable to determine account name for {0}", this.imap_name);
547 return false;
550 // Need to check the directory on disk to see if it's a junk/spam folder,
551 // since the folder name will be "foo/spam" and not match the check below.
552 DirectoryInfo dir_info = new DirectoryInfo (dir_name);
553 if (this.IsSpamFolder (dir_info.Name))
554 return false;
556 // Check if the folder is listed in the configuration as to be excluded from indexing
557 if (this.IgnoreFolder (dir_info.FullName))
558 return false;
560 this.folder_name = GetFolderName (new DirectoryInfo (dir_name));
562 return true;
565 private string FolderCacheName {
566 get {
567 if (this.account_name == null || this.folder_name == null)
568 return null;
570 if (this.folder_cache_name == null)
571 this.folder_cache_name = "status-" + this.account_name + "-" + this.folder_name.Replace ('/', '-');
573 return this.folder_cache_name;
577 private bool LoadCache ()
579 Stream cacheStream;
580 BinaryFormatter formatter;
582 if (this.FolderCacheName == null) {
583 this.mapping = new Hashtable ();
584 return false;
587 try {
588 cacheStream = this.queryable.ReadDataStream (this.FolderCacheName);
589 formatter = new BinaryFormatter ();
590 this.mapping = formatter.Deserialize (cacheStream) as Hashtable;
591 cacheStream.Close ();
592 Logger.Log.Debug ("Successfully loaded previous crawled data from disk: {0}", this.FolderCacheName);
594 return true;
595 } catch {
596 this.mapping = new Hashtable ();
598 return false;
602 private void SaveCache ()
604 Stream cacheStream;
605 BinaryFormatter formatter;
607 if (this.FolderCacheName == null)
608 return;
610 cacheStream = this.queryable.WriteDataStream (this.FolderCacheName);
611 formatter = new BinaryFormatter ();
612 formatter.Serialize (cacheStream, mapping);
613 cacheStream.Close ();
616 public override bool HasNextIndexable ()
618 if (this.account_name == null) {
619 if (!Setup ()) {
620 this.queryable.RemoveGeneratorProgress (this);
621 return false;
625 if (this.mapping == null) {
626 bool cache_loaded = this.LoadCache ();
628 this.deleted_list = new ArrayList (this.mapping.Keys);
629 this.deleted_list.Sort ();
630 Logger.Log.Debug ("Deleted list starting at {0} for {1}", this.deleted_list.Count, this.folder_name);
632 // Check to see if we even need to bother walking the summary
633 if (cache_loaded && this.queryable.FileAttributesStore.IsUpToDate (this.CrawlFile.FullName)) {
634 Logger.Log.Debug ("{0}: summary has not been updated; crawl unncessary", this.folder_name);
635 this.queryable.RemoveGeneratorProgress (this);
636 return false;
640 if (this.summary == null) {
641 try {
642 if (this.backend_type == ImapBackendType.Imap)
643 this.summary = Camel.Summary.LoadImapSummary (this.summary_info.FullName);
644 else
645 this.summary = Camel.Summary.LoadImap4Summary (this.summary_info.FullName);
646 } catch (Exception e) {
647 Logger.Log.Warn (e, "Unable to index {0}:", this.folder_name);
648 this.queryable.RemoveGeneratorProgress (this);
649 return false;
653 if (this.summary_enumerator == null)
654 this.summary_enumerator = this.summary.GetEnumerator ();
656 if (this.summary_enumerator.MoveNext ())
657 return true;
659 this.delete_mode = true;
661 if (this.deleted_list.Count > 0)
662 return true;
664 string progress = "";
665 if (this.count > 0 && this.summary.header.count > 0) {
666 progress = String.Format ("({0}/{1} {2:###.0}%)",
667 this.count,
668 this.summary.header.count,
669 100.0 * this.count / this.summary.header.count);
672 Logger.Log.Debug ("{0}: Finished indexing {1} messages {2}, {3} messages deleted", this.folder_name, this.indexed_count, progress, this.delete_count);
674 this.SaveCache ();
675 this.CrawlFinished ();
677 return false;
680 // Kind of nasty, but we need the function.
681 [System.Runtime.InteropServices.DllImport("libglib-2.0.so.0")]
682 static extern int g_str_hash (string str);
684 // Stolen from deep within e-d-s's camel-data-cache.c Very evil.
685 private const int CAMEL_DATA_CACHE_MASK = ((1 << 6) - 1);
687 public override Indexable GetNextIndexable ()
689 Indexable indexable = null;
691 // No more new messages to index, so start on the removals.
692 if (this.delete_mode) {
693 string uid = (string) this.deleted_list [0];
694 Uri uri = EvolutionMailQueryable.EmailUri (this.account_name, this.folder_name, uid);
696 indexable = new Indexable (IndexableType.Remove, uri);
698 this.deleted_list.RemoveAt (0);
699 this.mapping.Remove (uid);
701 this.delete_count++;
703 return indexable;
706 Camel.MessageInfo mi = (Camel.MessageInfo) this.summary_enumerator.Current;
708 ++this.count;
710 if (Debug) {
711 Logger.Log.Debug ("Constructed message {0} with uid {1}, flags {2}.",
712 this.count, mi.uid, mi.flags);
715 // Try to load the cached message data off disk
716 object flags = this.mapping[mi.uid];
718 if (flags == null) {
719 // New, previously unseen message
720 string msg_file;
722 if (this.backend_type == ImapBackendType.Imap)
723 msg_file = Path.Combine (summary_info.DirectoryName, mi.uid + ".");
724 else {
725 // This is taken from e-d-s's camel-data-cache.c. No doubt
726 // NotZed would scream bloody murder if he saw this here.
727 int hash = (g_str_hash (mi.uid) >> 5) & CAMEL_DATA_CACHE_MASK;
728 string cache_path = String.Format ("cache/{0:x}/{1}", hash, mi.uid);
729 msg_file = Path.Combine (summary_info.DirectoryName, cache_path);
732 indexable = this.CamelMessageToIndexable (mi, msg_file);
734 if (Debug)
735 Logger.Log.Debug ("Unseen message, indexable {0} null", indexable == null ? "" : "not");
737 this.mapping[mi.uid] = mi.flags;
739 if (indexable != null)
740 ++this.indexed_count;
741 } else if ((uint) flags != mi.flags) {
742 // Previously seen message, but flags have changed.
743 Uri uri = CamelMessageUri (mi);
744 indexable = new Indexable (uri);
745 indexable.Type = IndexableType.PropertyChange;
747 Property flag_prop = Property.NewUnsearched ("fixme:flags", mi.flags);
748 flag_prop.IsMutable = true;
749 indexable.AddProperty (flag_prop);
751 if (Debug)
752 Logger.Log.Debug ("Previously seen message, flags changed: {0} -> {1}", flags, mi.flags);
754 ++this.indexed_count;
755 } else {
756 if (Debug)
757 Logger.Log.Debug ("Previously seen message, unchanged.");
760 if (flags != null)
761 this.deleted_list.Remove (mi.uid);
763 return indexable;
766 private Uri CamelMessageUri (Camel.MessageInfo message_info)
768 return EvolutionMailQueryable.EmailUri (this.account_name, this.folder_name, message_info.uid);
771 private Indexable CamelMessageToIndexable (Camel.MessageInfo messageInfo, string msg_file)
773 // Don't index messages flagged as junk
774 if (messageInfo.IsJunk)
775 return null;
777 // Many properties will be set by the filter when
778 // processing the cached data, if it's there. So
779 // don't set a number of properties in that case.
780 bool have_content = File.Exists (msg_file);
782 Uri uri = CamelMessageUri (messageInfo);
783 Indexable indexable = new Indexable (uri);
785 indexable.Timestamp = messageInfo.SentDate;
786 indexable.MimeType = "message/rfc822";
787 indexable.HitType = "MailMessage";
789 indexable.AddProperty (Property.NewUnsearched ("fixme:account", this.imap_name));
790 indexable.AddProperty (Property.NewUnsearched ("fixme:folder", this.folder_name));
791 indexable.AddProperty (Property.NewUnsearched ("fixme:client", "evolution"));
793 if (!have_content) {
794 indexable.AddProperty (Property.New ("dc:title", GMime.Utils.HeaderDecodePhrase (messageInfo.subject)));
795 indexable.AddProperty (Property.NewDate ("fixme:date", messageInfo.SentDate));
798 GMime.InternetAddressList addrs;
799 addrs = GMime.InternetAddressList.ParseString (messageInfo.to);
800 foreach (GMime.InternetAddress ia in addrs) {
801 if (!have_content) {
802 indexable.AddProperty (Property.NewUnsearched ("fixme:to", ia.ToString (false)));
803 if (ia.AddressType != GMime.InternetAddressType.Group)
804 indexable.AddProperty (Property.New ("fixme:to_address", ia.Addr));
806 indexable.AddProperty (Property.New ("fixme:to_name", ia.Name));
809 if (this.folder_name == "Sent" && ia.AddressType != GMime.InternetAddressType.Group)
810 indexable.AddProperty (Property.NewUnsearched ("fixme:sentTo", ia.Addr));
812 addrs.Dispose ();
814 addrs = GMime.InternetAddressList.ParseString (messageInfo.cc);
815 foreach (GMime.InternetAddress ia in addrs) {
816 if (!have_content) {
817 indexable.AddProperty (Property.NewUnsearched ("fixme:cc", ia.ToString (false)));
818 if (ia.AddressType != GMime.InternetAddressType.Group)
819 indexable.AddProperty (Property.New ("fixme:cc_address", ia.Addr));
821 indexable.AddProperty (Property.New ("fixme:cc_name", ia.Name));
824 if (this.folder_name == "Sent" && ia.AddressType != GMime.InternetAddressType.Group)
825 indexable.AddProperty (Property.NewUnsearched ("fixme:sentTo", ia.Addr));
827 addrs.Dispose ();
829 addrs = GMime.InternetAddressList.ParseString (messageInfo.from);
830 foreach (GMime.InternetAddress ia in addrs) {
831 if (!have_content) {
832 indexable.AddProperty (Property.NewUnsearched ("fixme:from", ia.ToString (false)));
833 if (ia.AddressType != GMime.InternetAddressType.Group)
834 indexable.AddProperty (Property.New ("fixme:from_address", ia.Addr));
836 indexable.AddProperty (Property.New ("fixme:from_name", ia.Name));
839 if (this.folder_name != "Sent" && ia.AddressType != GMime.InternetAddressType.Group)
840 indexable.AddProperty (Property.NewUnsearched ("fixme:gotFrom", ia.Addr));
842 addrs.Dispose ();
844 indexable.AddProperty (Property.NewKeyword ("fixme:mlist", messageInfo.mlist));
846 Property flag_prop = Property.NewUnsearched ("fixme:flags", messageInfo.flags);
847 flag_prop.IsMutable = true;
848 indexable.AddProperty (flag_prop);
850 if (this.folder_name == "Sent")
851 indexable.AddProperty (Property.NewFlag ("fixme:isSent"));
853 if (messageInfo.IsAnswered)
854 indexable.AddProperty (Property.NewFlag ("fixme:isAnswered"));
856 if (messageInfo.IsDeleted)
857 indexable.AddProperty (Property.NewFlag ("fixme:isDeleted"));
859 if (messageInfo.IsDraft)
860 indexable.AddProperty (Property.NewFlag ("fixme:isDraft"));
862 if (messageInfo.IsFlagged)
863 indexable.AddProperty (Property.NewFlag ("fixme:isFlagged"));
865 if (messageInfo.IsSeen)
866 indexable.AddProperty (Property.NewFlag ("fixme:isSeen"));
868 if (messageInfo.HasAttachments && !have_content)
869 indexable.AddProperty (Property.NewFlag ("fixme:hasAttachments"));
871 if (messageInfo.IsAnsweredAll)
872 indexable.AddProperty (Property.NewFlag ("fixme:isAnsweredAll"));
874 if (have_content)
875 indexable.ContentUri = UriFu.PathToFileUri (msg_file);
876 else
877 indexable.NoContent = true;
879 return indexable;
882 public override void Checkpoint ()
884 if (this.summary != null) {
886 string progress = "";
887 if (this.count > 0 && this.summary.header.count > 0) {
888 progress = String.Format (" ({0}/{1} {2:###.0}%)",
889 this.count,
890 this.summary.header.count,
891 100.0 * this.count / this.summary.header.count);
893 this.queryable.SetGeneratorProgress (this, (int) (100.0 * this.count / this.summary.header.count));
897 Logger.Log.Debug ("{0}: indexed {1} messages{2}",
898 this.folder_name, this.indexed_count, progress);
901 this.SaveCache ();
904 public override string GetTarget ()
906 return "summary-file:" + summary_info.FullName;
909 protected override FileInfo CrawlFile {
910 get { return this.summary_info; }