sup 0.8.1 → 0.9

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (67) hide show
  1. data/CONTRIBUTORS +13 -6
  2. data/History.txt +19 -0
  3. data/ReleaseNotes +35 -0
  4. data/bin/sup +82 -77
  5. data/bin/sup-add +7 -7
  6. data/bin/sup-config +104 -85
  7. data/bin/sup-dump +4 -5
  8. data/bin/sup-recover-sources +9 -10
  9. data/bin/sup-sync +121 -100
  10. data/bin/sup-sync-back +18 -15
  11. data/bin/sup-tweak-labels +24 -21
  12. data/lib/sup.rb +53 -33
  13. data/lib/sup/account.rb +0 -2
  14. data/lib/sup/buffer.rb +47 -22
  15. data/lib/sup/colormap.rb +6 -6
  16. data/lib/sup/contact.rb +0 -2
  17. data/lib/sup/crypto.rb +34 -23
  18. data/lib/sup/draft.rb +6 -14
  19. data/lib/sup/ferret_index.rb +471 -0
  20. data/lib/sup/hook.rb +30 -43
  21. data/lib/sup/hook.rb.BACKUP.8625.rb +158 -0
  22. data/lib/sup/hook.rb.BACKUP.8681.rb +158 -0
  23. data/lib/sup/hook.rb.BASE.8625.rb +155 -0
  24. data/lib/sup/hook.rb.BASE.8681.rb +155 -0
  25. data/lib/sup/hook.rb.LOCAL.8625.rb +142 -0
  26. data/lib/sup/hook.rb.LOCAL.8681.rb +142 -0
  27. data/lib/sup/hook.rb.REMOTE.8625.rb +145 -0
  28. data/lib/sup/hook.rb.REMOTE.8681.rb +145 -0
  29. data/lib/sup/imap.rb +18 -8
  30. data/lib/sup/index.rb +70 -528
  31. data/lib/sup/interactive-lock.rb +74 -0
  32. data/lib/sup/keymap.rb +26 -26
  33. data/lib/sup/label.rb +2 -4
  34. data/lib/sup/logger.rb +54 -35
  35. data/lib/sup/maildir.rb +41 -6
  36. data/lib/sup/mbox.rb +1 -1
  37. data/lib/sup/mbox/loader.rb +18 -6
  38. data/lib/sup/mbox/ssh-file.rb +1 -7
  39. data/lib/sup/message-chunks.rb +36 -23
  40. data/lib/sup/message.rb +126 -46
  41. data/lib/sup/mode.rb +3 -2
  42. data/lib/sup/modes/console-mode.rb +108 -0
  43. data/lib/sup/modes/edit-message-mode.rb +15 -5
  44. data/lib/sup/modes/inbox-mode.rb +2 -4
  45. data/lib/sup/modes/label-list-mode.rb +1 -1
  46. data/lib/sup/modes/line-cursor-mode.rb +18 -18
  47. data/lib/sup/modes/log-mode.rb +29 -16
  48. data/lib/sup/modes/poll-mode.rb +7 -9
  49. data/lib/sup/modes/reply-mode.rb +5 -3
  50. data/lib/sup/modes/scroll-mode.rb +2 -2
  51. data/lib/sup/modes/search-results-mode.rb +9 -11
  52. data/lib/sup/modes/text-mode.rb +2 -2
  53. data/lib/sup/modes/thread-index-mode.rb +26 -16
  54. data/lib/sup/modes/thread-view-mode.rb +84 -39
  55. data/lib/sup/person.rb +6 -8
  56. data/lib/sup/poll.rb +46 -47
  57. data/lib/sup/rfc2047.rb +1 -5
  58. data/lib/sup/sent.rb +27 -20
  59. data/lib/sup/source.rb +90 -13
  60. data/lib/sup/textfield.rb +4 -4
  61. data/lib/sup/thread.rb +15 -13
  62. data/lib/sup/undo.rb +0 -1
  63. data/lib/sup/update.rb +0 -1
  64. data/lib/sup/util.rb +51 -43
  65. data/lib/sup/xapian_index.rb +566 -0
  66. metadata +57 -46
  67. data/lib/sup/suicide.rb +0 -36
