sup 0.10.2 → 0.11

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.

@@ -1,289 +0,0 @@
1
- # ncurses-ruby is a ruby module for accessing the FSF's ncurses library
2
- # (C) 2002, 2003 Tobias Peters <t-peters@users.berlios.de>
3
- # (C) 2004 Simon Kaczor <skaczor@cox.net>
4
- #
5
- # This module is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License as published by the Free Software Foundation; either
8
- # version 2 of the License, or (at your option) any later version.
9
- #
10
- # This module is distributed in the hope that it will be useful,
11
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
- # Lesser General Public License for more details.
14
- #
15
- # You should have received a copy of the GNU Lesser General Public
16
- # License along with this module; if not, write to the Free Software
17
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
-
19
- # $Id: ncurses.rb,v 1.5 2004/07/31 08:34:09 t-peters Exp $
20
-
21
- require "ncurses.so"
22
-
23
-
24
- # Ncurses constants with leading underscore
25
- def Ncurses._XOPEN_CURSES
26
- Ncurses::XOPEN_CURSES
27
- end
28
- def Ncurses._SUBWIN
29
- Ncurses::SUBWIN
30
- end
31
- def Ncurses._ENDLINE
32
- Ncurses::ENDLINE
33
- end
34
- def Ncurses._FULLWIN
35
- Ncurses::FULLWIN
36
- end
37
- def Ncurses._SCROLLWIN
38
- Ncurses::SCROLLWIN
39
- end
40
- def Ncurses._ISPAD
41
- Ncurses::ISPAD
42
- end
43
- def Ncurses._HASMOVED
44
- Ncurses::HASMOVED
45
- end
46
- def Ncurses._WRAPPED
47
- Ncurses::WRAPPED
48
- end
49
- def Ncurses._NOCHANGE
50
- Ncurses::NOCHANGE
51
- end
52
- def Ncurses._NEWINDEX
53
- Ncurses::NEWINDEX
54
- end
55
-
56
-
57
- module Ncurses
58
- module Destroy_checker; def destroyed?; @destroyed; end; end
59
- class WINDOW
60
- include Destroy_checker
61
- def method_missing(name, *args)
62
- name = name.to_s
63
- if (name[0,2] == "mv")
64
- test_name = name.dup
65
- test_name[2,0] = "w" # insert "w" after"mv"
66
- if (Ncurses.respond_to?(test_name))
67
- return Ncurses.send(test_name, self, *args)
68
- end
69
- end
70
- test_name = "w" + name
71
- if (Ncurses.respond_to?(test_name))
72
- return Ncurses.send(test_name, self, *args)
73
- end
74
- Ncurses.send(name, self, *args)
75
- end
76
- def respond_to?(name)
77
- name = name.to_s
78
- if (name[0,2] == "mv" && Ncurses.respond_to?("mvw" + name[2..-1]))
79
- return true
80
- end
81
- Ncurses.respond_to?("w" + name) || Ncurses.respond_to?(name)
82
- end
83
- def del
84
- Ncurses.delwin(self)
85
- end
86
- alias delete del
87
- def WINDOW.new(*args)
88
- Ncurses.newwin(*args)
89
- end
90
- end
91
- class SCREEN
92
- include Destroy_checker
93
- def del
94
- Ncurses.delscreen(self)
95
- end
96
- alias delete del
97
- end
98
- class MEVENT
99
- attr_accessor :id, :x,:y,:z, :bstate
100
- end
101
- GETSTR_LIMIT = 1024
102
-
103
- module Panel
104
- class PANEL; end
105
- end
106
-
107
- module Form
108
- class FORM
109
- attr_reader :user_object
110
-
111
- # This placeholder replaces the field_userptr function in curses
112
- def user_object=(obj)
113
- @user_object = obj
114
- end
115
- end
116
-
117
- class FIELD
118
- attr_reader :user_object
119
-
120
- # This placeholder replaces the field_userptr function in curses
121
- def user_object=(obj)
122
- @user_object = obj
123
- end
124
- end
125
-
126
- class FIELDTYPE
127
- end
128
- end
129
- end
130
- def Ncurses.inchnstr(str,n)
131
- Ncurses.winchnstr(Ncurses.stdscr, str, n)
132
- end
133
- def Ncurses.inchstr(str)
134
- Ncurses.winchstr(Ncurses.stdscr, str)
135
- end
136
- def Ncurses.mvinchnstr(y,x, str, n)
137
- Ncurses.mvwinchnstr(Ncurses.stdscr, y,x, str, n)
138
- end
139
- def Ncurses.mvinchstr(y,x, str)
140
- Ncurses.mvwinchstr(Ncurses.stdscr, y,x, str)
141
- end
142
- def Ncurses.mvwinchnstr(win, y,x, str, n)
143
- if (Ncurses.wmove(win,y,x) == Ncurses::ERR)
144
- Ncurses::ERR
145
- else
146
- Ncurses.winchnstr(win,str,n)
147
- end
148
- end
149
- def Ncurses.mvwinchstr(win, y,x, str)
150
- maxy = []; maxx = []; getmaxyx(win, maxy,maxx)
151
- return Ncurses::ERR if (maxx[0] == Ncurses::ERR)
152
- Ncurses.mvwinchnstr(win, y,x, str, maxx[0]+1)
153
- end
154
- def Ncurses.winchstr(win, str)
155
- maxy = []; maxx = []; getmaxyx(win, maxy,maxx)
156
- return Ncurses::ERR if (maxx[0] == Ncurses::ERR)
157
- Ncurses.winchnstr(win, str, maxx[0]+1)
158
- end
159
-
160
- def Ncurses.getnstr(str,n)
161
- Ncurses.wgetnstr(Ncurses.stdscr, str, n)
162
- end
163
- def Ncurses.mvgetnstr(y,x, str, n)
164
- Ncurses.mvwgetnstr(Ncurses.stdscr, y,x, str, n)
165
- end
166
- def Ncurses.mvwgetnstr(win, y,x, str, n)
167
- if (Ncurses.wmove(win,y,x) == Ncurses::ERR)
168
- Ncurses::ERR
169
- else
170
- Ncurses.wgetnstr(win,str,n)
171
- end
172
- end
173
-
174
- def Ncurses.innstr(str,n)
175
- Ncurses.winnstr(Ncurses.stdscr, str, n)
176
- end
177
- def Ncurses.instr(str)
178
- Ncurses.winstr(Ncurses.stdscr, str)
179
- end
180
- def Ncurses.mvinnstr(y,x, str, n)
181
- Ncurses.mvwinnstr(Ncurses.stdscr, y,x, str, n)
182
- end
183
- def Ncurses.mvinstr(y,x, str)
184
- Ncurses.mvwinstr(Ncurses.stdscr, y,x, str)
185
- end
186
- def Ncurses.mvwinnstr(win, y,x, str, n)
187
- if (Ncurses.wmove(win,y,x) == Ncurses::ERR)
188
- Ncurses::ERR
189
- else
190
- Ncurses.winnstr(win,str,n)
191
- end
192
- end
193
- def Ncurses.mvwinstr(win, y,x, str)
194
- maxy = []; maxx = []; getmaxyx(win, maxy,maxx)
195
- return Ncurses::ERR if (maxx[0] == Ncurses::ERR)
196
- Ncurses.mvwinnstr(win, y,x, str, maxx[0]+1)
197
- end
198
- def Ncurses.winstr(win, str)
199
- maxy = []; maxx = []; getmaxyx(win, maxy,maxx)
200
- return Ncurses::ERR if (maxx[0] == Ncurses::ERR)
201
- Ncurses.winnstr(win, str, maxx[0]+1)
202
- end
203
-
204
- def Ncurses.mouse_trafo(pY, pX, to_screen)
205
- Ncurses.wmouse_trafo(Ncurses.stdscr, pY, pX, to_screen)
206
- end
207
-
208
- def Ncurses.getcurx(win)
209
- x = []; y = []; Ncurses.getyx(win, y,x); x[0]
210
- end
211
- def Ncurses.getcury(win)
212
- x = []; y = []; Ncurses.getyx(win, y,x); y[0]
213
- end
214
- def Ncurses.getbegx(win)
215
- x = []; y = []; Ncurses.getbegyx(win, y,x); x[0]
216
- end
217
- def Ncurses.getbegy(win)
218
- x = []; y = []; Ncurses.getbegyx(win, y,x); y[0]
219
- end
220
- def Ncurses.getmaxx(win)
221
- x = []; y = []; Ncurses.getmaxyx(win, y,x); x[0]
222
- end
223
- def Ncurses.getmaxy(win)
224
- x = []; y = []; Ncurses.getmaxyx(win, y,x); y[0]
225
- end
226
- def Ncurses.getparx(win)
227
- x = []; y = []; Ncurses.getparyx(win, y,x); x[0]
228
- end
229
- def Ncurses.getpary(win)
230
- x = []; y = []; Ncurses.getparyx(win, y,x); y[0]
231
- end
232
- def Ncurses.erase
233
- Ncurses.werase(Ncurses.stdscr)
234
- end
235
- def Ncurses.getstr(str)
236
- Ncurses.getnstr(str, Ncurses::GETSTR_LIMIT)
237
- end
238
- def Ncurses.mvgetstr(y,x, str)
239
- Ncurses.mvgetnstr(y,x, str, Ncurses::GETSTR_LIMIT)
240
- end
241
- def Ncurses.mvwgetstr(win, y,x, str)
242
- Ncurses.mvwgetnstr(win, y,x, str, Ncurses::GETSTR_LIMIT)
243
- end
244
- def Ncurses.wgetstr(win, str)
245
- Ncurses.wgetnstr(win, str, Ncurses::GETSTR_LIMIT)
246
- end
247
-
248
- def Ncurses.scanw(format, result)
249
- Ncurses.wscanw(Ncurses.stdscr, format, result)
250
- end
251
- def Ncurses.mvscanw(y,x, format, result)
252
- Ncurses.mvwscanw(Ncurses.stdscr, y,x, format, result)
253
- end
254
- def Ncurses.mvwscanw(win, y,x, format, result)
255
- if (Ncurses.wmove(win, y,x) == Ncurses::ERR)
256
- Ncurses::ERR
257
- else
258
- Ncurses.wscanw(win, format, result)
259
- end
260
- end
261
- def Ncurses.wscanw(win, format, result)
262
- str = ""
263
- if (Ncurses.wgetstr(win, str) == Ncurses::ERR)
264
- Ncurses::ERR
265
- else
266
- require "scanf.rb" # Use ruby's implementation of scanf
267
- result.replace(str.scanf(format))
268
- end
269
- end
270
-
271
- def Ncurses.mvprintw(*args)
272
- Ncurses.mvwprintw(Ncurses.stdscr, *args)
273
- end
274
- def Ncurses.mvwprintw(win, y,x, *args)
275
- if (Ncurses.wmove(win,y,x) == Ncurses::ERR)
276
- Ncurses::ERR
277
- else
278
- wprintw(win, *args)
279
- end
280
- end
281
- def Ncurses.printw(*args)
282
- Ncurses.wprintw(Ncurses.stdscr, *args)
283
- end
284
- def Ncurses.touchline(win, start, count)
285
- Ncurses.wtouchln(win, start, count, 1)
286
- end
287
- def Ncurses.touchwin(win)
288
- wtouchln(win, 0, getmaxy(win), 1)
289
- end
@@ -1,476 +0,0 @@
1
- require 'ferret'
2
-
3
- module Redwood
4
-
5
- class FerretIndex < BaseIndex
6
-
7
- HookManager.register "custom-search", <<EOS
8
- Executes before a string search is applied to the index,
9
- returning a new search string.
10
- Variables:
11
- subs: The string being searched.
12
- EOS
13
-
14
- def is_a_deprecated_ferret_index?; true end
15
-
16
- def initialize dir=BASE_DIR
17
- super
18
-
19
- @index_mutex = Monitor.new
20
- wsa = Ferret::Analysis::WhiteSpaceAnalyzer.new false
21
- sa = Ferret::Analysis::StandardAnalyzer.new [], true
22
- @analyzer = Ferret::Analysis::PerFieldAnalyzer.new wsa
23
- @analyzer[:body] = sa
24
- @analyzer[:subject] = sa
25
- @qparser ||= Ferret::QueryParser.new :default_field => :body, :analyzer => @analyzer, :or_default => false
26
- end
27
-
28
- def load_index dir=File.join(@dir, "ferret")
29
- if File.exists? dir
30
- debug "loading index..."
31
- @index_mutex.synchronize do
32
- @index = Ferret::Index::Index.new(:path => dir, :analyzer => @analyzer, :id_field => 'message_id')
33
- debug "loaded index of #{@index.size} messages"
34
- end
35
- else
36
- debug "creating index..."
37
- @index_mutex.synchronize do
38
- field_infos = Ferret::Index::FieldInfos.new :store => :yes
39
- field_infos.add_field :message_id, :index => :untokenized
40
- field_infos.add_field :source_id
41
- field_infos.add_field :source_info
42
- field_infos.add_field :date, :index => :untokenized
43
- field_infos.add_field :body
44
- field_infos.add_field :label
45
- field_infos.add_field :attachments
46
- field_infos.add_field :subject
47
- field_infos.add_field :from
48
- field_infos.add_field :to
49
- field_infos.add_field :refs
50
- field_infos.add_field :snippet, :index => :no, :term_vector => :no
51
- field_infos.create_index dir
52
- @index = Ferret::Index::Index.new(:path => dir, :analyzer => @analyzer, :id_field => 'message_id')
53
- end
54
- end
55
- end
56
-
57
- def add_message m; sync_message m end
58
- def update_message m; sync_message m end
59
- def update_message_state m; sync_message m end
60
-
61
- def sync_message m, opts={}
62
- entry = @index[m.id]
63
-
64
- raise "no source info for message #{m.id}" unless m.source && m.source_info
65
-
66
- source_id = if m.source.is_a? Integer
67
- m.source
68
- else
69
- m.source.id or raise "unregistered source #{m.source} (id #{m.source.id.inspect})"
70
- end
71
-
72
- snippet = if m.snippet_contains_encrypted_content? && $config[:discard_snippets_from_encrypted_messages]
73
- ""
74
- else
75
- m.snippet
76
- end
77
-
78
- ## write the new document to the index. if the entry already exists in the
79
- ## index, reuse it (which avoids having to reload the entry from the source,
80
- ## which can be quite expensive for e.g. large threads of IMAP actions.)
81
- ##
82
- ## exception: if the index entry belongs to an earlier version of the
83
- ## message, use everything from the new message instead, but union the
84
- ## flags. this allows messages sent to mailing lists to have their header
85
- ## updated and to have flags set properly.
86
- ##
87
- ## minor hack: messages in sources with lower ids have priority over
88
- ## messages in sources with higher ids. so messages in the inbox will
89
- ## override everyone, and messages in the sent box will be overridden
90
- ## by everyone else.
91
- ##
92
- ## written in this manner to support previous versions of the index which
93
- ## did not keep around the entry body. upgrading is thus seamless.
94
- entry ||= {}
95
- labels = m.labels # override because this is the new state, unless...
96
-
97
- ## if we are a later version of a message, ignore what's in the index,
98
- ## but merge in the labels.
99
- if entry[:source_id] && entry[:source_info] && entry[:label] &&
100
- ((entry[:source_id].to_i > source_id) || (entry[:source_info].to_i < m.source_info))
101
- labels += entry[:label].to_set_of_symbols
102
- #debug "found updated version of message #{m.id}: #{m.subj}"
103
- #debug "previous version was at #{entry[:source_id].inspect}:#{entry[:source_info].inspect}, this version at #{source_id.inspect}:#{m.source_info.inspect}"
104
- #debug "merged labels are #{labels.inspect} (index #{entry[:label].inspect}, message #{m.labels.inspect})"
105
- entry = {}
106
- end
107
-
108
- ## if force_overwite is true, ignore what's in the index. this is used
109
- ## primarily by sup-sync to force index updates.
110
- entry = {} if opts[:force_overwrite]
111
-
112
- d = {
113
- :message_id => m.id,
114
- :source_id => source_id,
115
- :source_info => m.source_info,
116
- :date => (entry[:date] || m.date.to_indexable_s),
117
- :body => (entry[:body] || m.indexable_content),
118
- :snippet => snippet, # always override
119
- :label => labels.to_a.join(" "),
120
- :attachments => (entry[:attachments] || m.attachments.uniq.join(" ")),
121
-
122
- ## always override :from and :to.
123
- ## older versions of Sup would often store the wrong thing in the index
124
- ## (because they were canonicalizing email addresses, resulting in the
125
- ## wrong name associated with each.) the correct address is read from
126
- ## the original header when these messages are opened in thread-view-mode,
127
- ## so this allows people to forcibly update the address in the index by
128
- ## marking those threads for saving.
129
- :from => (m.from ? m.from.indexable_content : ""),
130
- :to => (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" "),
131
-
132
- ## always overwrite :refs.
133
- ## these might have changed due to manual thread joining.
134
- :refs => (m.refs + m.replytos).uniq.join(" "),
135
-
136
- :subject => (entry[:subject] || wrap_subj(Message.normalize_subj(m.subj))),
137
- }
138
-
139
- @index_mutex.synchronize do
140
- @index.delete m.id
141
- @index.add_document d
142
- end
143
- end
144
- private :sync_message
145
-
146
- def save_index fn=File.join(@dir, "ferret")
147
- # don't have to do anything, apparently
148
- end
149
-
150
- def contains_id? id
151
- @index_mutex.synchronize { @index.search(Ferret::Search::TermQuery.new(:message_id, id)).total_hits > 0 }
152
- end
153
-
154
- def size
155
- @index_mutex.synchronize { @index.size }
156
- end
157
-
158
- EACH_BY_DATE_NUM = 100
159
- def each_id_by_date query={}
160
- return if empty? # otherwise ferret barfs ###TODO: remove this once my ferret patch is accepted
161
- ferret_query = build_ferret_query query
162
- offset = 0
163
- while true
164
- limit = (query[:limit])? [EACH_BY_DATE_NUM, query[:limit] - offset].min : EACH_BY_DATE_NUM
165
- results = @index_mutex.synchronize { @index.search ferret_query, :sort => "date DESC", :limit => limit, :offset => offset }
166
- debug "got #{results.total_hits} results for query (offset #{offset}) #{ferret_query.inspect}"
167
- results.hits.each do |hit|
168
- yield @index_mutex.synchronize { @index[hit.doc][:message_id] }, lambda { build_message hit.doc }
169
- end
170
- break if query[:limit] and offset >= query[:limit] - limit
171
- break if offset >= results.total_hits - limit
172
- offset += limit
173
- end
174
- end
175
-
176
- def num_results_for query={}
177
- return 0 if empty? # otherwise ferret barfs ###TODO: remove this once my ferret patch is accepted
178
- ferret_query = build_ferret_query query
179
- @index_mutex.synchronize { @index.search(ferret_query, :limit => 1).total_hits }
180
- end
181
-
182
- SAME_SUBJECT_DATE_LIMIT = 7
183
- MAX_CLAUSES = 1000
184
- def each_message_in_thread_for m, opts={}
185
- #debug "Building thread for #{m.id}: #{m.subj}"
186
- messages = {}
187
- searched = {}
188
- num_queries = 0
189
-
190
- pending = [m.id]
191
- if $config[:thread_by_subject] # do subject queries
192
- date_min = m.date - (SAME_SUBJECT_DATE_LIMIT * 12 * 3600)
193
- date_max = m.date + (SAME_SUBJECT_DATE_LIMIT * 12 * 3600)
194
-
195
- q = Ferret::Search::BooleanQuery.new true
196
- sq = Ferret::Search::PhraseQuery.new(:subject)
197
- wrap_subj(Message.normalize_subj(m.subj)).split.each do |t|
198
- sq.add_term t
199
- end
200
- q.add_query sq, :must
201
- q.add_query Ferret::Search::RangeQuery.new(:date, :>= => date_min.to_indexable_s, :<= => date_max.to_indexable_s), :must
202
-
203
- q = build_ferret_query :qobj => q
204
-
205
- p1 = @index_mutex.synchronize { @index.search(q).hits.map { |hit| @index[hit.doc][:message_id] } }
206
- debug "found #{p1.size} results for subject query #{q}"
207
-
208
- p2 = @index_mutex.synchronize { @index.search(q.to_s, :limit => :all).hits.map { |hit| @index[hit.doc][:message_id] } }
209
- debug "found #{p2.size} results in string form"
210
-
211
- pending = (pending + p1 + p2).uniq
212
- end
213
-
214
- until pending.empty? || (opts[:limit] && messages.size >= opts[:limit])
215
- q = Ferret::Search::BooleanQuery.new true
216
- # this disappeared in newer ferrets... wtf.
217
- # q.max_clause_count = 2048
218
-
219
- lim = [MAX_CLAUSES / 2, pending.length].min
220
- pending[0 ... lim].each do |id|
221
- searched[id] = true
222
- q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
223
- q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
224
- end
225
- pending = pending[lim .. -1]
226
-
227
- q = build_ferret_query :qobj => q
228
-
229
- num_queries += 1
230
- killed = false
231
- @index_mutex.synchronize do
232
- @index.search_each(q, :limit => :all) do |docid, score|
233
- break if opts[:limit] && messages.size >= opts[:limit]
234
- if @index[docid][:label].split(/\s+/).include?("killed") && opts[:skip_killed]
235
- killed = true
236
- break
237
- end
238
- mid = @index[docid][:message_id]
239
- unless messages.member?(mid)
240
- #debug "got #{mid} as a child of #{id}"
241
- messages[mid] ||= lambda { build_message docid }
242
- refs = @index[docid][:refs].split
243
- pending += refs.select { |id| !searched[id] }
244
- end
245
- end
246
- end
247
- end
248
-
249
- if killed
250
- #debug "thread for #{m.id} is killed, ignoring"
251
- false
252
- else
253
- #debug "ran #{num_queries} queries to build thread of #{messages.size} messages for #{m.id}: #{m.subj}" if num_queries > 0
254
- messages.each { |mid, builder| yield mid, builder }
255
- true
256
- end
257
- end
258
-
259
- ## builds a message object from a ferret result
260
- def build_message docid
261
- @index_mutex.synchronize do
262
- doc = @index[docid] or return
263
-
264
- source = SourceManager[doc[:source_id].to_i]
265
- raise "invalid source #{doc[:source_id]}" unless source
266
-
267
- #puts "building message #{doc[:message_id]} (#{source}##{doc[:source_info]})"
268
-
269
- fake_header = {
270
- "date" => Time.at(doc[:date].to_i),
271
- "subject" => unwrap_subj(doc[:subject]),
272
- "from" => doc[:from],
273
- "to" => doc[:to].split.join(", "), # reformat
274
- "message-id" => doc[:message_id],
275
- "references" => doc[:refs].split.map { |x| "<#{x}>" }.join(" "),
276
- }
277
-
278
- m = Message.new :source => source, :source_info => doc[:source_info].to_i,
279
- :labels => doc[:label].to_set_of_symbols,
280
- :snippet => doc[:snippet]
281
- m.parse_header fake_header
282
- m
283
- end
284
- end
285
-
286
- def delete id
287
- @index_mutex.synchronize { @index.delete id }
288
- end
289
-
290
- def load_contacts emails, h={}
291
- q = Ferret::Search::BooleanQuery.new true
292
- emails.each do |e|
293
- qq = Ferret::Search::BooleanQuery.new true
294
- qq.add_query Ferret::Search::TermQuery.new(:from, e), :should
295
- qq.add_query Ferret::Search::TermQuery.new(:to, e), :should
296
- q.add_query qq
297
- end
298
- q.add_query Ferret::Search::TermQuery.new(:label, "spam"), :must_not
299
-
300
- debug "contact search: #{q}"
301
- contacts = {}
302
- num = h[:num] || 20
303
- @index_mutex.synchronize do
304
- @index.search_each q, :sort => "date DESC", :limit => :all do |docid, score|
305
- break if contacts.size >= num
306
- #debug "got message #{docid} to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
307
- f = @index[docid][:from]
308
- t = @index[docid][:to]
309
-
310
- if AccountManager.is_account_email? f
311
- t.split(" ").each { |e| contacts[Person.from_address(e)] = true }
312
- else
313
- contacts[Person.from_address(f)] = true
314
- end
315
- end
316
- end
317
-
318
- contacts.keys.compact
319
- end
320
-
321
- def each_id query={}
322
- ferret_query = build_ferret_query query
323
- results = @index_mutex.synchronize { @index.search ferret_query, :limit => (query[:limit] || :all) }
324
- results.hits.map { |hit| yield @index[hit.doc][:message_id] }
325
- end
326
-
327
- def optimize
328
- @index_mutex.synchronize { @index.optimize }
329
- end
330
-
331
- def source_for_id id
332
- entry = @index[id]
333
- return unless entry
334
- entry[:source_id].to_i
335
- end
336
-
337
- class ParseError < StandardError; end
338
-
339
- ## parse a query string from the user. returns a query object
340
- ## that can be passed to any index method with a 'query'
341
- ## argument, as well as build_ferret_query.
342
- ##
343
- ## raises a ParseError if something went wrong.
344
- def parse_query s
345
- query = {}
346
-
347
- subs = HookManager.run("custom-search", :subs => s) || s
348
- subs = subs.gsub(/\b(to|from):(\S+)\b/) do
349
- field, name = $1, $2
350
- if(p = ContactManager.contact_for(name))
351
- [field, p.email]
352
- elsif name == "me"
353
- [field, "(" + AccountManager.user_emails.join("||") + ")"]
354
- else
355
- [field, name]
356
- end.join(":")
357
- end
358
-
359
- ## if we see a label:deleted or a label:spam term anywhere in the query
360
- ## string, we set the extra load_spam or load_deleted options to true.
361
- ## bizarre? well, because the query allows arbitrary parenthesized boolean
362
- ## expressions, without fully parsing the query, we can't tell whether
363
- ## the user is explicitly directing us to search spam messages or not.
364
- ## e.g. if the string is -(-(-(-(-label:spam)))), does the user want to
365
- ## search spam messages or not?
366
- ##
367
- ## so, we rely on the fact that turning these extra options ON turns OFF
368
- ## the adding of "-label:deleted" or "-label:spam" terms at the very
369
- ## final stage of query processing. if the user wants to search spam
370
- ## messages, not adding that is the right thing; if he doesn't want to
371
- ## search spam messages, then not adding it won't have any effect.
372
- query[:load_spam] = true if subs =~ /\blabel:spam\b/
373
- query[:load_deleted] = true if subs =~ /\blabel:deleted\b/
374
-
375
- ## gmail style "is" operator
376
- subs = subs.gsub(/\b(is|has):(\S+)\b/) do
377
- field, label = $1, $2
378
- case label
379
- when "read"
380
- "-label:unread"
381
- when "spam"
382
- query[:load_spam] = true
383
- "label:spam"
384
- when "deleted"
385
- query[:load_deleted] = true
386
- "label:deleted"
387
- else
388
- "label:#{$2}"
389
- end
390
- end
391
-
392
- ## gmail style attachments "filename" and "filetype" searches
393
- subs = subs.gsub(/\b(filename|filetype):(\((.+?)\)\B|(\S+)\b)/) do
394
- field, name = $1, ($3 || $4)
395
- case field
396
- when "filename"
397
- debug "filename: translated #{field}:#{name} to attachments:(#{name.downcase})"
398
- "attachments:(#{name.downcase})"
399
- when "filetype"
400
- debug "filetype: translated #{field}:#{name} to attachments:(*.#{name.downcase})"
401
- "attachments:(*.#{name.downcase})"
402
- end
403
- end
404
-
405
- if $have_chronic
406
- subs = subs.gsub(/\b(before|on|in|during|after):(\((.+?)\)\B|(\S+)\b)/) do
407
- field, datestr = $1, ($3 || $4)
408
- realdate = Chronic.parse datestr, :guess => false, :context => :past
409
- if realdate
410
- case field
411
- when "after"
412
- debug "chronic: translated #{field}:#{datestr} to #{realdate.end}"
413
- "date:(>= #{sprintf "%012d", realdate.end.to_i})"
414
- when "before"
415
- debug "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
416
- "date:(<= #{sprintf "%012d", realdate.begin.to_i})"
417
- else
418
- debug "chronic: translated #{field}:#{datestr} to #{realdate}"
419
- "date:(<= #{sprintf "%012d", realdate.end.to_i}) date:(>= #{sprintf "%012d", realdate.begin.to_i})"
420
- end
421
- else
422
- raise ParseError, "can't understand date #{datestr.inspect}"
423
- end
424
- end
425
- end
426
-
427
- ## limit:42 restrict the search to 42 results
428
- subs = subs.gsub(/\blimit:(\S+)\b/) do
429
- lim = $1
430
- if lim =~ /^\d+$/
431
- query[:limit] = lim.to_i
432
- ''
433
- else
434
- raise ParseError, "non-numeric limit #{lim.inspect}"
435
- end
436
- end
437
-
438
- begin
439
- query[:qobj] = @qparser.parse(subs)
440
- query[:text] = s
441
- query
442
- rescue Ferret::QueryParser::QueryParseException => e
443
- raise ParseError, e.message
444
- end
445
- end
446
-
447
- private
448
-
449
- def build_ferret_query query
450
- q = Ferret::Search::BooleanQuery.new
451
- q.add_query Ferret::Search::MatchAllQuery.new, :must
452
- q.add_query query[:qobj], :must if query[:qobj]
453
- labels = ([query[:label]] + (query[:labels] || [])).compact
454
- labels.each { |t| q.add_query Ferret::Search::TermQuery.new("label", t.to_s), :must }
455
- if query[:participants]
456
- q2 = Ferret::Search::BooleanQuery.new
457
- query[:participants].each do |p|
458
- q2.add_query Ferret::Search::TermQuery.new("from", p.email), :should
459
- q2.add_query Ferret::Search::TermQuery.new("to", p.email), :should
460
- end
461
- q.add_query q2, :must
462
- end
463
-
464
- q.add_query Ferret::Search::TermQuery.new("label", "spam"), :must_not unless query[:load_spam] || labels.include?(:spam)
465
- q.add_query Ferret::Search::TermQuery.new("label", "deleted"), :must_not unless query[:load_deleted] || labels.include?(:deleted)
466
- q.add_query Ferret::Search::TermQuery.new("label", "killed"), :must_not if query[:skip_killed]
467
-
468
- q.add_query Ferret::Search::TermQuery.new("source_id", query[:source_id]), :must if query[:source_id]
469
- q
470
- end
471
-
472
- def wrap_subj subj; "__START_SUBJECT__ #{subj} __END_SUBJECT__"; end
473
- def unwrap_subj subj; subj =~ /__START_SUBJECT__ (.*?) __END_SUBJECT__/ && $1; end
474
- end
475
-
476
- end