2 // EvolutionMailDriver.cs
4 // Copyright (C) 2004 Novell, Inc.
8 // Permission is hereby granted, free of charge, to any person obtaining a
9 // copy of this software and associated documentation files (the "Software"),
10 // to deal in the Software without restriction, including without limitation
11 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 // and/or sell copies of the Software, and to permit persons to whom the
13 // Software is furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in
16 // all copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 // DEALINGS IN THE SOFTWARE.
29 using System
.Collections
;
31 using System
.Threading
;
34 using Camel
= Beagle
.Util
.Camel
;
36 using LNI
= Lucene
.Net
.Index
;
38 namespace Beagle
.Daemon
.EvolutionMailDriver
{
40 [QueryableFlavor (Name
="Mail", Domain
=QueryDomain
.Local
, RequireInotify
=false)]
41 public class EvolutionMailQueryable
: LuceneQueryable
{
43 public int polling_interval_in_seconds
= 60;
45 public static Logger log
= Logger
.Get ("mail");
46 private string local_path
, imap_path
, imap4_path
;
48 private MailCrawler crawler
;
51 // 1: Original version, stored all recipient addresses as a
53 // 2: Stores recipients in separate properties,
54 // filters/indexes all attachments
55 private const int INDEX_VERSION
= 2;
57 public EvolutionMailQueryable () : base ("MailIndex", INDEX_VERSION
)
59 this.local_path
= Path
.Combine (PathFinder
.HomeDir
, ".evolution/mail/local");
60 this.imap_path
= Path
.Combine (PathFinder
.HomeDir
, ".evolution/mail/imap");
61 this.imap4_path
= Path
.Combine (PathFinder
.HomeDir
, ".evolution/mail/imap4");
67 foreach (FileInfo file
in crawler
.Summaries
)
69 foreach (FileInfo file
in crawler
.Mboxes
)
73 private void CrawlHook (Scheduler
.Task task
)
76 task
.Reschedule
= true;
77 task
.TriggerTime
= DateTime
.Now
.AddSeconds (polling_interval_in_seconds
);
80 //////////////////////////////////////////////////////////////////////////////////////////////
82 // Evil hack: manipulate the lucene index directly to extract
83 // statistics about who you sent mail to.
84 static private void AnalyzeYourRecipients (LuceneQueryingDriver driver
)
86 Logger
.Log
.Debug ("FIXME: AnalyzeYourRecipients commented out!");
88 LNI
.IndexReader reader
;
89 reader
= LNI
.IndexReader
.Open (driver
.Store
);
91 LNI
.TermEnum term_enum
;
92 term_enum
= reader
.Terms ();
94 const string prop_field
= "prop:k:fixme:sentTo";
97 skip_to
= new LNI
.Term (prop_field
, ""); // ACK!
99 term_enum
.SkipTo (skip_to
);
102 while (term_enum
.Next ()) {
103 LNI
.Term t
= term_enum
.Term ();
104 if (t
.Field () != prop_field
) // Double-ACK!
107 AddAsYourRecipient (t
.Text (), term_enum
.DocFreq ());
116 static private Hashtable your_recipient_weights
= new Hashtable ();
117 static private int total_recipient_weight
= 0;
119 static private void AddAsYourRecipient (string address
, int weight
)
121 object obj
= your_recipient_weights
[address
];
122 int n
= obj
!= null ? (int) obj
: 0;
123 your_recipient_weights
[address
] = n
+ weight
;
126 // FIXME: We update the weights when we add indexables, but not
127 // when we remove records... the daemon has to be restarted
128 // to pick up those changes.
129 static public void AddAsYourRecipient (Indexable indexable
)
131 if (indexable
== null)
133 foreach (Property prop
in indexable
.Properties
)
134 if (prop
.Key
== "fixme:sentTo")
135 AddAsYourRecipient (prop
.Value
, 1);
138 static private double AnalyzeYourRecipientsMultiplier (Hit hit
)
140 // The first magic constant: exempt mail that is less than
142 if (hit
.DaysSinceTimestamp
< 30)
146 foreach (Property prop
in hit
.Properties
) {
148 // This optimization only counts for mails you
149 // received: if you sent this mail, bail out immediately.
150 if (prop
.Key
== "fixme:isSent")
153 if (prop
.Key
== "fixme:gotFrom") {
154 object obj
= your_recipient_weights
[prop
.Value
];
161 const double min_multiplier
= 0.6;
162 const int weight_threshold
= 10;
165 return min_multiplier
;
167 if (weight
>= weight_threshold
)
170 // t == 0.0 if weight == 0
171 // t == 1.0 if weight == weight_threshold
172 double t
= weight
/ (double) weight_threshold
;
176 multiplier
= min_multiplier
+ (1 - min_multiplier
) * t
;
182 //////////////////////////////////////////////////////////////////////////////////////////////
184 private void StartWorker ()
186 Logger
.Log
.Info ("Starting Evolution mail backend");
188 Stopwatch stopwatch
= new Stopwatch ();
191 // Check that we have data to index
192 if ((! Directory
.Exists (this.local_path
)) && (! Directory
.Exists (this.imap_path
))) {
193 // No mails present, repoll every minute
194 log
.Warn ("Evolution mail store not found, watching for it.");
195 GLib
.Timeout
.Add (60000, new GLib
.TimeoutHandler (CheckForMailData
));
199 // This might be dangerous.
200 AnalyzeYourRecipients (Driver
);
202 // Get notification when an index or summary file changes
203 if (Inotify
.Enabled
) {
204 Watch (this.local_path
);
205 Watch (this.imap_path
);
206 Watch (this.imap4_path
);
209 Logger
.Log
.Debug ("Starting mail crawl");
210 crawler
= new MailCrawler (this.local_path
, this.imap_path
, this.imap4_path
);
212 Logger
.Log
.Debug ("Mail crawl finished");
214 // If we don't have inotify, we have to poll the file system. Ugh.
215 if (! Inotify
.Enabled
) {
216 Scheduler
.Task task
= Scheduler
.TaskFromHook (new Scheduler
.TaskHook (CrawlHook
));
217 task
.Tag
= "Crawling ~/.evolution to find summary changes";
218 ThisScheduler
.Add (task
);
222 Logger
.Log
.Info ("Evolution mail driver worker thread done in {0}",
226 public override void Start ()
228 Logger
.Log
.Info ("Starting Evolution mail backend");
231 ExceptionHandlingThread
.Start (new ThreadStart (StartWorker
));
234 private void Watch (string path
)
236 DirectoryInfo root
= new DirectoryInfo (path
);
240 Queue queue
= new Queue ();
241 queue
.Enqueue (root
);
243 while (queue
.Count
> 0) {
244 DirectoryInfo dir
= queue
.Dequeue () as DirectoryInfo
;
249 Inotify
.Subscribe (dir
.FullName
, OnInotifyEvent
,
250 Inotify
.EventType
.Create
251 | Inotify
.EventType
.Delete
252 | Inotify
.EventType
.MovedTo
);
254 foreach (DirectoryInfo subdir
in dir
.GetDirectories ())
255 queue
.Enqueue (subdir
);
259 private void OnInotifyEvent (Inotify
.Watch watch
,
263 Inotify
.EventType type
)
268 string fullPath
= Path
.Combine (path
, subitem
);
270 if ((type
& Inotify
.EventType
.Create
) != 0 && (type
& Inotify
.EventType
.IsDirectory
) != 0) {
275 if ((type
& Inotify
.EventType
.Delete
) != 0 && (type
& Inotify
.EventType
.IsDirectory
) != 0) {
276 watch
.Unsubscribe ();
280 if ((type
& Inotify
.EventType
.MovedTo
) != 0) {
281 if (subitem
== "summary") {
283 log
.Info ("Reindexing updated IMAP summary: {0}", fullPath
);
284 this.IndexSummary (new FileInfo (fullPath
));
285 } else if (Path
.GetExtension (fullPath
) == ".ev-summary") {
287 string mbox_file
= Path
.ChangeExtension (fullPath
, null);
288 log
.Info ("Reindexing updated mbox: {0}", mbox_file
);
289 this.IndexMbox (new FileInfo (mbox_file
));
294 private bool CheckForMailData ()
296 if ((! Directory
.Exists (this.local_path
)) && (! Directory
.Exists (this.imap_path
)))
297 return true; // continue polling
299 // Otherwise stop polling and start indexing
305 get { return "EvolutionMail"; }
308 public void IndexSummary (FileInfo summaryInfo
)
310 // If there's already a task running for this folder,
311 // don't interrupt it.
312 if (ThisScheduler
.ContainsByTag (summaryInfo
.FullName
)) {
313 Logger
.Log
.Debug ("Not adding task for already running task: {0}", summaryInfo
.FullName
);
317 EvolutionMailIndexableGeneratorImap generator
= new EvolutionMailIndexableGeneratorImap (this, summaryInfo
);
319 task
= NewAddTask (generator
, new Scheduler
.Hook (generator
.Checkpoint
));
320 task
.Tag
= summaryInfo
.FullName
;
321 // IndexableGenerator tasks default to having priority Scheduler.Priority Generator
322 ThisScheduler
.Add (task
);
325 public void IndexMbox (FileInfo mboxInfo
)
327 // If there's already a task running for this mbox,
328 // don't interrupt it.
329 if (ThisScheduler
.ContainsByTag (mboxInfo
.FullName
)) {
330 Logger
.Log
.Debug ("Not adding task for already running task: {0}", mboxInfo
.FullName
);
334 EvolutionMailIndexableGeneratorMbox generator
= new EvolutionMailIndexableGeneratorMbox (this, mboxInfo
);
336 task
= NewAddTask (generator
, new Scheduler
.Hook (generator
.Checkpoint
));
337 task
.Tag
= mboxInfo
.FullName
;
338 // IndexableGenerator tasks default to having priority Scheduler.Priority Generator
339 ThisScheduler
.Add (task
);
342 public static Uri
EmailUri (string accountName
, string folderName
, string uid
)
344 return new Uri (String
.Format ("email://{0}/{1};uid={2}",
345 accountName
, folderName
, uid
));
348 override protected double RelevancyMultiplier (Hit hit
)
352 // FIXME: We should probably be more careful
353 // about how we combine these two numbers into one,
354 // since the cardinal value of the score is used for
355 // comparisons with hits of other types. It isn't
356 // sufficient to just worry about the ordinal relationship
357 // between two scores.
358 t
*= HalfLifeMultiplierFromProperty (hit
, 0.25,
359 "fixme:received", "fixme:sentdate");
361 t
*= AnalyzeYourRecipientsMultiplier (hit
);