@@ -1,20 +1,20 @@
1
- ## the index structure for redwood. interacts with ferret.
1
+ ## Index interface, subclassed by Ferret indexer.
2
2
 
3
3
  require 'fileutils'
4
- require 'ferret'
5
- require 'fastthread'
6
4
 
7
5
  begin
8
6
  require 'chronic'
9
7
  $have_chronic = true
10
8
  rescue LoadError => e
11
- Redwood::log "optional 'chronic' library not found (run 'gem install chronic' to install)"
9
+ debug "optional 'chronic' library not found; date-time query restrictions disabled"
12
10
  $have_chronic = false
13
11
  end
14
12
 
15
13
  module Redwood
16
14
 
17
- class Index
15
+ class BaseIndex
16
+ include InteractiveLock
17
+
18
18
  class LockError < StandardError
19
19
  def initialize h
20
20
  @h = h
@@ -25,34 +25,15 @@ class Index
25
25
 
26
26
  include Singleton
27
27
 
28
- ## these two accessors should ONLY be used by single-threaded programs.
29
- ## otherwise you will have a naughty ferret on your hands.
30
- attr_reader :index
31
- alias ferret index
32
-
33
28
  def initialize dir=BASE_DIR
34
- @index_mutex = Monitor.new
35
-
36
29
  @dir = dir
37
- @sources = {}
38
- @sources_dirty = false
39
- @source_mutex = Monitor.new
40
-
41
- wsa = Ferret::Analysis::WhiteSpaceAnalyzer.new false
42
- sa = Ferret::Analysis::StandardAnalyzer.new [], true
43
- @analyzer = Ferret::Analysis::PerFieldAnalyzer.new wsa
44
- @analyzer[:body] = sa
45
- @analyzer[:subject] = sa
46
- @qparser ||= Ferret::QueryParser.new :default_field => :body, :analyzer => @analyzer, :or_default => false
47
30
  @lock = Lockfile.new lockfile, :retries => 0, :max_age => nil
48
-
49
- self.class.i_am_the_instance self
50
31
  end
51
32
 
52
33
  def lockfile; File.join @dir, "lock" end
53
34
 
54
35
  def lock
55
- Redwood::log "locking #{lockfile}..."
36
+ debug "locking #{lockfile}..."
56
37
  begin
57
38
  @lock.lock
58
39
  rescue Lockfile::MaxTriesLockError
@@ -74,239 +55,60 @@ class Index
74
55
  @lock_update_thread = nil
75
56
  end
76
57
 
77
- def possibly_pluralize number_of, kind
78
- "#{number_of} #{kind}" +
79
- if number_of == 1 then "" else "s" end
80
- end
81
-
82
- def fancy_lock_error_message_for e
83
- secs = (Time.now - e.mtime).to_i
84
- mins = secs / 60
85
- time =
86
- if mins == 0
87
- possibly_pluralize secs , "second"
88
- else
89
- possibly_pluralize mins, "minute"
90
- end
91
-
92
- <<EOS
93
- Error: the sup index is locked by another process! User '#{e.user}' on
94
- host '#{e.host}' is running #{e.pname} with pid #{e.pid}. The process was alive
95
- as of #{time} ago.
96
- EOS
97
- end
98
-
99
- def lock_or_die
100
- begin
101
- lock
102
- rescue LockError => e
103
- $stderr.puts fancy_lock_error_message_for(e)
104
- $stderr.puts <<EOS
105
-
106
- You can wait for the process to finish, or, if it crashed and left a
107
- stale lock file behind, you can manually delete #{@lock.path}.
108
- EOS
109
- exit
110
- end
111
- end
112
-
113
58
  def unlock
114
59
  if @lock && @lock.locked?
115
- Redwood::log "unlocking #{lockfile}..."
60
+ debug "unlocking #{lockfile}..."
116
61
  @lock.unlock
117
62
  end
118
63
  end
119
64
 
120
65
  def load
121
- load_sources
66
+ SourceManager.load_sources
122
67
  load_index
123
68
  end
124
69
 
125
70
  def save
126
- Redwood::log "saving index and sources..."
71
+ debug "saving index and sources..."
127
72
  FileUtils.mkdir_p @dir unless File.exists? @dir
