3 // EvolutionMailIndexableGenerator.cs
5 // Copyright (C) 2004 Novell, Inc.
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.
29 using System
.Collections
;
31 using System
.Runtime
.Serialization
.Formatters
.Binary
;
32 using System
.Threading
;
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 ()
79 protected static void InitializeGMime ()
81 if (!gmime_initialized
) {
83 gmime_initialized
= true;
87 protected bool IsSpamFolder (string name
)
89 if (name
.ToLower () == "spam" || name
.ToLower () == "junk")
95 protected bool IgnoreFolder (string path
)
97 // FIXME: Use System.IO.Path
98 foreach (ExcludeItem exclude
in excludes
) {
99 if (exclude
.IsMatch (path
))
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)
124 if (Object
.ReferenceEquals (this, generator
))
127 if (this.CrawlFile
.FullName
== generator
.CrawlFile
.FullName
)
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
;
155 string folder_name
= "";
157 di
= file_info
.Directory
;
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
);
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
))
179 if (this.IgnoreFolder (this.mbox_info
.FullName
))
185 private long MboxLastOffset
{
187 string offset_str
= this.queryable
.ReadDataLine ("offset-" + this.folder_name
.Replace ('/', '-'));
188 long offset
= Convert
.ToInt64 (offset_str
);
194 this.queryable
.WriteDataLine ("offset-" + this.folder_name
.Replace ('/', '-'), value.ToString ());
198 public override bool HasNextIndexable ()
200 if (this.account_name
== null) {
202 this.queryable
.RemoveGeneratorProgress (this);
207 if (this.mbox_fd
< 0) {
208 Logger
.Log
.Debug ("Opening mbox {0}", this.mbox_info
.Name
);
212 } catch (Exception e
) {
213 Logger
.Log
.Warn ("Caught exception trying to initalize gmime:");
218 this.mbox_fd
= Mono
.Unix
.Native
.Syscall
.open (this.mbox_info
.FullName
, Mono
.Unix
.Native
.OpenFlags
.O_RDONLY
);
219 this.mbox_stream
= new GMime
.StreamFs (this.mbox_fd
);
220 this.mbox_stream
.Seek ((int) this.MboxLastOffset
);
221 this.mbox_parser
= new GMime
.Parser (this.mbox_stream
);
222 this.mbox_parser
.ScanFrom
= true;
224 FileInfo info
= new FileInfo (this.mbox_info
.FullName
);
225 this.file_size
= info
.Length
;
228 if (this.mbox_parser
.Eos ()) {
229 long offset
= this.mbox_parser
.FromOffset
;
231 this.mbox_stream
.Close ();
234 this.mbox_stream
.Dispose ();
235 this.mbox_stream
= null;
236 this.mbox_parser
.Dispose ();
237 this.mbox_parser
= null;
239 Logger
.Log
.Debug ("{0}: Finished indexing {1} messages", this.folder_name
, this.indexed_count
);
242 this.MboxLastOffset
= offset
;
243 this.CrawlFinished ();
250 public override Indexable
GetNextIndexable ()
252 using (GMime
.Message message
= this.mbox_parser
.ConstructMessage ()) {
253 // Work around what I think is a bug in GMime: If you
254 // have a zero-byte file or seek to the end of a
255 // file, parser.Eos () will return true until it
256 // actually tries to read something off the wire.
257 // Since parser.ConstructMessage() always returns a
258 // message (which may also be a bug), we'll often get
259 // one empty message which we need to deal with here.
261 // Check if its empty by seeing if the Headers
262 // property is null or empty.
263 if (message
== null || message
.Headers
== null || message
.Headers
== "")
268 string x_evolution
= message
.GetHeader ("X-Evolution");
269 if (x_evolution
== null || x_evolution
== "") {
270 Logger
.Log
.Info ("{0}: Message at offset {1} has no X-Evolution header!",
271 this.folder_name
, this.mbox_parser
.FromOffset
);
275 // This extracts the UID and flags from the X-Evolution header.
276 // It may also contain user-defined flags and tags, but we don't
277 // support those right now.
278 int separator_idx
= x_evolution
.IndexOf ('-');
280 string uid_str
= x_evolution
.Substring (0, separator_idx
);
281 string uid
= Convert
.ToUInt32 (uid_str
, 16).ToString (); // ugh.
282 uint flags
= Convert
.ToUInt32 (x_evolution
.Substring (separator_idx
+ 1, 4), 16);
284 Indexable indexable
= this.GMimeMessageToIndexable (uid
, message
, flags
);
287 Logger
.Log
.Debug ("Constructed message {0} with uid {1}, flags {2}. Indexable {3} null",
288 this.count
, uid
, flags
, indexable
== null ? "" : "not");
291 if (indexable
== null)
294 ++this.indexed_count
;
300 private static bool CheckFlags (uint flags
, Camel
.CamelFlags test
)
302 return (flags
& (uint) test
) == (uint) test
;
305 private Indexable
GMimeMessageToIndexable (string uid
, GMime
.Message message
, uint flags
)
307 // Don't index messages flagged as junk
308 if (CheckFlags (flags
, Camel
.CamelFlags
.Junk
))
311 System
.Uri uri
= EvolutionMailQueryable
.EmailUri (this.account_name
, this.folder_name
, uid
);
312 Indexable indexable
= new Indexable (uri
);
314 indexable
.Timestamp
= message
.Date
.ToUniversalTime ();
315 indexable
.HitType
= "MailMessage";
316 indexable
.MimeType
= "message/rfc822";
317 indexable
.CacheContent
= false;
319 indexable
.AddProperty (Property
.NewUnsearched ("fixme:client", "evolution"));
320 indexable
.AddProperty (Property
.NewUnsearched ("fixme:account", "Local"));
321 indexable
.AddProperty (Property
.NewUnsearched ("fixme:folder", this.folder_name
));
323 GMime
.InternetAddressList addrs
;
325 addrs
= message
.GetRecipients (GMime
.Message
.RecipientType
.To
);
326 foreach (GMime
.InternetAddress ia
in addrs
) {
327 if (this.folder_name
== "Sent" && ia
.AddressType
!= GMime
.InternetAddressType
.Group
)
328 indexable
.AddProperty (Property
.NewUnsearched ("fixme:sentTo", ia
.Addr
));
332 addrs
= message
.GetRecipients (GMime
.Message
.RecipientType
.Cc
);
333 foreach (GMime
.InternetAddress ia
in addrs
) {
334 if (this.folder_name
== "Sent" && ia
.AddressType
!= GMime
.InternetAddressType
.Group
)
335 indexable
.AddProperty (Property
.NewUnsearched ("fixme:sentTo", ia
.Addr
));
339 addrs
= GMime
.InternetAddressList
.ParseString (GMime
.Utils
.HeaderDecodePhrase (message
.Sender
));
340 foreach (GMime
.InternetAddress ia
in addrs
) {
341 if (this.folder_name
!= "Sent" && ia
.AddressType
!= GMime
.InternetAddressType
.Group
)
342 indexable
.AddProperty (Property
.NewUnsearched ("fixme:gotFrom", ia
.Addr
));
346 if (this.folder_name
== "Sent")
347 indexable
.AddProperty (Property
.NewFlag ("fixme:isSent"));
349 Property flag_prop
= Property
.NewUnsearched ("fixme:flags", flags
);
350 flag_prop
.IsMutable
= true;
351 indexable
.AddProperty (flag_prop
);
353 if (CheckFlags (flags
, Camel
.CamelFlags
.Answered
))
354 indexable
.AddProperty (Property
.NewFlag ("fixme:isAnswered"));
356 if (CheckFlags (flags
, Camel
.CamelFlags
.Deleted
))
357 indexable
.AddProperty (Property
.NewFlag ("fixme:isDeleted"));
359 if (CheckFlags (flags
, Camel
.CamelFlags
.Draft
))
360 indexable
.AddProperty (Property
.NewFlag ("fixme:isDraft"));
362 if (CheckFlags (flags
, Camel
.CamelFlags
.Flagged
))
363 indexable
.AddProperty (Property
.NewFlag ("fixme:isFlagged"));
365 if (CheckFlags (flags
, Camel
.CamelFlags
.Seen
))
366 indexable
.AddProperty (Property
.NewFlag ("fixme:isSeen"));
368 if (CheckFlags (flags
, Camel
.CamelFlags
.AnsweredAll
))
369 indexable
.AddProperty (Property
.NewFlag ("fixme:isAnsweredAll"));
371 indexable
.SetBinaryStream (message
.Stream
);
376 public override void Checkpoint ()
380 if (this.mbox_parser
!= null) {
381 offset
= this.mbox_parser
.FromOffset
;
384 this.MboxLastOffset
= offset
;
387 string progress
= "";
388 if (this.file_size
> 0 && offset
> 0) {
389 progress
= String
.Format (" ({0}/{1} bytes {2:###.0}%)",
390 offset
, this.file_size
, 100.0 * offset
/ this.file_size
);
392 this.queryable
.SetGeneratorProgress (this, (int) (100.0 * offset
/ this.file_size
));
395 Logger
.Log
.Debug ("{0}: indexed {1} messages{2}",
396 this.folder_name
, this.indexed_count
, progress
);
399 public override string GetTarget ()
401 return "mbox-file:" + mbox_info
.FullName
;
404 protected override FileInfo CrawlFile
{
405 get { return this.mbox_info; }
409 public class EvolutionMailIndexableGeneratorImap
: EvolutionMailIndexableGenerator
{
410 private enum ImapBackendType
{
415 private FileInfo summary_info
;
416 private string imap_name
;
417 private ImapBackendType backend_type
;
418 private Camel
.Summary summary
;
419 private IEnumerator summary_enumerator
;
420 private ICollection accounts
;
421 private string folder_cache_name
;
422 private Hashtable mapping
;
423 private ArrayList deleted_list
;
424 private bool delete_mode
;
425 private int delete_count
;
427 public EvolutionMailIndexableGeneratorImap (EvolutionMailQueryable queryable
, FileInfo summary_info
) : base (queryable
)
429 this.summary_info
= summary_info
;
432 protected override string GetFolderName (FileSystemInfo info
)
434 DirectoryInfo dir_info
= (DirectoryInfo
) info
;
435 string folder_name
= "";
437 while (dir_info
!= null) {
438 folder_name
= Path
.Combine (dir_info
.Name
, folder_name
);
440 dir_info
= dir_info
.Parent
;
442 if (dir_info
.Name
!= "subfolders")
445 dir_info
= dir_info
.Parent
;
451 protected override bool Setup ()
453 string dir_name
= summary_info
.DirectoryName
;
456 int idx
= dir_name
.IndexOf (".evolution/mail/imap4/");
459 this.backend_type
= ImapBackendType
.Imap4
;
460 imap_start_idx
= idx
+ 22;
462 this.backend_type
= ImapBackendType
.Imap
;
463 imap_start_idx
= dir_name
.IndexOf (".evolution/mail/imap/") + 21;
466 string imap_start
= dir_name
.Substring (imap_start_idx
);
467 this.imap_name
= imap_start
.Substring (0, imap_start
.IndexOf ('/'));
470 this.accounts
= (ICollection
) GConfThreadHelper
.Get ("/apps/evolution/mail/accounts");
471 } catch (Exception ex
) {
472 Logger
.Log
.Warn ("Caught exception in Setup(): " + ex
.Message
);
473 Logger
.Log
.Warn ("There are no configured evolution accounts, ignoring {0}", this.imap_name
);
477 // This should only happen if we shut down while waiting for the GConf results to come back.
478 if (this.accounts
== null)
481 foreach (string xml
in this.accounts
) {
482 XmlDocument xmlDoc
= new XmlDocument ();
484 xmlDoc
.LoadXml (xml
);
486 XmlNode account
= xmlDoc
.SelectSingleNode ("//account");
493 foreach (XmlAttribute attr
in account
.Attributes
) {
494 if (attr
.Name
== "uid") {
495 uid
= attr
.InnerText
;
503 XmlNode imap_url_node
= xmlDoc
.SelectSingleNode ("//source/url");
505 if (imap_url_node
== null)
508 string imap_url
= imap_url_node
.InnerText
;
509 // If there is a semicolon in the username part of the URL, it
510 // indicates that there's an auth scheme there. We don't care
511 // about that, so remove it.
512 int user_end
= imap_url
.IndexOf ('@');
513 int semicolon
= imap_url
.IndexOf (';', 0, user_end
+ 1);
516 imap_url
= imap_url
.Substring (0, semicolon
) + imap_url
.Substring (user_end
);
518 // Escape backslashes, which frequently appear when using IMAP against Exchange servers
519 this.imap_name
= this.imap_name
.Replace ("\\", "%5c");
521 // Escape out additional @s in the name. I hate the class libs so much.
522 int lastIdx
= this.imap_name
.LastIndexOf ('@');
523 if (this.imap_name
.IndexOf ('@') != lastIdx
) {
524 string toEscape
= this.imap_name
.Substring (0, lastIdx
);
525 this.imap_name
= toEscape
.Replace ("@", "%40") + this.imap_name
.Substring (lastIdx
);
528 string backend_url_prefix
;
529 if (this.backend_type
== ImapBackendType
.Imap
)
530 backend_url_prefix
= "imap";
532 backend_url_prefix
= "imap4";
534 if (imap_url
.StartsWith (backend_url_prefix
+ "://" + this.imap_name
+ "/")) {
535 this.account_name
= uid
;
540 if (this.account_name
== null) {
541 Logger
.Log
.Info ("Unable to determine account name for {0}", this.imap_name
);
545 // Need to check the directory on disk to see if it's a junk/spam folder,
546 // since the folder name will be "foo/spam" and not match the check below.
547 DirectoryInfo dir_info
= new DirectoryInfo (dir_name
);
548 if (this.IsSpamFolder (dir_info
.Name
))
551 // Check if the folder is listed in the configuration as to be excluded from indexing
552 if (this.IgnoreFolder (dir_info
.FullName
))
555 this.folder_name
= GetFolderName (new DirectoryInfo (dir_name
));
560 private string FolderCacheName
{
562 if (this.account_name
== null || this.folder_name
== null)
565 if (this.folder_cache_name
== null)
566 this.folder_cache_name
= "status-" + this.account_name
+ "-" + this.folder_name
.Replace ('/', '-');
568 return this.folder_cache_name
;
572 private bool LoadCache ()
575 BinaryFormatter formatter
;
577 if (this.FolderCacheName
== null) {
578 this.mapping
= new Hashtable ();
583 cacheStream
= this.queryable
.ReadDataStream (this.FolderCacheName
);
584 formatter
= new BinaryFormatter ();
585 this.mapping
= formatter
.Deserialize (cacheStream
) as Hashtable
;
586 cacheStream
.Close ();
587 Logger
.Log
.Debug ("Successfully loaded previous crawled data from disk: {0}", this.FolderCacheName
);
591 this.mapping
= new Hashtable ();
597 private void SaveCache ()
600 BinaryFormatter formatter
;
602 if (this.FolderCacheName
== null)
605 cacheStream
= this.queryable
.WriteDataStream (this.FolderCacheName
);
606 formatter
= new BinaryFormatter ();
607 formatter
.Serialize (cacheStream
, mapping
);
608 cacheStream
.Close ();
611 public override bool HasNextIndexable ()
613 if (this.account_name
== null) {
615 this.queryable
.RemoveGeneratorProgress (this);
620 if (this.mapping
== null) {
621 bool cache_loaded
= this.LoadCache ();
623 this.deleted_list
= new ArrayList (this.mapping
.Keys
);
624 this.deleted_list
.Sort ();
625 Logger
.Log
.Debug ("Deleted list starting at {0} for {1}", this.deleted_list
.Count
, this.folder_name
);
627 // Check to see if we even need to bother walking the summary
628 if (cache_loaded
&& this.queryable
.FileAttributesStore
.IsUpToDate (this.CrawlFile
.FullName
)) {
629 Logger
.Log
.Debug ("{0}: summary has not been updated; crawl unncessary", this.folder_name
);
630 this.queryable
.RemoveGeneratorProgress (this);
635 if (this.summary
== null) {
637 if (this.backend_type
== ImapBackendType
.Imap
)
638 this.summary
= Camel
.Summary
.LoadImapSummary (this.summary_info
.FullName
);
640 this.summary
= Camel
.Summary
.LoadImap4Summary (this.summary_info
.FullName
);
641 } catch (Exception e
) {
642 Logger
.Log
.Warn ("Unable to index {0}: {1}", this.folder_name
,
644 this.queryable
.RemoveGeneratorProgress (this);
649 if (this.summary_enumerator
== null)
650 this.summary_enumerator
= this.summary
.GetEnumerator ();
652 if (this.summary_enumerator
.MoveNext ())
655 this.delete_mode
= true;
657 if (this.deleted_list
.Count
> 0)
660 string progress
= "";
661 if (this.count
> 0 && this.summary
.header
.count
> 0) {
662 progress
= String
.Format ("({0}/{1} {2:###.0}%)",
664 this.summary
.header
.count
,
665 100.0 * this.count
/ this.summary
.header
.count
);
668 Logger
.Log
.Debug ("{0}: Finished indexing {1} messages {2}, {3} messages deleted", this.folder_name
, this.indexed_count
, progress
, this.delete_count
);
671 this.CrawlFinished ();
676 // Kind of nasty, but we need the function.
677 [System
.Runtime
.InteropServices
.DllImport("libglib-2.0.so.0")]
678 static extern int g_str_hash (string str
);
680 // Stolen from deep within e-d-s's camel-data-cache.c Very evil.
681 private const int CAMEL_DATA_CACHE_MASK
= ((1 << 6) - 1);
683 public override Indexable
GetNextIndexable ()
685 Indexable indexable
= null;
687 // No more new messages to index, so start on the removals.
688 if (this.delete_mode
) {
689 string uid
= (string) this.deleted_list
[0];
690 Uri uri
= EvolutionMailQueryable
.EmailUri (this.account_name
, this.folder_name
, uid
);
692 indexable
= new Indexable (IndexableType
.Remove
, uri
);
694 this.deleted_list
.RemoveAt (0);
695 this.mapping
.Remove (uid
);
702 Camel
.MessageInfo mi
= (Camel
.MessageInfo
) this.summary_enumerator
.Current
;
707 Logger
.Log
.Debug ("Constructed message {0} with uid {1}, flags {2}.",
708 this.count
, mi
.uid
, mi
.flags
);
711 // Try to load the cached message data off disk
712 object flags
= this.mapping
[mi
.uid
];
715 // New, previously unseen message
718 if (this.backend_type
== ImapBackendType
.Imap
)
719 msg_file
= Path
.Combine (summary_info
.DirectoryName
, mi
.uid
+ ".");
721 // This is taken from e-d-s's camel-data-cache.c. No doubt
722 // NotZed would scream bloody murder if he saw this here.
723 int hash
= (g_str_hash (mi
.uid
) >> 5) & CAMEL_DATA_CACHE_MASK
;
724 string cache_path
= String
.Format ("cache/{0:x}/{1}", hash
, mi
.uid
);
725 msg_file
= Path
.Combine (summary_info
.DirectoryName
, cache_path
);
728 indexable
= this.CamelMessageToIndexable (mi
, msg_file
);
731 Logger
.Log
.Debug ("Unseen message, indexable {0} null", indexable
== null ? "" : "not");
733 this.mapping
[mi
.uid
] = mi
.flags
;
735 if (indexable
!= null)
736 ++this.indexed_count
;
737 } else if ((uint) flags
!= mi
.flags
) {
738 // Previously seen message, but flags have changed.
739 Uri uri
= CamelMessageUri (mi
);
740 indexable
= new Indexable (uri
);
741 indexable
.Type
= IndexableType
.PropertyChange
;
743 Property flag_prop
= Property
.NewUnsearched ("fixme:flags", mi
.flags
);
744 flag_prop
.IsMutable
= true;
745 indexable
.AddProperty (flag_prop
);
748 Logger
.Log
.Debug ("Previously seen message, flags changed: {0} -> {1}", flags
, mi
.flags
);
750 ++this.indexed_count
;
753 Logger
.Log
.Debug ("Previously seen message, unchanged.");
757 this.deleted_list
.Remove (mi
.uid
);
762 private Uri
CamelMessageUri (Camel
.MessageInfo message_info
)
764 return EvolutionMailQueryable
.EmailUri (this.account_name
, this.folder_name
, message_info
.uid
);
767 private Indexable
CamelMessageToIndexable (Camel
.MessageInfo messageInfo
, string msg_file
)
769 // Don't index messages flagged as junk
770 if (messageInfo
.IsJunk
)
773 // Many properties will be set by the filter when
774 // processing the cached data, if it's there. So
775 // don't set a number of properties in that case.
776 bool have_content
= File
.Exists (msg_file
);
778 Uri uri
= CamelMessageUri (messageInfo
);
779 Indexable indexable
= new Indexable (uri
);
781 indexable
.Timestamp
= messageInfo
.SentDate
;
782 indexable
.MimeType
= "message/rfc822";
783 indexable
.HitType
= "MailMessage";
785 indexable
.AddProperty (Property
.NewUnsearched ("fixme:account", this.imap_name
));
786 indexable
.AddProperty (Property
.NewUnsearched ("fixme:folder", this.folder_name
));
787 indexable
.AddProperty (Property
.NewUnsearched ("fixme:client", "evolution"));
790 indexable
.AddProperty (Property
.New ("dc:title", GMime
.Utils
.HeaderDecodePhrase (messageInfo
.subject
)));
791 indexable
.AddProperty (Property
.NewDate ("fixme:date", messageInfo
.SentDate
));
794 GMime
.InternetAddressList addrs
;
795 addrs
= GMime
.InternetAddressList
.ParseString (messageInfo
.to
);
796 foreach (GMime
.InternetAddress ia
in addrs
) {
798 indexable
.AddProperty (Property
.NewUnsearched ("fixme:to", ia
.ToString (false)));
799 if (ia
.AddressType
!= GMime
.InternetAddressType
.Group
) {
800 indexable
.AddProperty (Property
.New ("fixme:to_address", ia
.Addr
));
801 indexable
.AddProperty (Property
.New ("fixme:to_sanitized", StringFu
.SanitizeEmail (ia
.Addr
)));
803 indexable
.AddProperty (Property
.New ("fixme:to_name", ia
.Name
));
806 if (this.folder_name
== "Sent" && ia
.AddressType
!= GMime
.InternetAddressType
.Group
)
807 indexable
.AddProperty (Property
.NewUnsearched ("fixme:sentTo", ia
.Addr
));
811 addrs
= GMime
.InternetAddressList
.ParseString (messageInfo
.cc
);
812 foreach (GMime
.InternetAddress ia
in addrs
) {
814 indexable
.AddProperty (Property
.NewUnsearched ("fixme:cc", ia
.ToString (false)));
815 if (ia
.AddressType
!= GMime
.InternetAddressType
.Group
) {
816 indexable
.AddProperty (Property
.New ("fixme:cc_address", ia
.Addr
));
817 indexable
.AddProperty (Property
.New ("fixme:cc_sanitized", StringFu
.SanitizeEmail (ia
.Addr
)));
819 indexable
.AddProperty (Property
.New ("fixme:cc_name", ia
.Name
));
822 if (this.folder_name
== "Sent" && ia
.AddressType
!= GMime
.InternetAddressType
.Group
)
823 indexable
.AddProperty (Property
.NewUnsearched ("fixme:sentTo", ia
.Addr
));
827 addrs
= GMime
.InternetAddressList
.ParseString (messageInfo
.from);
828 foreach (GMime
.InternetAddress ia
in addrs
) {
830 indexable
.AddProperty (Property
.NewUnsearched ("fixme:from", ia
.ToString (false)));
831 if (ia
.AddressType
!= GMime
.InternetAddressType
.Group
) {
832 indexable
.AddProperty (Property
.New ("fixme:from_address", ia
.Addr
));
833 indexable
.AddProperty (Property
.New ("fixme:from_sanitized", StringFu
.SanitizeEmail (ia
.Addr
)));
835 indexable
.AddProperty (Property
.New ("fixme:from_name", ia
.Name
));
838 if (this.folder_name
!= "Sent" && ia
.AddressType
!= GMime
.InternetAddressType
.Group
)
839 indexable
.AddProperty (Property
.NewUnsearched ("fixme:gotFrom", ia
.Addr
));
843 indexable
.AddProperty (Property
.NewKeyword ("fixme:mlist", messageInfo
.mlist
));
845 Property flag_prop
= Property
.NewUnsearched ("fixme:flags", messageInfo
.flags
);
846 flag_prop
.IsMutable
= true;
847 indexable
.AddProperty (flag_prop
);
849 if (this.folder_name
== "Sent")
850 indexable
.AddProperty (Property
.NewFlag ("fixme:isSent"));
852 if (messageInfo
.IsAnswered
)
853 indexable
.AddProperty (Property
.NewFlag ("fixme:isAnswered"));
855 if (messageInfo
.IsDeleted
)
856 indexable
.AddProperty (Property
.NewFlag ("fixme:isDeleted"));
858 if (messageInfo
.IsDraft
)
859 indexable
.AddProperty (Property
.NewFlag ("fixme:isDraft"));
861 if (messageInfo
.IsFlagged
)
862 indexable
.AddProperty (Property
.NewFlag ("fixme:isFlagged"));
864 if (messageInfo
.IsSeen
)
865 indexable
.AddProperty (Property
.NewFlag ("fixme:isSeen"));
867 if (messageInfo
.HasAttachments
&& !have_content
)
868 indexable
.AddProperty (Property
.NewFlag ("fixme:hasAttachments"));
870 if (messageInfo
.IsAnsweredAll
)
871 indexable
.AddProperty (Property
.NewFlag ("fixme:isAnsweredAll"));
874 indexable
.ContentUri
= UriFu
.PathToFileUri (msg_file
);
876 indexable
.NoContent
= true;
881 public override void Checkpoint ()
883 if (this.summary
!= null) {
885 string progress
= "";
886 if (this.count
> 0 && this.summary
.header
.count
> 0) {
887 progress
= String
.Format (" ({0}/{1} {2:###.0}%)",
889 this.summary
.header
.count
,
890 100.0 * this.count
/ this.summary
.header
.count
);
892 this.queryable
.SetGeneratorProgress (this, (int) (100.0 * this.count
/ this.summary
.header
.count
));
896 Logger
.Log
.Debug ("{0}: indexed {1} messages{2}",
897 this.folder_name
, this.indexed_count
, progress
);
903 public override string GetTarget ()
905 return "summary-file:" + summary_info
.FullName
;
908 protected override FileInfo CrawlFile
{
909 get { return this.summary_info; }