sup 0.7 → 0.8

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 (46) hide show
  1. data/CONTRIBUTORS +8 -3
  2. data/History.txt +19 -0
  3. data/README.txt +45 -44
  4. data/ReleaseNotes +6 -0
  5. data/bin/sup +36 -5
  6. data/bin/sup-add +0 -0
  7. data/bin/sup-config +0 -0
  8. data/bin/sup-dump +0 -0
  9. data/bin/sup-recover-sources +8 -12
  10. data/bin/sup-sync +22 -16
  11. data/bin/sup-sync-back +1 -1
  12. data/bin/sup-tweak-labels +8 -8
  13. data/lib/sup.rb +3 -17
  14. data/lib/sup/account.rb +2 -3
  15. data/lib/sup/buffer.rb +21 -10
  16. data/lib/sup/colormap.rb +30 -27
  17. data/lib/sup/contact.rb +1 -1
  18. data/lib/sup/draft.rb +1 -3
  19. data/lib/sup/imap.rb +1 -1
  20. data/lib/sup/index.rb +70 -48
  21. data/lib/sup/label.rb +12 -10
  22. data/lib/sup/logger.rb +1 -1
  23. data/lib/sup/maildir.rb +1 -1
  24. data/lib/sup/mbox.rb +13 -70
  25. data/lib/sup/mbox/loader.rb +26 -15
  26. data/lib/sup/message-chunks.rb +18 -6
  27. data/lib/sup/message.rb +56 -67
  28. data/lib/sup/mode.rb +2 -1
  29. data/lib/sup/modes/buffer-list-mode.rb +6 -2
  30. data/lib/sup/modes/compose-mode.rb +0 -1
  31. data/lib/sup/modes/contact-list-mode.rb +1 -1
  32. data/lib/sup/modes/edit-message-mode.rb +37 -9
  33. data/lib/sup/modes/inbox-mode.rb +34 -0
  34. data/lib/sup/modes/label-list-mode.rb +10 -3
  35. data/lib/sup/modes/reply-mode.rb +24 -13
  36. data/lib/sup/modes/resume-mode.rb +2 -0
  37. data/lib/sup/modes/scroll-mode.rb +10 -9
  38. data/lib/sup/modes/search-results-mode.rb +2 -2
  39. data/lib/sup/modes/thread-index-mode.rb +157 -38
  40. data/lib/sup/modes/thread-view-mode.rb +27 -11
  41. data/lib/sup/person.rb +22 -73
  42. data/lib/sup/poll.rb +18 -20
  43. data/lib/sup/source.rb +44 -0
  44. data/lib/sup/undo.rb +39 -0
  45. data/lib/sup/util.rb +25 -16
  46. metadata +46 -45
data/lib/sup.rb CHANGED
@@ -6,19 +6,6 @@ require 'fileutils'
6
6
  require 'gettext'
7
7
  require 'curses'
8
8
 
9
- ## the following magic enables wide characters when used with a ruby
10
- ## ncurses.so that's been compiled against libncursesw. (note the w.) why
11
- ## this works, i have no idea. much like pretty much every aspect of
12
- ## dealing with curses. cargo cult programming at its best.
13
-
14
- require 'dl/import'
15
- module LibC
16
- extend DL::Importable
17
- dlload Config::CONFIG['arch'] =~ /darwin/ ? "libc.dylib" : "libc.so.6"
18
- extern "void setlocale(int, const char *)"
19
- end
20
- LibC.setlocale(6, "") # LC_ALL == 6
21
-
22
9
  class Object
23
10
  ## this is for debugging purposes because i keep calling #id on the
24
11
  ## wrong object and i want it to throw an exception
@@ -46,14 +33,13 @@ class Module
46
33
  end
47
34
 
48
35
  module Redwood
49
- VERSION = "0.7"
36
+ VERSION = "0.8"
50
37
 
51
38
  BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
52
39
  CONFIG_FN = File.join(BASE_DIR, "config.yaml")
53
40
  COLOR_FN = File.join(BASE_DIR, "colors.yaml")
54
41
  SOURCE_FN = File.join(BASE_DIR, "sources.yaml")
55
42
  LABEL_FN = File.join(BASE_DIR, "labels.txt")