128
- save_sources
73
+ SourceManager.save_sources
129
74
  save_index
130
75
  end
131
76
 
132
- def add_source source
133
- @source_mutex.synchronize do
134
- raise "duplicate source!" if @sources.include? source
135
- @sources_dirty = true
136
- max = @sources.max_of { |id, s| s.is_a?(DraftLoader) || s.is_a?(SentLoader) ? 0 : id }
137
- source.id ||= (max || 0) + 1
138
- ##source.id += 1 while @sources.member? source.id
139
- @sources[source.id] = source
140
- end
141
- end
142
-
143
- def sources
144
- ## favour the inbox by listing non-archived sources first
145
- @source_mutex.synchronize { @sources.values }.sort_by { |s| s.id }.partition { |s| !s.archived? }.flatten
77
+ def load_index
78
+ unimplemented
146
79
  end
147
80
 
148
- def source_for uri; sources.find { |s| s.is_source_for? uri }; end
149
- def usual_sources; sources.find_all { |s| s.usual? }; end
81
+ def add_message m; unimplemented end
82
+ def update_message m; unimplemented end
83
+ def update_message_state m; unimplemented end
150
84
 
151
- def load_index dir=File.join(@dir, "ferret")
152
- if File.exists? dir
153
- Redwood::log "loading index..."
154
- @index_mutex.synchronize do
155
- @index = Ferret::Index::Index.new(:path => dir, :analyzer => @analyzer)
156
- Redwood::log "loaded index of #{@index.size} messages"
157
- end
158
- else
159
- Redwood::log "creating index..."
160
- @index_mutex.synchronize do
161
- field_infos = Ferret::Index::FieldInfos.new :store => :yes
162
- field_infos.add_field :message_id, :index => :untokenized
163
- field_infos.add_field :source_id
164
- field_infos.add_field :source_info
165
- field_infos.add_field :date, :index => :untokenized
166
- field_infos.add_field :body
167
- field_infos.add_field :label
168
- field_infos.add_field :attachments
169
- field_infos.add_field :subject
170
- field_infos.add_field :from
171
- field_infos.add_field :to
172
- field_infos.add_field :refs
173
- field_infos.add_field :snippet, :index => :no, :term_vector => :no
174
- field_infos.create_index dir
175
- @index = Ferret::Index::Index.new(:path => dir, :analyzer => @analyzer)
176
- end
177
- end
85
+ def save_index fn
86
+ unimplemented
178
87
  end
179
88
 
