sup 0.6 → 0.7

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.

data/CONTRIBUTORS CHANGED
@@ -1,13 +1,17 @@
1
1
  William Morgan <wmorgan-sup at the masanjin dot nets>
2
2
  Ismo Puustinen <ismo at the iki dot fis>
3
+ Nicolas Pouillard <nicolas.pouillard at the gmail dot coms>
3
4
  Marcus Williams <marcus-sup at the bar-coded dot nets>
4
5
  Lionel Ott <white.magic at the gmx dot des>
5
- Nicolas Pouillard <nicolas.pouillard at the gmail dot coms>
6
+ Mark Alexander <marka at the pobox dot coms>
7
+ Christopher Warrington <chrisw at the rice dot edus>
6
8
  Marc Hartstein <marc.hartstein at the alum.vassar dot edus>
7
9
  Ben Walton <bwalton at the artsci.utoronto dot cas>
8
10
  Grant Hollingworth <grant at the antiflux dot orgs>
11
+ Steve Goldman <sgoldman at the tower-research dot coms>
12
+ Decklin Foster <decklin at the red-bean dot coms>
9
13
  Jeff Balogh <its.jeff.balogh at the gmail dot coms>
10
- Christopher Warrington <chrisw at the rice dot edus>
11
14
  Giorgio Lando <patroclo7 at the gmail dot coms>
12
- Decklin Foster <decklin at the red-bean dot coms>
15
+ Israel Herraiz <israel.herraiz at the gmail dot coms>
13
16
  Ian Taylor <ian at the lorf dot orgs>
17
+ Rich Lane <rlane at the club.cc.cmu dot edus>
data/History.txt CHANGED
@@ -1,3 +1,15 @@
1
+ == 0.7 / 2009-03-16
2
+ * Ferret index corruption issues fixed (hopefully!)
3
+ * Text entry now scrolls to the right on overflow, i.e. is actually usable
4
+ * Ctrl-C now asks user if Sup should die ungracefully
5
+ * Add a limit:<int> search operator to limit the number of results
6
+ * Added a --query option to sup-tweak-labels
7
+ * Added a new hook: shutdown
8
+ * Automatically add self as recipient on crypted sent messages
9
+ * Read in X-Foo headers
10
+ * Added global keybinding 'U' shows only unread messages
11
+ * As always, many bugfixes and tweaks
12
+
1
13
  == 0.6 / 2008-08-04
2
14
  * new hooks: mark-as-spam, reply-to, reply-from
3
15
  * configurable colors. finally!
data/README.txt CHANGED
@@ -108,7 +108,7 @@ Current limitations which will be fixed:
108
108
 
109
109
  == INSTALL:
110
110
 
111
- * gem install sup -y
111
+ * gem install sup
112
112
 
113
113
  == PROBLEMS:
114
114
 
@@ -116,7 +116,7 @@ See FAQ.txt for some common problems and their solutions.
116
116
 
117
117
  == LICENSE:
118
118
 
119
- Copyright (c) 2006, 2007 William Morgan.
119
+ Copyright (c) 2006--2009 William Morgan.
120
120
 
121
121
  This program is free software; you can redistribute it and/or
122
122
  modify it under the terms of the GNU General Public License
data/ReleaseNotes CHANGED
@@ -1,3 +1,14 @@
1
+ Release 0.7:
2
+
3
+ The big win in this release is that Ferret index corruption issues should now
4
+ be fixed, thanks to an extensive programming of locking and
5
+ thread-safety-adding.
6
+
7
+ The other nice change is that text entry will now scroll to the right upon
8
+ overflow, thanks to some arcane Curses magic that Steve Goldman discovered.
9
+
10
+ As always, this release includes many other bugfixes and enhancements.
11
+
1
12
  Release 0.6:
2
13
 
3
14
  Message attachment searchability automatically takes effect on new messages,
data/bin/sup CHANGED
@@ -8,7 +8,7 @@ require 'trollop'
8
8
  require 'fastthread'
9
9
  require "sup"
10
10
 
11
- BIN_VERSION = "0.6"
11
+ BIN_VERSION = "0.7"
12
12
 
13
13
  unless Redwood::VERSION == BIN_VERSION