56
- PERSON_FN = File.join(BASE_DIR, "people.txt")
57
43
  CONTACT_FN = File.join(BASE_DIR, "contacts.txt")
58
44
  DRAFT_DIR = File.join(BASE_DIR, "drafts")
59
45
  SENT_FN = File.join(BASE_DIR, "sent.mbox")
@@ -115,7 +101,6 @@ module Redwood
115
101
  end
116
102
 
117
103
  def start
118
- Redwood::PersonManager.new Redwood::PERSON_FN
119
104
  Redwood::SentManager.new Redwood::SENT_FN
120
105
  Redwood::ContactManager.new Redwood::CONTACT_FN
121
106
  Redwood::LabelManager.new Redwood::LABEL_FN
@@ -125,12 +110,12 @@ module Redwood
125
110
  Redwood::PollManager.new
126
111
  Redwood::SuicideManager.new Redwood::SUICIDE_FN
127
112
  Redwood::CryptoManager.new
113
+ Redwood::UndoManager.new
128
114
  end
129
115
 
130
116
  def finish
131
117
  Redwood::LabelManager.save if Redwood::LabelManager.instantiated?
132
118
  Redwood::ContactManager.save if Redwood::ContactManager.instantiated?
133
- Redwood::PersonManager.save if Redwood::PersonManager.instantiated?
134
119
  Redwood::BufferManager.deinstantiate! if Redwood::BufferManager.instantiated?
135
120
  end
136
121
 
@@ -282,6 +267,7 @@ require "sup/tagger"
282
267
  require "sup/draft"
283
268
  require "sup/poll"
284
269
  require "sup/crypto"
270
+ require "sup/undo"
285
271
  require "sup/horizontal-selector"
286
272
  require "sup/modes/line-cursor-mode"
287
273
  require "sup/modes/help-mode"
@@ -5,8 +5,8 @@ class Account < Person
5
5
 
6
6
  def initialize h
7
7
  raise ArgumentError, "no name for account" unless h[:name]
8
- raise ArgumentError, "no name for email" unless h[:name]
9
- super h[:name], h[:email], 0, true
8
+ raise ArgumentError, "no email for account" unless h[:email]
9
+ super h[:name], h[:email]
10
10
  @sendmail = h[:sendmail]
11
11
  @signature = h[:signature]
12
12
  end
@@ -42,7 +42,6 @@ class AccountManager
42
42
  hash[:alternates] ||= []
43
43
 
44
44
  a = Account.new hash
45
- PersonManager.register a
46
45
  @accounts[a] = true
47
46
 
48
47
  if default
@@ -51,8 +51,8 @@ module Redwood
51
51
  class InputSequenceAborted < StandardError; end
52
52
 
53
53
  class Buffer
54
- attr_reader :mode, :x, :y, :width, :height, :title
55
- bool_reader :dirty
54
+ attr_reader :mode, :x, :y, :width, :height, :title, :atime
55
+ bool_reader :dirty, :system
56
56
  bool_accessor :force_to_top
57
57
 
58
58
  def initialize window, mode, width, height, opts={}
@@ -63,6 +63,8 @@ class Buffer
63
63
  @title = opts[:title] || ""
64
64
  @force_to_top = opts[:force_to_top] || false
65
65
  @x, @y, @width, @height = 0, 0, width, height
66
+ @atime = Time.at 0
67
+ @system = opts[:system] || false
66
68
  end
67
69
 
68
70
  def content_height; @height - 1; end
@@ -97,6 +99,7 @@ class Buffer
97
99
  @mode.draw
98
100
  draw_status status
99
101
  commit
102
+ @atime = Time.now
100
103
  end
101
104
 
102
105
  ## s nil means a blank line!
@@ -105,10 +108,16 @@ class Buffer
105
108
 
106
109
  @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
107
110
  s ||= ""