180
- ## Syncs the message to the index, replacing any previous version. adding
181
- ## either way. Index state will be determined by the message's #labels
182
- ## accessor.
183
- ##
184
- ## if need_load is false, docid and entry are assumed to be set to the
185
- ## result of load_entry_for_id (which can be nil).
186
- def sync_message m, need_load=true, docid=nil, entry=nil, opts={}
187
- docid, entry = load_entry_for_id m.id if need_load
188
-
189
- raise "no source info for message #{m.id}" unless m.source && m.source_info
190
- @index_mutex.synchronize do
191
- raise "trying to delete non-corresponding entry #{docid} with index message-id #{@index[docid][:message_id].inspect} and parameter message id #{m.id.inspect}" if docid && @index[docid][:message_id] != m.id
192
- end
193
-
194
- source_id = if m.source.is_a? Integer
195
- m.source
196
- else
197
- m.source.id or raise "unregistered source #{m.source} (id #{m.source.id.inspect})"
198
- end
199
-
200
- snippet = if m.snippet_contains_encrypted_content? && $config[:discard_snippets_from_encrypted_messages]
201
- ""
202
- else
203
- m.snippet
204
- end
205
-
206
- ## write the new document to the index. if the entry already exists in the
207
- ## index, reuse it (which avoids having to reload the entry from the source,
208
- ## which can be quite expensive for e.g. large threads of IMAP actions.)
209
- ##
210
- ## exception: if the index entry belongs to an earlier version of the
211
- ## message, use everything from the new message instead, but union the
212
- ## flags. this allows messages sent to mailing lists to have their header
213
- ## updated and to have flags set properly.
214
- ##
215
- ## minor hack: messages in sources with lower ids have priority over
216
- ## messages in sources with higher ids. so messages in the inbox will
217
- ## override everyone, and messages in the sent box will be overridden
218
- ## by everyone else.
219
- ##
220
- ## written in this manner to support previous versions of the index which
221
- ## did not keep around the entry body. upgrading is thus seamless.
222
- entry ||= {}
223
- labels = m.labels.uniq # override because this is the new state, unless...
224
-
225
- ## if we are a later version of a message, ignore what's in the index,
226
- ## but merge in the labels.
227
- if entry[:source_id] && entry[:source_info] && entry[:label] &&
228
- ((entry[:source_id].to_i > source_id) || (entry[:source_info].to_i < m.source_info))
229
- labels = (entry[:label].symbolistize + m.labels).uniq
230
- #Redwood::log "found updated version of message #{m.id}: #{m.subj}"
231
- #Redwood::log "previous version was at #{entry[:source_id].inspect}:#{entry[:source_info].inspect}, this version at #{source_id.inspect}:#{m.source_info.inspect}"
232
- #Redwood::log "merged labels are #{labels.inspect} (index #{entry[:label].inspect}, message #{m.labels.inspect})"
233
- entry = {}
234
- end
235
-
236
- ## if force_overwite is true, ignore what's in the index. this is used
237
- ## primarily by sup-sync to force index updates.
238
- entry = {} if opts[:force_overwrite]
239
-
240
- d = {
241
- :message_id => m.id,
242
- :source_id => source_id,
243
- :source_info => m.source_info,
244
- :date => (entry[:date] || m.date.to_indexable_s),
245
- :body => (entry[:body] || m.indexable_content),
246
- :snippet => snippet, # always override
247
- :label => labels.uniq.join(" "),
248
- :attachments => (entry[:attachments] || m.attachments.uniq.join(" ")),
249
-
250
- ## always override :from and :to.
251
- ## older versions of Sup would often store the wrong thing in the index
252
- ## (because they were canonicalizing email addresses, resulting in the
253
- ## wrong name associated with each.) the correct address is read from
254
- ## the original header when these messages are opened in thread-view-mode,
255
- ## so this allows people to forcibly update the address in the index by
256
- ## marking those threads for saving.
257
- :from => (m.from ? m.from.indexable_content : ""),
258
- :to => (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" "),
259
-
260
- :subject => (entry[:subject] || wrap_subj(Message.normalize_subj(m.subj))),
261
- :refs => (entry[:refs] || (m.refs + m.replytos).uniq.join(" ")),
262
- }
263
-
264
- @index_mutex.synchronize do
265
- @index.delete docid if docid
266
- @index.add_document d
267
- end
268
-
269
- ## this hasn't been triggered in a long time.
270
- ## docid, entry = load_entry_for_id m.id
271
- ## raise "just added message #{m.id.inspect} but couldn't find it in a search" unless docid
89
+ def contains_id? id
90
+ unimplemented
272
91
  end
273
92
 
274
- def save_index fn=File.join(@dir, "ferret")
275
- # don't have to do anything, apparently
276
- end
93
+ def contains? m; contains_id? m.id end
277
94
 
278
- def contains_id? id
279
- @index_mutex.synchronize { @index.search(Ferret::Search::TermQuery.new(:message_id, id)).total_hits > 0 }
95
+ def size
96
+ unimplemented
280
97
  end
281
- def contains? m; contains_id? m.id end
282
- def size; @index_mutex.synchronize { @index.size } end
98
+
283
99
  def empty?; size == 0 end
284
100
 
285
- ## you should probably not call this on a block that doesn't break
101
+ ## Yields a message-id and message-building lambda for each
102
+ ## message that matches the given query, in descending date order.
103
+ ## You should probably not call this on a block that doesn't break
286
104
  ## rather quickly because the results can be very large.
