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 +7 -3
- data/History.txt +12 -0
- data/README.txt +2 -2
- data/ReleaseNotes +11 -0
- data/bin/sup +26 -5
- data/bin/sup-tweak-labels +5 -0
- data/lib/sup.rb +2 -1
- data/lib/sup/buffer.rb +3 -3
- data/lib/sup/crypto.rb +1 -1
- data/lib/sup/draft.rb +4 -1
- data/lib/sup/hook.rb +12 -1
- data/lib/sup/imap.rb +24 -4
- data/lib/sup/index.rb +154 -98
- data/lib/sup/label.rb +1 -1
- data/lib/sup/mbox.rb +8 -8
- data/lib/sup/message-chunks.rb +5 -3
- data/lib/sup/message.rb +4 -4
- data/lib/sup/modes/thread-index-mode.rb +2 -1
- data/lib/sup/modes/thread-view-mode.rb +16 -2
- data/lib/sup/rfc2047.rb +1 -6
- data/lib/sup/textfield.rb +1 -1
- data/lib/sup/util.rb +17 -0
- metadata +56 -81
- data/HACKING +0 -42
- data/Manifest.txt +0 -75
- data/Rakefile +0 -60
- data/doc/FAQ.txt +0 -132
- data/doc/Hooks.txt +0 -38
- data/doc/NewUserGuide.txt +0 -255
- data/doc/Philosophy.txt +0 -69
- data/test/test_mbox_parsing.rb +0 -114
- data/test/test_message.rb +0 -441
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
|
-
|
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
|
-
|
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
|
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
|
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.
|
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", '
|
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
|
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 =
|
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.
|
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
|
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
|
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
|
-
|
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
|
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
|
83
|
+
secs = (Time.now - e.mtime).to_i
|
84
|
+
mins = secs / 60
|
72
85
|
time =
|
73
86
|
if mins == 0
|
74
|
-
|
87
|
+
possibly_pluralize secs , "second"
|
75
88
|
else
|
76
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
@
|
140
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
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
|
-
@
|
235
|
-
|
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
|
251
|
-
def size; @index.size
|
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
|
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
|
-
|
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
|
264
|
-
|
265
|
-
|
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
|
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
|
-
@
|
334
|
-
|
335
|
-
|
336
|
-
killed
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
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
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
"
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
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
|
418
|
+
def drop_entry docno; @index_mutex.synchronize { @index.delete docno } end
|
385
419
|
|
386
420
|
def load_entry_for_id mid
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
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
|
-
@
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
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
|
-
@
|
425
|
-
|
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
|
-
|
554
|
-
|
555
|
-
|
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
|
-
|
560
|
-
File.chmod 0600, fn
|
617
|
+
@sources_dirty = false
|
561
618
|
end
|
562
|
-
@sources_dirty = false
|
563
619
|
end
|
564
620
|
end
|
565
621
|
|