108
- maxl = @width - x
109
- @w.mvaddstr y, x, s[0 ... maxl]
110
- unless s.length >= maxl || opts[:no_fill]
111
- @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
111
+ maxl = @width - x # maximum display width width
112
+ stringl = maxl # string "length"
113
+ ## the next horribleness is thanks to ruby's lack of widechar support
114
+ stringl += 1 while stringl < s.length && s[0 ... stringl].display_length < maxl
115
+ @w.mvaddstr y, x, s[0 ... stringl]
116
+ unless opts[:no_fill]
117
+ l = s.display_length
118
+ unless l >= maxl
119
+ @w.mvaddstr(y, x + l, " " * (maxl - l))
120
+ end
112
121
  end
113
122
  end
114
123
 
@@ -338,7 +347,7 @@ EOS
338
347
  ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
339
348
  ## (opts[:left] || 0))
340
349
  w = Ncurses.stdscr
341
- b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
350
+ b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => opts[:force_to_top], :system => opts[:system]
342
351
  mode.buffer = b
343
352
  @name_map[realtitle] = b
344
353
 
@@ -478,13 +487,15 @@ EOS
478
487
  default = default_labels.join(" ")
479
488
  default += " " unless default.empty?
480
489
 
481
- applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
490
+ # here I would prefer to give more control and allow all_labels instead of
491
+ # user_defined_labels only
492
+ applyable_labels = (LabelManager.user_defined_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
482
493
 
483
494
  answer = ask_many_with_completions domain, question, applyable_labels, default
484
495
 
485
496
  return unless answer
486
497
 
487
- user_labels = answer.split(/\s+/).map { |l| l.intern }
498
+ user_labels = answer.symbolistize
488
499
  user_labels.each do |l|
489
500
  if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
490
501
  BufferManager.flash "'#{l}' is a reserved label!"
@@ -506,7 +517,7 @@ EOS
506
517
  answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
507
518
 
508
519
  if answer
509
- answer.split_on_commas.map { |x| ContactManager.contact_for(x) || PersonManager.person_for(x) }
520
+ answer.split_on_commas.map { |x| ContactManager.contact_for(x) || Person.from_address(x) }
510
521
  end
511
522
  end
512
523
 
@@ -11,42 +11,45 @@ class Colormap
11
11
  Curses::COLOR_YELLOW, Curses::COLOR_BLUE,
12
12
  Curses::COLOR_MAGENTA, Curses::COLOR_CYAN,
13
13
  Curses::COLOR_WHITE, Curses::COLOR_DEFAULT]
14
- NUM_COLORS = 15
14
+ NUM_COLORS = (CURSES_COLORS.size - 1) * (CURSES_COLORS.size - 1)
15
15
 