287
- EACH_BY_DATE_NUM = 100
288
- def each_id_by_date opts={}
289
- return if empty? # otherwise ferret barfs ###TODO: remove this once my ferret patch is accepted
290
- query = build_query opts
291
- offset = 0
292
- while true
293
- limit = (opts[:limit])? [EACH_BY_DATE_NUM, opts[:limit] - offset].min : EACH_BY_DATE_NUM
294
- results = @index_mutex.synchronize { @index.search query, :sort => "date DESC", :limit => limit, :offset => offset }
295
- Redwood::log "got #{results.total_hits} results for query (offset #{offset}) #{query.inspect}"
296
- results.hits.each do |hit|
297
- yield @index_mutex.synchronize { @index[hit.doc][:message_id] }, lambda { build_message hit.doc }
298
- end
299
- break if opts[:limit] and offset >= opts[:limit] - limit
300
- break if offset >= results.total_hits - limit
301
- offset += limit
302
- end
105
+ def each_id_by_date query={}
106
+ unimplemented
303
107
  end
304
108
 
305
- def num_results_for opts={}
306
- return 0 if empty? # otherwise ferret barfs ###TODO: remove this once my ferret patch is accepted
307
-
308
- q = build_query opts
309
- @index_mutex.synchronize { @index.search(q, :limit => 1).total_hits }
109
+ ## Return the number of matches for query in the index
110
+ def num_results_for query={}
111
+ unimplemented
310
112
  end
311
113
 
312
114
  ## yield all messages in the thread containing 'm' by repeatedly
@@ -317,328 +119,68 @@ EOS
317
119
  ## only two options, :limit and :skip_killed. if :skip_killed is
318
120
  ## true, stops loading any thread if a message with a :killed flag
319
121
  ## is found.
320
- SAME_SUBJECT_DATE_LIMIT = 7
321
- MAX_CLAUSES = 1000
322
122
  def each_message_in_thread_for m, opts={}
323
- #Redwood::log "Building thread for #{m.id}: #{m.subj}"
324
- messages = {}
325
- searched = {}
326
- num_queries = 0
327
-
328
- pending = [m.id]
329
- if $config[:thread_by_subject] # do subject queries
330
- date_min = m.date - (SAME_SUBJECT_DATE_LIMIT * 12 * 3600)
331
- date_max = m.date + (SAME_SUBJECT_DATE_LIMIT * 12 * 3600)
332
-
333
- q = Ferret::Search::BooleanQuery.new true
334
- sq = Ferret::Search::PhraseQuery.new(:subject)
335
- wrap_subj(Message.normalize_subj(m.subj)).split.each do |t|
336
- sq.add_term t
337
- end
338
- q.add_query sq, :must
339
- q.add_query Ferret::Search::RangeQuery.new(:date, :>= => date_min.to_indexable_s, :<= => date_max.to_indexable_s), :must
340
-
341
- q = build_query :qobj => q
342
-
343
- p1 = @index_mutex.synchronize { @index.search(q).hits.map { |hit| @index[hit.doc][:message_id] } }
344
- Redwood::log "found #{p1.size} results for subject query #{q}"
345
-
346
- p2 = @index_mutex.synchronize { @index.search(q.to_s, :limit => :all).hits.map { |hit| @index[hit.doc][:message_id] } }
347
- Redwood::log "found #{p2.size} results in string form"
348
-
349
- pending = (pending + p1 + p2).uniq
350
- end
351
-
352
- until pending.empty? || (opts[:limit] && messages.size >= opts[:limit])
353
- q = Ferret::Search::BooleanQuery.new true
354
- # this disappeared in newer ferrets... wtf.
355
- # q.max_clause_count = 2048
356
-
357
- lim = [MAX_CLAUSES / 2, pending.length].min
358
- pending[0 ... lim].each do |id|
359
- searched[id] = true
360
- q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
361
- q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
362
- end
363
- pending = pending[lim .. -1]
364
-
365
- q = build_query :qobj => q
366
-
367
- num_queries += 1
368
- killed = false
369
- @index_mutex.synchronize do
370
- @index.search_each(q, :limit => :all) do |docid, score|
371
- break if opts[:limit] && messages.size >= opts[:limit]
372
- if @index[docid][:label].split(/\s+/).include?("killed") && opts[:skip_killed]
373
- killed = true
374
- break
375
- end
376
- mid = @index[docid][:message_id]
377
- unless messages.member?(mid)
378
- #Redwood::log "got #{mid} as a child of #{id}"
379
- messages[mid] ||= lambda { build_message docid }
380
- refs = @index[docid][:refs].split
381
- pending += refs.select { |id| !searched[id] }
382
- end
383
- end
384
- end
385
- end
386
-
387
- if killed
388
- Redwood::log "thread for #{m.id} is killed, ignoring"
389
- false
390
- else
391
- Redwood::log "ran #{num_queries} queries to build thread of #{messages.size} messages for #{m.id}: #{m.subj}" if num_queries > 0
392
- messages.each { |mid, builder| yield mid, builder }
393
- true
394
- end
123
+ unimplemented
395
124
  end