14
14
  $stderr.puts <<EOS
@@ -45,6 +45,14 @@ No variables.
45
45
  No return value.
46
46
  EOS
47
47
 
48
+ Redwood::HookManager.register "shutdown", <<EOS
49
+ Executes when sup is shutting down. May be run when sup is crashing,
50
+ so don\'t do anything too important. Run before the label, contacts,
51
+ and people are saved.
52
+ No variables.
53
+ No return value.
54
+ EOS
55
+
48
56
  if $opts[:list_hooks]
49
57
  Redwood::HookManager.print_hooks
50
58
  exit
@@ -57,7 +65,7 @@ module Redwood
57
65
  global_keymap = Keymap.new do |k|
58
66
  k.add :quit_ask, "Quit Sup, but ask first", 'q'
59
67
  k.add :quit_now, "Quit Sup immediately", 'Q'
60
- k.add :help, "Show help", 'H', '?'
68
+ k.add :help, "Show help", '?'
61
69
  k.add :roll_buffers, "Switch to next buffer", 'b'
62
70
  # k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
63
71
  k.add :kill_buffer, "Kill the current buffer", 'x'
@@ -65,6 +73,7 @@ global_keymap = Keymap.new do |k|
65
73
  k.add :list_contacts, "List contacts", 'C'
66
74
  k.add :redraw, "Redraw screen", :ctrl_l
67
75
  k.add :search, "Search all messages", '\\', 'F'
76
+ k.add :search_unread, "Show all unread messages", 'U'
68
77
  k.add :list_labels, "List labels", 'L'
69
78
  k.add :poll, "Poll for new messages", 'P'
70
79
  k.add :compose, "Compose new message", 'm', 'c'
@@ -101,7 +110,7 @@ rescue Index::LockError => e
101
110
  h.say Index.fancy_lock_error_message_for(e)
102
111
 
103
112
  case h.ask("Should I ask that process to kill itself? ")
104
- when /^\s*y\s*$/i
113
+ when /^\s*y(es)?\s*$/i
105
114
  h.say "Ok, suggesting seppuku..."
106
115
  FileUtils.touch Redwood::SUICIDE_FN
107
116
  sleep SuicideManager::DELAY * 2
@@ -180,7 +189,16 @@ begin
180
189
  end
181
190
 
182
191
  until Redwood::exceptions.nonempty? || SuicideManager.die?
183
- c = Ncurses.nonblocking_getch
192
+ c =
193
+ begin
194
+ Ncurses.nonblocking_getch
195
+ rescue Exception => e
196
+ if e.is_a?(Interrupt)
197
+ raise if BufferManager.ask_yes_or_no("Die ungracefully now?")
198
+ bm.draw_screen
199
+ nil
200
+ end
201
+ end
184
202
  next unless c
185
203
  bm.erase_flash
186
204
 
@@ -194,7 +212,6 @@ begin
194
212
  rescue InputSequenceAborted
195
213
  :nothing
196
214
  end
197
-
198
215
  case action
199
216
  when :quit_now
200
217
  break if bm.kill_all_buffers_safely
@@ -220,6 +237,8 @@ begin
220
237
  query = BufferManager.ask :search, "search all messages: "
221
238
  next unless query && query !~ /^\s*$/
222
239
  SearchResultsMode.spawn_from_query query
240
+ when :search_unread
241
+ SearchResultsMode.spawn_from_query "is:unread"
223
242
  when :list_labels
224
243
  labels = LabelManager.listable_labels.map { |l| LabelManager.string_for l }
225
244
  user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
@@ -268,6 +287,8 @@ ensure
268
287
  Index.stop_lock_update_thread
269
288
  end
270
289
 
290
+ HookManager.run "shutdown"
291
+
271
292
  Redwood::finish
272
293
  stop_cursing
273
294
  Redwood::log "stopped cursing"
data/bin/sup-tweak-labels CHANGED
@@ -39,6 +39,7 @@ Options:
39
39
  EOS
40
40
  opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :type => String
41
41
  opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :type => String
42
+ opt :query, "A Sup search query", :type => String
42
43
 
43
44
  text <<EOS
44
45
 
