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.
- data/CONTRIBUTORS +8 -3
- data/History.txt +19 -0
- data/README.txt +45 -44
- data/ReleaseNotes +6 -0
- data/bin/sup +36 -5
- data/bin/sup-add +0 -0
- data/bin/sup-config +0 -0
- data/bin/sup-dump +0 -0
- data/bin/sup-recover-sources +8 -12
- data/bin/sup-sync +22 -16
- data/bin/sup-sync-back +1 -1
- data/bin/sup-tweak-labels +8 -8
- data/lib/sup.rb +3 -17
- data/lib/sup/account.rb +2 -3
- data/lib/sup/buffer.rb +21 -10
- data/lib/sup/colormap.rb +30 -27
- data/lib/sup/contact.rb +1 -1
- data/lib/sup/draft.rb +1 -3
- data/lib/sup/imap.rb +1 -1
- data/lib/sup/index.rb +70 -48
- data/lib/sup/label.rb +12 -10
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +1 -1
- data/lib/sup/mbox.rb +13 -70
- data/lib/sup/mbox/loader.rb +26 -15
- data/lib/sup/message-chunks.rb +18 -6
- data/lib/sup/message.rb +56 -67
- data/lib/sup/mode.rb +2 -1
- data/lib/sup/modes/buffer-list-mode.rb +6 -2
- data/lib/sup/modes/compose-mode.rb +0 -1
- data/lib/sup/modes/contact-list-mode.rb +1 -1
- data/lib/sup/modes/edit-message-mode.rb +37 -9
- data/lib/sup/modes/inbox-mode.rb +34 -0
- data/lib/sup/modes/label-list-mode.rb +10 -3
- data/lib/sup/modes/reply-mode.rb +24 -13
- data/lib/sup/modes/resume-mode.rb +2 -0
- data/lib/sup/modes/scroll-mode.rb +10 -9
- data/lib/sup/modes/search-results-mode.rb +2 -2
- data/lib/sup/modes/thread-index-mode.rb +157 -38
- data/lib/sup/modes/thread-view-mode.rb +27 -11
- data/lib/sup/person.rb +22 -73
- data/lib/sup/poll.rb +18 -20
- data/lib/sup/source.rb +44 -0
- data/lib/sup/undo.rb +39 -0
- data/lib/sup/util.rb +25 -16
- 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.
|
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"
|
data/lib/sup/account.rb
CHANGED
@@ -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
|
9
|
-
super h[:name], h[:email]
|
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
|
data/lib/sup/buffer.rb
CHANGED
@@ -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
|
-
|
110
|
-
|
111
|
-
|
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 =>
|
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
|
-
|
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.
|
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) ||
|
520
|
+
answer.split_on_commas.map { |x| ContactManager.contact_for(x) || Person.from_address(x) }
|
510
521
|
end
|
511
522
|
end
|
512
523
|
|
data/lib/sup/colormap.rb
CHANGED
@@ -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 =
|
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 => "
|
19
|
-
:index_new => { :fg => "white", :bg => "
|
20
|
-
:index_starred => { :fg => "yellow", :bg => "
|
21
|
-
:index_draft => { :fg => "red", :bg => "
|
22
|
-
:labellist_old => { :fg => "white", :bg => "
|
23
|
-
:labellist_new => { :fg => "white", :bg => "
|
24
|
-
:twiddle => { :fg => "blue", :bg => "
|
25
|
-
:label => { :fg => "yellow", :bg => "
|
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 => "
|
30
|
-
:cryptosig_valid => { :fg => "yellow", :bg => "
|
31
|
-
:cryptosig_unknown => { :fg => "cyan", :bg => "
|
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 => "
|
34
|
-
:quote_patina => { :fg => "yellow", :bg => "
|
35
|
-
:sig_patina => { :fg => "yellow", :bg => "
|
36
|
-
:quote => { :fg => "yellow", :bg => "
|
37
|
-
:sig => { :fg => "yellow", :bg => "
|
38
|
-
:to_me => { :fg => "green", :bg => "
|
39
|
-
:starred => { :fg => "yellow", :bg => "
|
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 => "
|
43
|
-
:option => { :fg => "white", :bg => "
|
44
|
-
:tagged => { :fg => "yellow", :bg => "
|
45
|
-
:draft_notification => { :fg => "red", :bg => "
|
46
|
-
:completion_character => { :fg => "white", :bg => "
|
47
|
-
:horizontal_selector_selected => { :fg => "yellow", :bg => "
|
48
|
-
:horizontal_selector_unselected => { :fg => "cyan", :bg => "
|
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
|
data/lib/sup/contact.rb
CHANGED
@@ -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 =
|
20
|
+
p = Person.from_address addr
|
21
21
|
@p2a[p] = aalias
|
22
22
|
@a2p[aalias] = p unless aalias.nil? || aalias.empty?
|
23
23
|
end
|
data/lib/sup/draft.rb
CHANGED
@@ -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
|
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
|
data/lib/sup/imap.rb
CHANGED
@@ -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
|
-
|
96
|
+
parse_raw_email_header StringIO.new(raw_header(id))
|
97
97
|
end
|
98
98
|
|
99
99
|
def load_message id
|
data/lib/sup/index.rb
CHANGED
@@ -177,31 +177,31 @@ EOS
|
|
177
177
|
end
|
178
178
|
end
|
179
179
|
|
180
|
-
## Syncs the message to the index
|
181
|
-
##
|
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
|
184
|
-
|
185
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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].
|
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
|
-
|
250
|
-
|
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
|
264
|
+
@index_mutex.synchronize do
|
256
265
|
@index.delete docid if docid
|
257
266
|
@index.add_document d
|
258
267
|
end
|
259
268
|
|
260
|
-
|
261
|
-
##
|
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
|
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
|
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
|
411
|
+
"to" => doc[:to].split.join(", "), # reformat
|
404
412
|
"message-id" => doc[:message_id],
|
405
|
-
"references" => doc[:refs].split
|
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].
|
410
|
-
:snippet => doc[:snippet]
|
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[
|
462
|
+
t.split(" ").each { |e| contacts[Person.from_address(e)] = true }
|
453
463
|
else
|
454
|
-
contacts[
|
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
|
-
|
480
|
-
|
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
|
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
|
-
|
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
|
-
|
575
|
-
subs = nil
|
597
|
+
raise ParseError, "non-numeric limit #{lim.inspect}"
|
576
598
|
end
|
577
599
|
end
|
578
600
|
|
579
|
-
|
601
|
+
begin
|
580
602
|
[@qparser.parse(subs), extraopts]
|
581
|
-
|
582
|
-
|
603
|
+
rescue Ferret::QueryParser::QueryParseException => e
|
604
|
+
raise ParseError, e.message
|
583
605
|
end
|
584
606
|
end
|
585
607
|
|