16
16
  DEFAULT_COLORS = {
17
17
  :status => { :fg => "white", :bg => "blue", :attrs => ["bold"] },
18
- :index_old => { :fg => "white", :bg => "black" },
19
- :index_new => { :fg => "white", :bg => "black", :attrs => ["bold"] },
20
- :index_starred => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
21
- :index_draft => { :fg => "red", :bg => "black", :attrs => ["bold"] },
22
- :labellist_old => { :fg => "white", :bg => "black" },
23
- :labellist_new => { :fg => "white", :bg => "black", :attrs => ["bold"] },
24
- :twiddle => { :fg => "blue", :bg => "black" },
25
- :label => { :fg => "yellow", :bg => "black" },
18
+ :index_old => { :fg => "white", :bg => "default" },
19
+ :index_new => { :fg => "white", :bg => "default", :attrs => ["bold"] },
20
+ :index_starred => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
21
+ :index_draft => { :fg => "red", :bg => "default", :attrs => ["bold"] },
22
+ :labellist_old => { :fg => "white", :bg => "default" },
23
+ :labellist_new => { :fg => "white", :bg => "default", :attrs => ["bold"] },
24
+ :twiddle => { :fg => "blue", :bg => "default" },
25
+ :label => { :fg => "yellow", :bg => "default" },
26
26
  :message_patina => { :fg => "black", :bg => "green" },
27
27
  :alternate_patina => { :fg => "black", :bg => "blue" },
28
28
  :missing_message => { :fg => "black", :bg => "red" },
29
- :attachment => { :fg => "cyan", :bg => "black" },
30
- :cryptosig_valid => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
31
- :cryptosig_unknown => { :fg => "cyan", :bg => "black" },
29
+ :attachment => { :fg => "cyan", :bg => "default" },
30
+ :cryptosig_valid => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
31
+ :cryptosig_unknown => { :fg => "cyan", :bg => "default" },
32
32
  :cryptosig_invalid => { :fg => "yellow", :bg => "red", :attrs => ["bold"] },
33
- :generic_notice_patina => { :fg => "cyan", :bg => "black" },
34
- :quote_patina => { :fg => "yellow", :bg => "black" },
35
- :sig_patina => { :fg => "yellow", :bg => "black" },
36
- :quote => { :fg => "yellow", :bg => "black" },
37
- :sig => { :fg => "yellow", :bg => "black" },
38
- :to_me => { :fg => "green", :bg => "black" },
39
- :starred => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
33
+ :generic_notice_patina => { :fg => "cyan", :bg => "default" },
34
+ :quote_patina => { :fg => "yellow", :bg => "default" },
35
+ :sig_patina => { :fg => "yellow", :bg => "default" },
36
+ :quote => { :fg => "yellow", :bg => "default" },
37
+ :sig => { :fg => "yellow", :bg => "default" },
38
+ :to_me => { :fg => "green", :bg => "default" },
39
+ :starred => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
40
40
  :starred_patina => { :fg => "yellow", :bg => "green", :attrs => ["bold"] },
41
41
  :alternate_starred_patina => { :fg => "yellow", :bg => "blue", :attrs => ["bold"] },
42
- :snippet => { :fg => "cyan", :bg => "black" },
43
- :option => { :fg => "white", :bg => "black" },
44
- :tagged => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
45
- :draft_notification => { :fg => "red", :bg => "black", :attrs => ["bold"] },
46
- :completion_character => { :fg => "white", :bg => "black", :attrs => ["bold"] },
47
- :horizontal_selector_selected => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
48
- :horizontal_selector_unselected => { :fg => "cyan", :bg => "black" },
49
- :search_highlight => { :fg => "black", :bg => "yellow", :attrs => ["bold"] }
42
+ :snippet => { :fg => "cyan", :bg => "default" },
43
+ :option => { :fg => "white", :bg => "default" },
44
+ :tagged => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
45
+ :draft_notification => { :fg => "red", :bg => "default", :attrs => ["bold"] },
46
+ :completion_character => { :fg => "white", :bg => "default", :attrs => ["bold"] },
47
+ :horizontal_selector_selected => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
48
+ :horizontal_selector_unselected => { :fg => "cyan", :bg => "default" },
49
+ :search_highlight => { :fg => "black", :bg => "yellow", :attrs => ["bold"] },
50
+ :system_buf => { :fg => "blue", :bg => "default" },
51
+ :regular_buf => { :fg => "white", :bg => "default" },
52
+ :modified_buffer => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
50
53
  }
51
54
 
52
55
  def initialize
@@ -17,7 +17,7 @@ class ContactManager
17
17
  IO.foreach(fn) do |l|
18
18
  l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
19
19
  aalias, addr = $1, $2
20
- p = PersonManager.person_for addr, :definitive => true
20
+ p = Person.from_address addr
21
21
  @p2a[p] = aalias
22
22
  @a2p[aalias] = p unless aalias.nil? || aalias.empty?
23
23
  end
@@ -79,9 +79,7 @@ class DraftLoader < Source
79
79
  def fn_for_offset o; File.join(@dir, o.to_s); end
80
80
 
81
81
  def load_header offset
82
- File.open fn_for_offset(offset) do |f|
83
- return MBox::read_header(f)
84
- end
82
+ File.open(fn_for_offset(offset)) { |f| parse_raw_email_header f }
85
83
  end
86
84
 
87
85
  def load_message offset
@@ -93,7 +93,7 @@ class IMAP < Source
93
93
  def == o; o.is_a?(IMAP) && o.uri == self.uri && o.username == self.username; end
94
94
 
95
95
  def load_header id
96
- MBox::read_header StringIO.new(raw_header(id))
96
+ parse_raw_email_header StringIO.new(raw_header(id))
97
97
  end
98
98
 
99
99
  def load_message id
@@ -177,31 +177,31 @@ EOS
177
177
  end
178
178
  end
179
179
 