@@ -76,6 +77,10 @@ begin
76
77
  ## query to only messages with those labels
77
78
  query += " +(" + remove_labels.map { |l| "label:#{l}" }.join(" ") + ")"
78
79
  end
80
+ query += ' ' + opts[:query] if opts[:query]
81
+
82
+ qobj, opts = Redwood::Index.parse_user_query_string query
83
+ query = Redwood::Index.build_query opts.merge(:qobj => qobj)
79
84
 
80
85
  results = index.ferret.search query, :limit => :all
81
86
  num_total = results.total_hits
data/lib/sup.rb CHANGED
@@ -46,7 +46,7 @@ class Module
46
46
  end
47
47
 
48
48
  module Redwood
49
- VERSION = "0.6"
49
+ VERSION = "0.7"
50
50
 
51
51
  BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
52
52
  CONFIG_FN = File.join(BASE_DIR, "config.yaml")
@@ -221,6 +221,7 @@ else
221
221
  :confirm_no_attachments => true,
222
222
  :confirm_top_posting => true,
223
223
  :discard_snippets_from_encrypted_messages => false,
224
+ :default_attachment_save_dir => "",
224
225
  }
225
226
  begin
226
227
  FileUtils.mkdir_p Redwood::BASE_DIR
data/lib/sup/buffer.rb CHANGED
@@ -433,7 +433,7 @@ EOS
433
433
  prefix, target = partial.split_on_commas_with_remainder
434
434
  target ||= prefix.pop || ""
435
435
  prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