396
125
 
397
- ## builds a message object from a ferret result
398
- def build_message docid
399
- @index_mutex.synchronize do
400
- doc = @index[docid]
401
-
402
- source = @source_mutex.synchronize { @sources[doc[:source_id].to_i] }
403
- raise "invalid source #{doc[:source_id]}" unless source
404
-
405
- #puts "building message #{doc[:message_id]} (#{source}##{doc[:source_info]})"
406
-
407
- fake_header = {
408
- "date" => Time.at(doc[:date].to_i),
409
- "subject" => unwrap_subj(doc[:subject]),
410
- "from" => doc[:from],
411
- "to" => doc[:to].split.join(", "), # reformat
412
- "message-id" => doc[:message_id],
413
- "references" => doc[:refs].split.map { |x| "<#{x}>" }.join(" "),
414
- }
415
-
416
- m = Message.new :source => source, :source_info => doc[:source_info].to_i,
417
- :labels => doc[:label].symbolistize,
418
- :snippet => doc[:snippet]
419
- m.parse_header fake_header
420
- m
421
- end
126
+ ## Load message with the given message-id from the index
127
+ def build_message id
128
+ unimplemented
422
129
  end
423
130
 
424
- def fresh_thread_id; @next_thread_id += 1; end
425
- def wrap_subj subj; "__START_SUBJECT__ #{subj} __END_SUBJECT__"; end
426
- def unwrap_subj subj; subj =~ /__START_SUBJECT__ (.*?) __END_SUBJECT__/ && $1; end
427
-
428
- def drop_entry docno; @index_mutex.synchronize { @index.delete docno } end
429
-
430
- def load_entry_for_id mid
431
- @index_mutex.synchronize do
432
- results = @index.search Ferret::Search::TermQuery.new(:message_id, mid)
433
- return if results.total_hits == 0
434
- docid = results.hits[0].doc
435
- entry = @index[docid]
436
- entry_dup = entry.fields.inject({}) { |h, f| h[f] = entry[f]; h }
437
- [docid, entry_dup]
438
- end
131
+ ## Delete message with the given message-id from the index
132
+ def delete id
133
+ unimplemented
439
134
  end
440
135
 
441
- def load_contacts emails, h={}
442
- q = Ferret::Search::BooleanQuery.new true
443
- emails.each do |e|
444
- qq = Ferret::Search::BooleanQuery.new true
445
- qq.add_query Ferret::Search::TermQuery.new(:from, e), :should
446
- qq.add_query Ferret::Search::TermQuery.new(:to, e), :should
447
- q.add_query qq
448
- end
449
- q.add_query Ferret::Search::TermQuery.new(:label, "spam"), :must_not
450
-
451
- Redwood::log "contact search: #{q}"
452
- contacts = {}
453
- num = h[:num] || 20
454
- @index_mutex.synchronize do
455
- @index.search_each q, :sort => "date DESC", :limit => :all do |docid, score|
456
- break if contacts.size >= num
457
- #Redwood::log "got message #{docid} to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
458
- f = @index[docid][:from]
459
- t = @index[docid][:to]
460
-
461
- if AccountManager.is_account_email? f
462
- t.split(" ").each { |e| contacts[Person.from_address(e)] = true }
463
- else
464
- contacts[Person.from_address(f)] = true
465
- end
466
- end
467
- end
136
+ ## Given an array of email addresses, return an array of Person objects that
137
+ ## have sent mail to or received mail from any of the given addresses.
138
+ def load_contacts email_addresses, h={}
139
+ unimplemented
140
+ end
468
141
 
469
- contacts.keys.compact
142
+ ## Yield each message-id matching query
143
+ def each_id query={}
144
+ unimplemented
470
145
  end
471
146
 