180
- ## Syncs the message to the index: deleting if it's already there,
181
- ## and adding either way. Index state will be determined by m.labels.
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.
182
183
  ##
183
- ## docid and entry can be specified if they're already known.
184
- def sync_message m, docid=nil, entry=nil, opts={}
185
- docid, entry = load_entry_for_id m.id unless docid && entry
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
186
188
 
187
189
  raise "no source info for message #{m.id}" unless m.source && m.source_info
188
190
  @index_mutex.synchronize do
189
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
190
192
  end
191
193
 
192
- source_id =
193
- if m.source.is_a? Integer
194
- m.source
195
- else
196
- m.source.id or raise "unregistered source #{m.source} (id #{m.source.id.inspect})"
197
- end
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
198
199
 
199
- snippet =
200
- if m.snippet_contains_encrypted_content? && $config[:discard_snippets_from_encrypted_messages]
201
- ""
202
- else
203
- m.snippet
204
- end
200
+ snippet = if m.snippet_contains_encrypted_content? && $config[:discard_snippets_from_encrypted_messages]
201
+ ""
202
+ else
203
+ m.snippet
204
+ end
205
205
 
206
206
  ## write the new document to the index. if the entry already exists in the
207
207
  ## index, reuse it (which avoids having to reload the entry from the source,
@@ -226,7 +226,7 @@ EOS
226
226
  ## but merge in the labels.
227
227
  if entry[:source_id] && entry[:source_info] && entry[:label] &&
228
228
  ((entry[:source_id].to_i > source_id) || (entry[:source_info].to_i < m.source_info))
229
- labels = (entry[:label].split(/\s+/).map { |l| l.intern } + m.labels).uniq
229
+ labels = (entry[:label].symbolistize + m.labels).uniq
230
230
  #Redwood::log "found updated version of message #{m.id}: #{m.subj}"
231
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
232
  #Redwood::log "merged labels are #{labels.inspect} (index #{entry[:label].inspect}, message #{m.labels.inspect})"
@@ -246,21 +246,29 @@ EOS
246
246
  :snippet => snippet, # always override
247
247
  :label => labels.uniq.join(" "),
248
248
  :attachments => (entry[:attachments] || m.attachments.uniq.join(" ")),
249
- :from => (entry[:from] || (m.from ? m.from.indexable_content : "")),
250
- :to => (entry[:to] || (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.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
+
251
260
  :subject => (entry[:subject] || wrap_subj(Message.normalize_subj(m.subj))),
252
261
  :refs => (entry[:refs] || (m.refs + m.replytos).uniq.join(" ")),
253
262
  }
254
263
 
255
- @index_mutex.synchronize do
264
+ @index_mutex.synchronize do
256
265
  @index.delete docid if docid
257
266
  @index.add_document d
258
267
  end
259
268
 
260
- docid, entry = load_entry_for_id m.id
261
- ## this hasn't been triggered in a long time. TODO: decide whether it's still a problem.
262
- raise "just added message #{m.id.inspect} but couldn't find it in a search" unless docid
263
- true
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
264
272
  end
265
273
 
266
274
  def save_index fn=File.join(@dir, "ferret")
@@ -324,7 +332,7 @@ EOS
324
332
 
325
333
  q = Ferret::Search::BooleanQuery.new true
326
334
  sq = Ferret::Search::PhraseQuery.new(:subject)
327
- wrap_subj(Message.normalize_subj(m.subj)).split(/\s+/).each do |t|
335
+ wrap_subj(Message.normalize_subj(m.subj)).split.each do |t|
328
336
  sq.add_term t
329
337
  end
330
338
  q.add_query sq, :must
@@ -369,7 +377,7 @@ EOS
369
377
  unless messages.member?(mid)
370
378
  #Redwood::log "got #{mid} as a child of #{id}"
371
379
  messages[mid] ||= lambda { build_message docid }
372
- refs = @index[docid][:refs].split(" ")
380
+ refs = @index[docid][:refs].split
373
381
  pending += refs.select { |id| !searched[id] }
374
382
  end
375
383
  end
@@ -380,7 +388,7 @@ EOS
380
388
  Redwood::log "thread for #{m.id} is killed, ignoring"
381
389
  false
382
390
  else
383
- Redwood::log "ran #{num_queries} queries to build thread of #{messages.size + 1} messages for #{m.id}: #{m.subj}" if num_queries > 0
391
+ Redwood::log "ran #{num_queries} queries to build thread of #{messages.size} messages for #{m.id}: #{m.subj}" if num_queries > 0
384
392
  messages.each { |mid, builder| yield mid, builder }
385
393
  true
386
394
  end
@@ -400,14 +408,16 @@ EOS
400
408
  "date" => Time.at(doc[:date].to_i),
401
409
  "subject" => unwrap_subj(doc[:subject]),
402
410
  "from" => doc[:from],
403
- "to" => doc[:to].split(/\s+/).join(", "), # reformat
411
+ "to" => doc[:to].split.join(", "), # reformat
404
412
  "message-id" => doc[:message_id],
405
- "references" => doc[:refs].split(/\s+/).map { |x| "<#{x}>" }.join(" "),
413
+ "references" => doc[:refs].split.map { |x| "<#{x}>" }.join(" "),
406
414
  }