436
- completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
436
+ completions.select { |x| x =~ /^#{Regexp::escape target}/i }.sort_by { |c| [ContactManager.contact_for(c) ? 0 : 1, c] }.map { |x| [prefix + x, x] }
437
437
  end
438
438
  end
439
439
 
@@ -501,12 +501,12 @@ EOS
501
501
  recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
502
502
  contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
503
503
 
504
- completions = (recent + contacts).flatten.uniq.sort
504
+ completions = (recent + contacts).flatten.uniq
505
505
  completions += HookManager.run("extra-contact-addresses") || []
506
506
  answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
507
507
 
508
508
  if answer
509
- answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
509
+ answer.split_on_commas.map { |x| ContactManager.contact_for(x) || PersonManager.person_for(x) }
510
510
  end
511
511
  end
512
512
 
data/lib/sup/crypto.rb CHANGED
@@ -53,7 +53,7 @@ class CryptoManager
53
53
  payload_fn.write format_payload(payload)
54
54
  payload_fn.close
55
55
 
56
- recipient_opts = to.map { |r| "--recipient '<#{r}>'" }.join(" ")
56
+ recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ")
57
57
  sign_opts = sign ? "--sign --local-user '#{from}'" : ""
58
58
  gpg_output = run_gpg "--output - --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}"
59
59
  raise Error, (gpg_output || "gpg command failed: #{cmd}") unless $?.success?
data/lib/sup/draft.rb CHANGED
@@ -32,7 +32,10 @@ class DraftManager
32
32
 
33
33
  def discard m
34
34
  docid, entry = Index.load_entry_for_id m.id
35
- raise ArgumentError, "can't find entry for draft: #{m.id.inspect}" unless entry
35
+ unless entry
36
+ Redwood::log "can't find entry for draft: #{m.id.inspect}. You probably already discarded it."
37
+ return
38
+ end
36
39
  raise ArgumentError, "not a draft: source id #{entry[:source_id].inspect}, should be #{DraftManager.source_id.inspect} for #{m.id.inspect} / docno #{docid}" unless entry[:source_id].to_i == DraftManager.source_id
37
40
  Index.drop_entry docid
38
41
  File.delete @source.fn_for_offset(entry[:source_info])
data/lib/sup/hook.rb CHANGED
@@ -52,6 +52,14 @@ class HookManager
52
52
  end
53
53
  end
54
54
 
55
+ def get tag
56
+ HookManager.tags[tag]
57
+ end
58
+
59
+ def set tag, value
60
+ HookManager.tags[tag] = value
61
+ end
62
+
55
63
  def __binding
56
64
  binding
57
65
  end
@@ -68,12 +76,15 @@ class HookManager
68
76
  @hooks = {}
69
77
  @descs = {}
70
78
  @contexts = {}
71
-
79
+ @tags = {}
80
+
72
81
  Dir.mkdir dir unless File.exists? dir
73
82
 
74
83
  self.class.i_am_the_instance self
75
84
  end
76
85
 
86
+ attr_reader :tags
87
+
77
88
  def run name, locals={}
78
89
  hook = hook_for(name) or return
79
90
  context = @contexts[hook] ||= HookContext.new(name)
data/lib/sup/imap.rb CHANGED
@@ -5,6 +5,9 @@ require 'time'
5
5
  require 'rmail'
6
6
  require 'cgi'
7
7
 
8
+ ## TODO: remove synchronized method protector calls; use a Monitor instead
9
+ ## (ruby's reentrant mutex)
10
+
8
11
  ## fucking imap fucking sucks. what the FUCK kind of committee of dunces
9
12
  ## designed this shit.
10
13
  ##
@@ -15,7 +18,7 @@ require 'cgi'
15
18
  ## restriction. it can change any time you log in. it can change EVERY
16
19
  ## time you log in. of course the imap spec "strongly recommends" that it
17
20
  ## never change, but there's nothing to stop people from just setting it
18
- ## to the current timestamp, and in fact that's exactly what the one imap
21
+ ## to the current timestamp, and in fact that's EXACTLY what the one imap
19
22
  ## server i have at my disposal does. thus the so-called uids are
20
23
  ## absolutely useless and imap provides no cross-session way of uniquely
21
24
  ## identifying a message. but thanks for the "strong recommendation",
@@ -114,20 +117,37 @@ class IMAP < Source
114
117
  end
115
118
  synchronized :raw_message
116
119
 
120
+ def mark_as_deleted ids
121
+ ids = [ids].flatten # accept single arguments
122
+ unsynchronized_scan_mailbox
123
+ imap_ids = ids.map { |i| @imap_state[i] && @imap_state[i][:id] }.compact
124
+ return if imap_ids.empty?
125
+ @imap.store imap_ids, "+FLAGS", [:Deleted]
126
+ end
127
+ synchronized :mark_as_deleted
128
+
129
+ def expunge
130
+ @imap.expunge
131
+ unsynchronized_scan_mailbox true
132
+ true
133
+ end
134
+ synchronized :expunge
135
+
117
136
  def connect
118
137
  return if @imap
119
138
  safely { } # do nothing!
120
139
  end
121
140
  synchronized :connect
122
141
 
123
- def scan_mailbox
124
- return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
142
+ def scan_mailbox force=false
143
+ return if !force && @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
125
144
  last_id = safely do
126
145
  @imap.examine mailbox
127
146
  @imap.responses["EXISTS"].last
128
147
  end
129
148
  @last_scan = Time.now
130
149
 
150
+ @ids = [] if force
131
151
  return if last_id == @ids.length
132
152
 
133
153
  range = (@ids.length + 1) .. last_id
@@ -259,7 +279,7 @@ private
259
279
  %w(RFC822.SIZE INTERNALDATE).each do |w|
260
280
  raise FatalSourceError, "requested data not in IMAP response: #{w}" unless imap_stuff.attr[w]
261
281
  end
262
-
282
+
263
283
  msize, mdate = imap_stuff.attr['RFC822.SIZE'] % 10000000, Time.parse(imap_stuff.attr["INTERNALDATE"])
264
284
  sprintf("%d%07d", mdate.to_i, msize).to_i
265
285
  end
data/lib/sup/index.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'fileutils'
4
4
  require 'ferret'
5
+ require 'fastthread'
6
+
5
7
  begin
6
8
  require 'chronic'
7
9
  $have_chronic = true
@@ -23,12 +25,18 @@ class Index
23
25
 
24
26
  include Singleton
25
27
 
28
+ ## these two accessors should ONLY be used by single-threaded programs.
29
+ ## otherwise you will have a naughty ferret on your hands.
26
30
  attr_reader :index
27
31
  alias ferret index
32
+
28
33
  def initialize dir=BASE_DIR
34
+ @index_mutex = Monitor.new
35
+
29
36
  @dir = dir
30
37
  @sources = {}
31
38
  @sources_dirty = false
39
+ @source_mutex = Monitor.new
32
40
 
33
41
  wsa = Ferret::Analysis::WhiteSpaceAnalyzer.new false
34
42
  sa = Ferret::Analysis::StandardAnalyzer.new [], true
@@ -66,14 +74,19 @@ class Index
66
74
  @lock_update_thread = nil
67
75
  end
68
76
 
77
+ def possibly_pluralize number_of, kind
78
+ "#{number_of} #{kind}" +
79
+ if number_of == 1 then "" else "s" end
80
+ end
81
+
69
82
  def fancy_lock_error_message_for e
70
- secs = Time.now - e.mtime
71
- mins = secs.to_i / 60
83
+ secs = (Time.now - e.mtime).to_i
84
+ mins = secs / 60
72
85
  time =
73
86
  if mins == 0
74
- "#{secs.to_i} seconds"
87
+ possibly_pluralize secs , "second"
75
88
  else
76
- "#{mins} minutes"
89
+ possibly_pluralize mins, "minute"
77
90
  end
78
91
 
79
92
  <<EOS
@@ -117,17 +130,19 @@ EOS
117
130
  end
118
131
 
119
132
  def add_source source
120
- raise "duplicate source!" if @sources.include? source
121
- @sources_dirty = true
122
- max = @sources.max_of { |id, s| s.is_a?(DraftLoader) || s.is_a?(SentLoader) ? 0 : id }
123
- source.id ||= (max || 0) + 1
124
- ##source.id += 1 while @sources.member? source.id
125
- @sources[source.id] = 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
126
141
  end
127
142
 
128
143
  def sources
129
144
  ## favour the inbox by listing non-archived sources first
130
- @sources.values.sort_by { |s| s.id }.partition { |s| !s.archived? }.flatten
145
+ @source_mutex.synchronize { @sources.values }.sort_by { |s| s.id }.partition { |s| !s.archived? }.flatten
131
146
  end
132
147
 
133
148
  def source_for uri; sources.find { |s| s.is_source_for? uri }; end
@@ -136,25 +151,29 @@ EOS
136
151
  def load_index dir=File.join(@dir, "ferret")
137
152
  if File.exists? dir
138
153
  Redwood::log "loading index..."
139
- @index = Ferret::Index::Index.new(:path => dir, :analyzer => @analyzer)
140
- Redwood::log "loaded index of #{@index.size} messages"
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
141
158
  else
142
159
  Redwood::log "creating index..."
143
- field_infos = Ferret::Index::FieldInfos.new :store => :yes
144
- field_infos.add_field :message_id, :index => :untokenized
145
- field_infos.add_field :source_id
146
- field_infos.add_field :source_info
147
- field_infos.add_field :date, :index => :untokenized
148
- field_infos.add_field :body
149
- field_infos.add_field :label
150
- field_infos.add_field :attachments
151
- field_infos.add_field :subject
152
- field_infos.add_field :from
153
- field_infos.add_field :to
154
- field_infos.add_field :refs
155
- field_infos.add_field :snippet, :index => :no, :term_vector => :no
156
- field_infos.create_index dir
157
- @index = Ferret::Index::Index.new(:path => dir, :analyzer => @analyzer)
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
158
177
  end
159
178
  end
160
179
 
@@ -166,7 +185,9 @@ EOS
166
185
  docid, entry = load_entry_for_id m.id unless docid && entry
167
186
 
168
187
  raise "no source info for message #{m.id}" unless m.source && m.source_info
169
- 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
188
+ @index_mutex.synchronize do
189
+ 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
+ end
170
191
 
171
192
  source_id =
172
193
  if m.source.is_a? Integer
@@ -231,9 +252,11 @@ EOS
231
252
  :refs => (entry[:refs] || (m.refs + m.replytos).uniq.join(" ")),
232
253
  }