472
- def load_sources fn=Redwood::SOURCE_FN
473
- source_array = (Redwood::load_yaml_obj(fn) || []).map { |o| Recoverable.new o }
474
- @source_mutex.synchronize do
475
- @sources = Hash[*(source_array).map { |s| [s.id, s] }.flatten]
476
- @sources_dirty = false
147
+ ## Yield each message matching query
148
+ def each_message query={}, &b
149
+ each_id query do |id|
150
+ yield build_message(id)
477
151
  end
478
152
  end
479
153
 
480
- def has_any_from_source_with_label? source, label
481
- q = Ferret::Search::BooleanQuery.new
482
- q.add_query Ferret::Search::TermQuery.new("source_id", source.id.to_s), :must
483
- q.add_query Ferret::Search::TermQuery.new("label", label.to_s), :must
484
- @index_mutex.synchronize { @index.search(q, :limit => 1).total_hits > 0 }
154
+ ## Implementation-specific optimization step
155
+ def optimize
156
+ unimplemented
485
157
  end
486
158
 
487
- ## takes a user query string and returns the list of docids for messages
488
- ## that match the query.
489
- ##
490
- ## messages can then be loaded from the index with #build_message.
491
- ##
492
- ## raises a ParseError if the parsing failed.
493
- def run_query query
494
- qobj, opts = Redwood::Index.parse_user_query_string query
495
- query = Redwood::Index.build_query opts.merge(:qobj => qobj)
496
- results = @index.search query, :limit => (opts[:limit] || :all)
497
- results.hits.map { |hit| hit.doc }
159
+ ## Return the id source of the source the message with the given message-id
160
+ ## was synced from
161
+ def source_for_id id
162
+ unimplemented
498
163
  end
499
164
 
500
- protected
501
-
502
165
  class ParseError < StandardError; end
503
166
 
504
- ## parse a query string from the user. returns a query object and a set of
505
- ## extra flags; both of these are meant to be passed to #build_query.
167
+ ## parse a query string from the user. returns a query object
168
+ ## that can be passed to any index method with a 'query'
169
+ ## argument.
506
170
  ##
507
171
  ## raises a ParseError if something went wrong.