407
415
 
408
- Message.new :source => source, :source_info => doc[:source_info].to_i,
409
- :labels => doc[:label].split(" ").map { |s| s.intern },
410
- :snippet => doc[:snippet], :header => fake_header
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
411
421
  end
412
422
  end
413
423
 
@@ -449,9 +459,9 @@ EOS
449
459
  t = @index[docid][:to]
450
460
 
451
461
  if AccountManager.is_account_email? f
452
- t.split(" ").each { |e| contacts[PersonManager.person_for(e)] = true }
462
+ t.split(" ").each { |e| contacts[Person.from_address(e)] = true }
453
463
  else
454
- contacts[PersonManager.person_for(f)] = true
464
+ contacts[Person.from_address(f)] = true
455
465
  end
456
466
  end
457
467
  end
@@ -474,10 +484,27 @@ EOS
474
484
  @index_mutex.synchronize { @index.search(q, :limit => 1).total_hits > 0 }
475
485
  end
476
486
 
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 }
498
+ end
499
+
477
500
  protected
478
501
 
479
- ## do any specialized parsing
480
- ## returns nil and flashes error message if parsing failed
502
+ class ParseError < StandardError; end
503
+
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.
506
+ ##
507
+ ## raises a ParseError if something went wrong.
481
508
  def parse_user_query_string s
482
509
  extraopts = {}
483
510
 
@@ -539,11 +566,9 @@ protected
539
566
  end
540
567
 
541
568
  if $have_chronic
542
- chronic_failure = false
543
569
  subs = subs.gsub(/\b(before|on|in|during|after):(\((.+?)\)\B|(\S+)\b)/) do
544
- break if chronic_failure
545
570
  field, datestr = $1, ($3 || $4)
546
- realdate = Chronic.parse(datestr, :guess => false, :context => :none)
571
+ realdate = Chronic.parse datestr, :guess => false, :context => :past
547
572
  if realdate
548
573
  case field
549
574
  when "after"
@@ -557,11 +582,9 @@ protected
557
582
  "date:(<= #{sprintf "%012d", realdate.end.to_i}) date:(>= #{sprintf "%012d", realdate.begin.to_i})"
558
583
  end
559
584
  else
560
- BufferManager.flash "Can't understand date #{datestr.inspect}!"
561
- chronic_failure = true
585
+ raise ParseError, "can't understand date #{datestr.inspect}"
562
586
  end
563
587
  end
564
- subs = nil if chronic_failure
565
588
  end
566
589
 
567
590
  ## limit:42 restrict the search to 42 results
@@ -571,15 +594,14 @@ protected
571
594
  extraopts[:limit] = lim.to_i
572
595
  ''
573
596
  else
574
- BufferManager.flash "Can't understand limit #{lim.inspect}!"
575
- subs = nil
597
+ raise ParseError, "non-numeric limit #{lim.inspect}"
576
598
  end
577
599
  end
578
600
 
579
- if subs
601
+ begin
580
602
  [@qparser.parse(subs), extraopts]
581
- else
582
- nil
603
+ rescue Ferret::QueryParser::QueryParseException => e
604
+ raise ParseError, e.message
583
605
  end
584
606
  end
585
607