233
254
 
234
- @index.delete docid if docid
235
- @index.add_document d
236
-
255
+ @index_mutex.synchronize do
256
+ @index.delete docid if docid
257
+ @index.add_document d
258
+ end
259
+
237
260
  docid, entry = load_entry_for_id m.id
238
261
  ## this hasn't been triggered in a long time. TODO: decide whether it's still a problem.
239
262
  raise "just added message #{m.id.inspect} but couldn't find it in a search" unless docid
@@ -245,32 +268,37 @@ EOS
245
268
  end
246
269
 
247
270
  def contains_id? id
248
- @index.search(Ferret::Search::TermQuery.new(:message_id, id)).total_hits > 0
271
+ @index_mutex.synchronize { @index.search(Ferret::Search::TermQuery.new(:message_id, id)).total_hits > 0 }
249
272
  end
250
- def contains? m; contains_id? m.id; end
251
- def size; @index.size; end
273
+ def contains? m; contains_id? m.id end
274
+ def size; @index_mutex.synchronize { @index.size } end
275
+ def empty?; size == 0 end
252
276
 
253
277
  ## you should probably not call this on a block that doesn't break
254
278
  ## rather quickly because the results can be very large.
255
279
  EACH_BY_DATE_NUM = 100
256
280
  def each_id_by_date opts={}