508
- def parse_user_query_string s
509
- extraopts = {}
510
-
511
- subs = s.gsub(/\b(to|from):(\S+)\b/) do
512
- field, name = $1, $2
513
- if(p = ContactManager.contact_for(name))
514
- [field, p.email]
515
- elsif name == "me"
516
- [field, "(" + AccountManager.user_emails.join("||") + ")"]
517
- else
518
- [field, name]
519
- end.join(":")
520
- end
521
-
522
- ## if we see a label:deleted or a label:spam term anywhere in the query
523
- ## string, we set the extra load_spam or load_deleted options to true.
524
- ## bizarre? well, because the query allows arbitrary parenthesized boolean
525
- ## expressions, without fully parsing the query, we can't tell whether
526
- ## the user is explicitly directing us to search spam messages or not.
527
- ## e.g. if the string is -(-(-(-(-label:spam)))), does the user want to
528
- ## search spam messages or not?
529
- ##
530
- ## so, we rely on the fact that turning these extra options ON turns OFF
531
- ## the adding of "-label:deleted" or "-label:spam" terms at the very
532
- ## final stage of query processing. if the user wants to search spam
533
- ## messages, not adding that is the right thing; if he doesn't want to
534
- ## search spam messages, then not adding it won't have any effect.
535
- extraopts[:load_spam] = true if subs =~ /\blabel:spam\b/
536
- extraopts[:load_deleted] = true if subs =~ /\blabel:deleted\b/
537
-
538
- ## gmail style "is" operator
539
- subs = subs.gsub(/\b(is|has):(\S+)\b/) do
540
- field, label = $1, $2
541
- case label
542
- when "read"
543
- "-label:unread"
544
- when "spam"
545
- extraopts[:load_spam] = true
546
- "label:spam"
547
- when "deleted"
548
- extraopts[:load_deleted] = true
549
- "label:deleted"
550
- else
551
- "label:#{$2}"
552
- end
553
- end
554
-
555
- ## gmail style attachments "filename" and "filetype" searches
556
- subs = subs.gsub(/\b(filename|filetype):(\((.+?)\)\B|(\S+)\b)/) do
557
- field, name = $1, ($3 || $4)
558
- case field
559
- when "filename"
560
- Redwood::log "filename - translated #{field}:#{name} to attachments:(#{name.downcase})"
561
- "attachments:(#{name.downcase})"
562
- when "filetype"
563
- Redwood::log "filetype - translated #{field}:#{name} to attachments:(*.#{name.downcase})"
564
- "attachments:(*.#{name.downcase})"
565
- end
566
- end
567
-
568
- if $have_chronic
569
- subs = subs.gsub(/\b(before|on|in|during|after):(\((.+?)\)\B|(\S+)\b)/) do
570
- field, datestr = $1, ($3 || $4)
571
- realdate = Chronic.parse datestr, :guess => false, :context => :past
572
- if realdate
573
- case field
574
- when "after"
575
- Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.end}"
576
- "date:(>= #{sprintf "%012d", realdate.end.to_i})"
577
- when "before"
578
- Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
579
- "date:(<= #{sprintf "%012d", realdate.begin.to_i})"
580
- else
581
- Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate}"
582
- "date:(<= #{sprintf "%012d", realdate.end.to_i}) date:(>= #{sprintf "%012d", realdate.begin.to_i})"
583
- end
584
- else
585
- raise ParseError, "can't understand date #{datestr.inspect}"
586
- end
587
- end
588
- end
589
-
590
- ## limit:42 restrict the search to 42 results
591
- subs = subs.gsub(/\blimit:(\S+)\b/) do
592
- lim = $1
593
- if lim =~ /^\d+$/
594
- extraopts[:limit] = lim.to_i
595
- ''
596
- else
597
- raise ParseError, "non-numeric limit #{lim.inspect}"
598
- end
599
- end
600
-
601
- begin
602
- [@qparser.parse(subs), extraopts]
603
- rescue Ferret::QueryParser::QueryParseException => e
604
- raise ParseError, e.message
605
- end
606
- end
607
-
608
- def build_query opts
609
- query = Ferret::Search::BooleanQuery.new
610
- query.add_query opts[:qobj], :must if opts[:qobj]
611
- labels = ([opts[:label]] + (opts[:labels] || [])).compact
612
- labels.each { |t| query.add_query Ferret::Search::TermQuery.new("label", t.to_s), :must }
613
- if opts[:participants]
614
- q2 = Ferret::Search::BooleanQuery.new
615
- opts[:participants].each do |p|
616
- q2.add_query Ferret::Search::TermQuery.new("from", p.email), :should
617
- q2.add_query Ferret::Search::TermQuery.new("to", p.email), :should
618
- end
619
- query.add_query q2, :must
620
- end
621
-
622
- query.add_query Ferret::Search::TermQuery.new("label", "spam"), :must_not unless opts[:load_spam] || labels.include?(:spam)
623
- query.add_query Ferret::Search::TermQuery.new("label", "deleted"), :must_not unless opts[:load_deleted] || labels.include?(:deleted)
624
- query.add_query Ferret::Search::TermQuery.new("label", "killed"), :must_not if opts[:skip_killed]
625
- query
172
+ def parse_query s
173
+ unimplemented
626
174
  end
175
+ end
627
176
 
628
- def save_sources fn=Redwood::SOURCE_FN
629
- @source_mutex.synchronize do
630
- if @sources_dirty || @sources.any? { |id, s| s.dirty? }
631
- bakfn = fn + ".bak"
632
- if File.exists? fn
633
- File.chmod 0600, fn
634
- FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(fn) == 0
635
- end
636
- Redwood::save_yaml_obj sources.sort_by { |s| s.id.to_i }, fn, true
637
- File.chmod 0600, fn
638
- end
639
- @sources_dirty = false
640
- end
641
- end
177
+ index_name = ENV['SUP_INDEX'] || $config[:index] || DEFAULT_INDEX
178
+ case index_name
179
+ when "xapian"; require "sup/xapian_index"
180
+ when "ferret"; require "sup/ferret_index"
181
+ else fail "unknown index type #{index_name.inspect}"
642
182
  end
183
+ Index = Redwood.const_get "#{index_name.capitalize}Index"
184
+ debug "using index #{Index.name}"
643
185
 
644
186
  end