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.
- data/CONTRIBUTORS +13 -6
- data/History.txt +19 -0
- data/ReleaseNotes +35 -0
- data/bin/sup +82 -77
- data/bin/sup-add +7 -7
- data/bin/sup-config +104 -85
- data/bin/sup-dump +4 -5
- data/bin/sup-recover-sources +9 -10
- data/bin/sup-sync +121 -100
- data/bin/sup-sync-back +18 -15
- data/bin/sup-tweak-labels +24 -21
- data/lib/sup.rb +53 -33
- data/lib/sup/account.rb +0 -2
- data/lib/sup/buffer.rb +47 -22
- data/lib/sup/colormap.rb +6 -6
- data/lib/sup/contact.rb +0 -2
- data/lib/sup/crypto.rb +34 -23
- data/lib/sup/draft.rb +6 -14
- data/lib/sup/ferret_index.rb +471 -0
- data/lib/sup/hook.rb +30 -43
- data/lib/sup/hook.rb.BACKUP.8625.rb +158 -0
- data/lib/sup/hook.rb.BACKUP.8681.rb +158 -0
- data/lib/sup/hook.rb.BASE.8625.rb +155 -0
- data/lib/sup/hook.rb.BASE.8681.rb +155 -0
- data/lib/sup/hook.rb.LOCAL.8625.rb +142 -0
- data/lib/sup/hook.rb.LOCAL.8681.rb +142 -0
- data/lib/sup/hook.rb.REMOTE.8625.rb +145 -0
- data/lib/sup/hook.rb.REMOTE.8681.rb +145 -0
- data/lib/sup/imap.rb +18 -8
- data/lib/sup/index.rb +70 -528
- data/lib/sup/interactive-lock.rb +74 -0
- data/lib/sup/keymap.rb +26 -26
- data/lib/sup/label.rb +2 -4
- data/lib/sup/logger.rb +54 -35
- data/lib/sup/maildir.rb +41 -6
- data/lib/sup/mbox.rb +1 -1
- data/lib/sup/mbox/loader.rb +18 -6
- data/lib/sup/mbox/ssh-file.rb +1 -7
- data/lib/sup/message-chunks.rb +36 -23
- data/lib/sup/message.rb +126 -46
- data/lib/sup/mode.rb +3 -2
- data/lib/sup/modes/console-mode.rb +108 -0
- data/lib/sup/modes/edit-message-mode.rb +15 -5
- data/lib/sup/modes/inbox-mode.rb +2 -4
- data/lib/sup/modes/label-list-mode.rb +1 -1
- data/lib/sup/modes/line-cursor-mode.rb +18 -18
- data/lib/sup/modes/log-mode.rb +29 -16
- data/lib/sup/modes/poll-mode.rb +7 -9
- data/lib/sup/modes/reply-mode.rb +5 -3
- data/lib/sup/modes/scroll-mode.rb +2 -2
- data/lib/sup/modes/search-results-mode.rb +9 -11
- data/lib/sup/modes/text-mode.rb +2 -2
- data/lib/sup/modes/thread-index-mode.rb +26 -16
- data/lib/sup/modes/thread-view-mode.rb +84 -39
- data/lib/sup/person.rb +6 -8
- data/lib/sup/poll.rb +46 -47
- data/lib/sup/rfc2047.rb +1 -5
- data/lib/sup/sent.rb +27 -20
- data/lib/sup/source.rb +90 -13
- data/lib/sup/textfield.rb +4 -4
- data/lib/sup/thread.rb +15 -13
- data/lib/sup/undo.rb +0 -1
- data/lib/sup/update.rb +0 -1
- data/lib/sup/util.rb +51 -43
- data/lib/sup/xapian_index.rb +566 -0
- metadata +57 -46
- data/lib/sup/suicide.rb +0 -36
data/lib/sup/index.rb
CHANGED
@@ -1,20 +1,20 @@
|
|
1
|
-
##
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
133
|
-
|
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
|
149
|
-
def
|
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
|
152
|
-
|
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
|
-
|
181
|
-
|
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
|
275
|
-
# don't have to do anything, apparently
|
276
|
-
end
|
93
|
+
def contains? m; contains_id? m.id end
|
277
94
|
|
278
|
-
def
|
279
|
-
|
95
|
+
def size
|
96
|
+
unimplemented
|
280
97
|
end
|
281
|
-
|
282
|
-
def size; @index_mutex.synchronize { @index.size } end
|
98
|
+
|
283
99
|
def empty?; size == 0 end
|
284
100
|
|
285
|
-
##
|
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
|
-
|
288
|
-
|
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
|
-
|
306
|
-
|
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
|
-
|
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
|
-
##
|
398
|
-
def build_message
|
399
|
-
|
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
|
-
|
425
|
-
def
|
426
|
-
|
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
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
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
|
-
|
142
|
+
## Yield each message-id matching query
|
143
|
+
def each_id query={}
|
144
|
+
unimplemented
|
470
145
|
end
|
471
146
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
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
|
-
|
481
|
-
|
482
|
-
|
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
|
-
##
|
488
|
-
##
|
489
|
-
|
490
|
-
|
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
|
505
|
-
##
|
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
|
509
|
-
|
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
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
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
|