257
- return if @index.size == 0 # otherwise ferret barfs ###TODO: remove this once my ferret patch is accepted
281
+ return if empty? # otherwise ferret barfs ###TODO: remove this once my ferret patch is accepted
258
282
  query = build_query opts
259
283
  offset = 0
260
284
  while true
261
- results = @index.search(query, :sort => "date DESC", :limit => EACH_BY_DATE_NUM, :offset => offset)
285
+ limit = (opts[:limit])? [EACH_BY_DATE_NUM, opts[:limit] - offset].min : EACH_BY_DATE_NUM
286
+ results = @index_mutex.synchronize { @index.search query, :sort => "date DESC", :limit => limit, :offset => offset }
262
287
  Redwood::log "got #{results.total_hits} results for query (offset #{offset}) #{query.inspect}"
263
- results.hits.each { |hit| yield @index[hit.doc][:message_id], lambda { build_message hit.doc } }
264
- break if offset >= results.total_hits - EACH_BY_DATE_NUM
265
- offset += EACH_BY_DATE_NUM
288
+ results.hits.each do |hit|
289
+ yield @index_mutex.synchronize { @index[hit.doc][:message_id] }, lambda { build_message hit.doc }
290
+ end
291
+ break if opts[:limit] and offset >= opts[:limit] - limit
292
+ break if offset >= results.total_hits - limit
293
+ offset += limit
266
294
  end
267
295
  end
268
296
 
269
297
  def num_results_for opts={}
270
- return 0 if @index.size == 0 # otherwise ferret barfs ###TODO: remove this once my ferret patch is accepted
298
+ return 0 if empty? # otherwise ferret barfs ###TODO: remove this once my ferret patch is accepted
271
299
 
272
300
  q = build_query opts
273
- index.search(q, :limit => 1).total_hits
301
+ @index_mutex.synchronize { @index.search(q, :limit => 1).total_hits }
274
302
  end
275
303
 
276
304
  ## yield all messages in the thread containing 'm' by repeatedly
@@ -304,10 +332,10 @@ EOS
304
332
 
305
333
  q = build_query :qobj => q
306
334
 
307
- p1 = @index.search(q).hits.map { |hit| @index[hit.doc][:message_id] }
335
+ p1 = @index_mutex.synchronize { @index.search(q).hits.map { |hit| @index[hit.doc][:message_id] } }
308
336
  Redwood::log "found #{p1.size} results for subject query #{q}"
309
337
 
310
- p2 = @index.search(q.to_s, :limit => :all).hits.map { |hit| @index[hit.doc][:message_id] }
338
+ p2 = @index_mutex.synchronize { @index.search(q.to_s, :limit => :all).hits.map { |hit| @index[hit.doc][:message_id] } }
311
339
  Redwood::log "found #{p2.size} results in string form"
312
340
 
313
341
  pending = (pending + p1 + p2).uniq
@@ -330,18 +358,20 @@ EOS
330
358
 
331
359
  num_queries += 1
332
360
  killed = false
333
- @index.search_each(q, :limit => :all) do |docid, score|
334
- break if opts[:limit] && messages.size >= opts[:limit]
335
- if @index[docid][:label].split(/\s+/).include?("killed") && opts[:skip_killed]
336
- killed = true
337
- break
338
- end
339
- mid = @index[docid][:message_id]
340
- unless messages.member?(mid)
341
- #Redwood::log "got #{mid} as a child of #{id}"
342
- messages[mid] ||= lambda { build_message docid }
343
- refs = @index[docid][:refs].split(" ")
344
- pending += refs.select { |id| !searched[id] }
361
+ @index_mutex.synchronize do
362
+ @index.search_each(q, :limit => :all) do |docid, score|
363
+ break if opts[:limit] && messages.size >= opts[:limit]
364
+ if @index[docid][:label].split(/\s+/).include?("killed") && opts[:skip_killed]
365
+ killed = true
366
+ break
367
+ end
368
+ mid = @index[docid][:message_id]
369
+ unless messages.member?(mid)
370
+ #Redwood::log "got #{mid} as a child of #{id}"
371
+ messages[mid] ||= lambda { build_message docid }
372
+ refs = @index[docid][:refs].split(" ")
373
+ pending += refs.select { |id| !searched[id] }
374
+ end
345
375
  end
346
376
  end
347
377
  end
@@ -358,36 +388,44 @@ EOS
358
388
 
359
389
  ## builds a message object from a ferret result
360
390
  def build_message docid
361
- doc = @index[docid]
362
- source = @sources[doc[:source_id].to_i]
363
- #puts "building message #{doc[:message_id]} (#{source}##{doc[:source_info]})"
364
- raise "invalid source #{doc[:source_id]}" unless source
365
-
366
- fake_header = {
367
- "date" => Time.at(doc[:date].to_i),
368
- "subject" => unwrap_subj(doc[:subject]),
369
- "from" => doc[:from],
370
- "to" => doc[:to].split(/\s+/).join(", "), # reformat
371
- "message-id" => doc[:message_id],
372
- "references" => doc[:refs].split(/\s+/).map { |x| "<#{x}>" }.join(" "),
373
- }
374
-
375
- Message.new :source => source, :source_info => doc[:source_info].to_i,
376
- :labels => doc[:label].split(" ").map { |s| s.intern },
377
- :snippet => doc[:snippet], :header => fake_header
391
+ @index_mutex.synchronize do
392
+ doc = @index[docid]
393
+
394
+ source = @source_mutex.synchronize { @sources[doc[:source_id].to_i] }
395
+ raise "invalid source #{doc[:source_id]}" unless source
396
+
397
+ #puts "building message #{doc[:message_id]} (#{source}##{doc[:source_info]})"
398
+
399
+ fake_header = {
400
+ "date" => Time.at(doc[:date].to_i),
401
+ "subject" => unwrap_subj(doc[:subject]),
402
+ "from" => doc[:from],
403
+ "to" => doc[:to].split(/\s+/).join(", "), # reformat
404
+ "message-id" => doc[:message_id],
405
+ "references" => doc[:refs].split(/\s+/).map { |x| "<#{x}>" }.join(" "),
406
+ }
407
+
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
411
+ end
378
412
  end
379
413
 
380
414
  def fresh_thread_id; @next_thread_id += 1; end
381
415
  def wrap_subj subj; "__START_SUBJECT__ #{subj} __END_SUBJECT__"; end
382
416
  def unwrap_subj subj; subj =~ /__START_SUBJECT__ (.*?) __END_SUBJECT__/ && $1; end
383
417
 
384
- def drop_entry docno; @index.delete docno; end
418
+ def drop_entry docno; @index_mutex.synchronize { @index.delete docno } end
385
419
 
386
420
  def load_entry_for_id mid
387
- results = @index.search(Ferret::Search::TermQuery.new(:message_id, mid))
388
- return if results.total_hits == 0
389
- docid = results.hits[0].doc
390
- [docid, @index[docid]]
421
+ @index_mutex.synchronize do
422
+ results = @index.search Ferret::Search::TermQuery.new(:message_id, mid)
423
+ return if results.total_hits == 0
424
+ docid = results.hits[0].doc
425
+ entry = @index[docid]
426
+ entry_dup = entry.fields.inject({}) { |h, f| h[f] = entry[f]; h }
427
+ [docid, entry_dup]
428
+ end
391
429
  end
392
430
 
393
431
  def load_contacts emails, h={}
@@ -403,16 +441,18 @@ EOS
403
441
  Redwood::log "contact search: #{q}"
404
442
  contacts = {}
405
443
  num = h[:num] || 20
406
- @index.search_each(q, :sort => "date DESC", :limit => :all) do |docid, score|
407
- break if contacts.size >= num
408
- #Redwood::log "got message #{docid} to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
409
- f = @index[docid][:from]
410
- t = @index[docid][:to]
411
-
412
- if AccountManager.is_account_email? f
413
- t.split(" ").each { |e| contacts[PersonManager.person_for(e)] = true }
414
- else
415
- contacts[PersonManager.person_for(f)] = true
444
+ @index_mutex.synchronize do
445
+ @index.search_each q, :sort => "date DESC", :limit => :all do |docid, score|
446
+ break if contacts.size >= num
447
+ #Redwood::log "got message #{docid} to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
448
+ f = @index[docid][:from]
449
+ t = @index[docid][:to]
450
+
451
+ if AccountManager.is_account_email? f
452
+ t.split(" ").each { |e| contacts[PersonManager.person_for(e)] = true }
453
+ else
454
+ contacts[PersonManager.person_for(f)] = true
455
+ end
416
456
  end
417
457
  end
418
458
 
@@ -421,15 +461,17 @@ EOS
421
461
 
422
462
  def load_sources fn=Redwood::SOURCE_FN
423
463
  source_array = (Redwood::load_yaml_obj(fn) || []).map { |o| Recoverable.new o }
424
- @sources = Hash[*(source_array).map { |s| [s.id, s] }.flatten]
425
- @sources_dirty = false
464
+ @source_mutex.synchronize do
465
+ @sources = Hash[*(source_array).map { |s| [s.id, s] }.flatten]
466
+ @sources_dirty = false
467
+ end
426
468
  end
427
469
 
428
470
  def has_any_from_source_with_label? source, label
429
471
  q = Ferret::Search::BooleanQuery.new
430
472
  q.add_query Ferret::Search::TermQuery.new("source_id", source.id.to_s), :must
431
473
  q.add_query Ferret::Search::TermQuery.new("label", label.to_s), :must
432
- index.search(q, :limit => 1).total_hits > 0
474
+ @index_mutex.synchronize { @index.search(q, :limit => 1).total_hits > 0 }
433
475
  end
434
476
 
435
477
  protected
@@ -521,6 +563,18 @@ protected
521
563
  end
522
564
  subs = nil if chronic_failure
523
565
  end
566
+
567
+ ## limit:42 restrict the search to 42 results
568
+ subs = subs.gsub(/\blimit:(\S+)\b/) do
569
+ lim = $1
570
+ if lim =~ /^\d+$/
571
+ extraopts[:limit] = lim.to_i
572
+ ''
573
+ else
574
+ BufferManager.flash "Can't understand limit #{lim.inspect}!"
575
+ subs = nil
576
+ end
577
+ end
524
578
 
525
579
  if subs
526
580
  [@qparser.parse(subs), extraopts]
@@ -550,16 +604,18 @@ protected
550
604
  end
551
605
 
552
606
  def save_sources fn=Redwood::SOURCE_FN
553
- if @sources_dirty || @sources.any? { |id, s| s.dirty? }
554
- bakfn = fn + ".bak"
555
- if File.exists? fn
607
+ @source_mutex.synchronize do
608
+ if @sources_dirty || @sources.any? { |id, s| s.dirty? }
609
+ bakfn = fn + ".bak"
610
+ if File.exists? fn
611
+ File.chmod 0600, fn
612
+ FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(fn) == 0
613
+ end
614
+ Redwood::save_yaml_obj sources.sort_by { |s| s.id.to_i }, fn, true
556
615
  File.chmod 0600, fn
557
- FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(fn) == 0
558
616
  end
559
- Redwood::save_yaml_obj sources.sort_by { |s| s.id.to_i }, fn, true
560
- File.chmod 0600, fn
617
+ @sources_dirty = false
561
618
  end
562
- @sources_dirty = false
563
619
  end
564
620
  end
565
621