sup 0.3 → 0.4
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/HACKING +31 -9
- data/History.txt +7 -0
- data/Manifest.txt +2 -0
- data/Rakefile +9 -5
- data/bin/sup +81 -57
- data/bin/sup-config +1 -1
- data/bin/sup-sync +3 -0
- data/bin/sup-tweak-labels +127 -0
- data/doc/TODO +23 -12
- data/lib/sup.rb +13 -11
- data/lib/sup/account.rb +25 -12
- data/lib/sup/buffer.rb +61 -41
- data/lib/sup/colormap.rb +2 -0
- data/lib/sup/contact.rb +28 -18
- data/lib/sup/crypto.rb +86 -31
- data/lib/sup/draft.rb +12 -6
- data/lib/sup/horizontal-selector.rb +47 -0
- data/lib/sup/imap.rb +50 -37
- data/lib/sup/index.rb +76 -13
- data/lib/sup/keymap.rb +27 -8
- data/lib/sup/maildir.rb +1 -1
- data/lib/sup/mbox/loader.rb +1 -1
- data/lib/sup/message-chunks.rb +43 -15
- data/lib/sup/message.rb +67 -31
- data/lib/sup/mode.rb +40 -9
- data/lib/sup/modes/completion-mode.rb +1 -1
- data/lib/sup/modes/compose-mode.rb +3 -3
- data/lib/sup/modes/contact-list-mode.rb +12 -8
- data/lib/sup/modes/edit-message-mode.rb +100 -36
- data/lib/sup/modes/file-browser-mode.rb +1 -0
- data/lib/sup/modes/forward-mode.rb +43 -8
- data/lib/sup/modes/inbox-mode.rb +8 -5
- data/lib/sup/modes/label-search-results-mode.rb +12 -1
- data/lib/sup/modes/line-cursor-mode.rb +4 -7
- data/lib/sup/modes/reply-mode.rb +59 -54
- data/lib/sup/modes/resume-mode.rb +6 -6
- data/lib/sup/modes/scroll-mode.rb +4 -3
- data/lib/sup/modes/search-results-mode.rb +8 -5
- data/lib/sup/modes/text-mode.rb +19 -2
- data/lib/sup/modes/thread-index-mode.rb +109 -40
- data/lib/sup/modes/thread-view-mode.rb +180 -49
- data/lib/sup/person.rb +3 -3
- data/lib/sup/poll.rb +9 -8
- data/lib/sup/rfc2047.rb +7 -1
- data/lib/sup/sent.rb +1 -1
- data/lib/sup/tagger.rb +10 -4
- data/lib/sup/textfield.rb +7 -7
- data/lib/sup/thread.rb +86 -49
- data/lib/sup/update.rb +11 -0
- data/lib/sup/util.rb +74 -34
- data/test/test_message.rb +441 -0
- metadata +136 -117
data/lib/sup/crypto.rb
CHANGED
@@ -3,42 +3,98 @@ module Redwood
|
|
3
3
|
class CryptoManager
|
4
4
|
include Singleton
|
5
5
|
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
|
9
|
+
[:sign, "Sign"],
|
10
|
+
[:sign_and_encrypt, "Sign and encrypt"],
|
11
|
+
[:encrypt, "Encrypt only"]
|
12
|
+
)
|
13
|
+
|
6
14
|
def initialize
|
7
15
|
@mutex = Mutex.new
|
8
16
|
self.class.i_am_the_instance self
|
9
17
|
|
10
18
|
bin = `which gpg`.chomp
|
11
|
-
bin = `which pgp`.chomp unless bin =~ /\S/
|
12
19
|
|
13
20
|
@cmd =
|
14
21
|
case bin
|
15
22
|
when /\S/
|
23
|
+
Redwood::log "crypto: detected gpg binary in #{bin}"
|
16
24
|
"#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
|
17
25
|
else
|
26
|
+
Redwood::log "crypto: no gpg binary detected"
|
18
27
|
nil
|
19
28
|
end
|
20
29
|
end
|
21
30
|
|
22
|
-
|
31
|
+
def have_crypto?; !@cmd.nil? end
|
32
|
+
|
33
|
+
def sign from, to, payload
|
34
|
+
payload_fn = Tempfile.new "redwood.payload"
|
35
|
+
payload_fn.write format_payload(payload)
|
36
|
+
payload_fn.close
|
37
|
+
|
38
|
+
output = run_gpg "--output - --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}"
|
39
|
+
|
40
|
+
raise Error, (output || "gpg command failed: #{cmd}") unless $?.success?
|
41
|
+
|
42
|
+
envelope = RMail::Message.new
|
43
|
+
envelope.header["Content-Type"] = 'multipart/signed; protocol="application/pgp-signature"; micalg=pgp-sha1'
|
44
|
+
|
45
|
+
envelope.add_part payload
|
46
|
+
signature = RMail::Message.make_attachment output, "application/pgp-signature", nil, "signature.asc"
|
47
|
+
envelope.add_part signature
|
48
|
+
envelope
|
49
|
+
end
|
50
|
+
|
51
|
+
def encrypt from, to, payload, sign=false
|
52
|
+
payload_fn = Tempfile.new "redwood.payload"
|
53
|
+
payload_fn.write format_payload(payload)
|
54
|
+
payload_fn.close
|
55
|
+
|
56
|
+
recipient_opts = to.map { |r| "--recipient '#{r}'" }.join(" ")
|
57
|
+
sign_opts = sign ? "--sign --local-user '#{from}'" : ""
|
58
|
+
gpg_output = run_gpg "--output - --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}"
|
59
|
+
raise Error, (gpg_output || "gpg command failed: #{cmd}") unless $?.success?
|
60
|
+
|
61
|
+
encrypted_payload = RMail::Message.new
|
62
|
+
encrypted_payload.header["Content-Type"] = "application/octet-stream"
|
63
|
+
encrypted_payload.header["Content-Disposition"] = 'inline; filename="msg.asc"'
|
64
|
+
encrypted_payload.body = gpg_output
|
65
|
+
|
66
|
+
control = RMail::Message.new
|
67
|
+
control.header["Content-Type"] = "application/pgp-encrypted"
|
68
|
+
control.header["Content-Disposition"] = "attachment"
|
69
|
+
control.body = "Version: 1\n"
|
70
|
+
|
71
|
+
envelope = RMail::Message.new
|
72
|
+
envelope.header["Content-Type"] = 'multipart/encrypted; protocol="application/pgp-encrypted"'
|
73
|
+
|
74
|
+
envelope.add_part control
|
75
|
+
envelope.add_part encrypted_payload
|
76
|
+
envelope
|
77
|
+
end
|
78
|
+
|
79
|
+
def sign_and_encrypt from, to, payload
|
80
|
+
encrypt from, to, payload, true
|
81
|
+
end
|
82
|
+
|
23
83
|
def verify payload, signature # both RubyMail::Message objects
|
24
84
|
return unknown_status(cant_find_binary) unless @cmd
|
25
85
|
|
26
86
|
payload_fn = Tempfile.new "redwood.payload"
|
27
|
-
payload_fn.write payload
|
87
|
+
payload_fn.write format_payload(payload)
|
28
88
|
payload_fn.close
|
29
89
|
|
30
90
|
signature_fn = Tempfile.new "redwood.signature"
|
31
91
|
signature_fn.write signature.decode
|
32
92
|
signature_fn.close
|
33
93
|
|
34
|
-
|
35
|
-
|
36
|
-
#Redwood::log "gpg: running: #{cmd}"
|
37
|
-
gpg_output = `#{cmd}`
|
38
|
-
#Redwood::log "got output: #{gpg_output.inspect}"
|
39
|
-
output_lines = gpg_output.split(/\n/)
|
94
|
+
output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}"
|
95
|
+
output_lines = output.split(/\n/)
|
40
96
|
|
41
|
-
if
|
97
|
+
if output =~ /^gpg: (.* signature from .*$)/
|
42
98
|
if $? == 0
|
43
99
|
Chunk::CryptoNotice.new :valid, $1, output_lines
|
44
100
|
else
|
@@ -49,35 +105,22 @@ class CryptoManager
|
|
49
105
|
end
|
50
106
|
end
|
51
107
|
|
52
|
-
|
53
|
-
def decrypt payload # RubyMail::Message
|
108
|
+
## returns decrypted_message, status, desc, lines
|
109
|
+
def decrypt payload # a RubyMail::Message object
|
54
110
|
return unknown_status(cant_find_binary) unless @cmd
|
55
111
|
|
56
|
-
# cmd = "#{@cmd} --decrypt 2> /dev/null"
|
57
|
-
|
58
|
-
# Redwood::log "gpg: running: #{cmd}"
|
59
|
-
|
60
|
-
# gpg_output =
|
61
|
-
# IO.popen(cmd, "a+") do |f|
|
62
|
-
# f.puts payload.to_s
|
63
|
-
# f.gets
|
64
|
-
# end
|
65
|
-
|
66
112
|
payload_fn = Tempfile.new "redwood.payload"
|
67
113
|
payload_fn.write payload.to_s
|
68
114
|
payload_fn.close
|
69
115
|
|
70
|
-
|
71
|
-
Redwood::log "gpg: running: #{cmd}"
|
72
|
-
gpg_output = `#{cmd}`
|
73
|
-
Redwood::log "got output: #{gpg_output.inspect}"
|
116
|
+
output = run_gpg "--decrypt #{payload_fn.path}"
|
74
117
|
|
75
|
-
if
|
118
|
+
if $?.success?
|
76
119
|
decrypted_payload, sig_lines =
|
77
|
-
if
|
120
|
+
if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
|
78
121
|
[$1, $2]
|
79
122
|
else
|
80
|
-
[
|
123
|
+
[output, nil]
|
81
124
|
end
|
82
125
|
|
83
126
|
sig =
|
@@ -92,7 +135,7 @@ class CryptoManager
|
|
92
135
|
notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
|
93
136
|
[RMail::Parser.read(decrypted_payload), sig, notice]
|
94
137
|
else
|
95
|
-
notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted",
|
138
|
+
notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
|
96
139
|
[nil, nil, notice]
|
97
140
|
end
|
98
141
|
end
|
@@ -104,7 +147,19 @@ private
|
|
104
147
|
end
|
105
148
|
|
106
149
|
def cant_find_binary
|
107
|
-
["Can't find gpg
|
150
|
+
["Can't find gpg binary in path."]
|
151
|
+
end
|
152
|
+
|
153
|
+
def format_payload payload
|
154
|
+
payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
|
155
|
+
end
|
156
|
+
|
157
|
+
def run_gpg args
|
158
|
+
cmd = "#{@cmd} #{args} 2> /dev/null"
|
159
|
+
#Redwood::log "crypto: running: #{cmd}"
|
160
|
+
output = `#{cmd}`
|
161
|
+
#Redwood::log "crypto: output: #{output.inspect}" unless $?.success?
|
162
|
+
output
|
108
163
|
end
|
109
164
|
end
|
110
165
|
end
|
data/lib/sup/draft.rb
CHANGED
@@ -23,20 +23,20 @@ class DraftManager
|
|
23
23
|
@source.each do |thisoffset, theselabels|
|
24
24
|
m = Message.new :source => @source, :source_info => thisoffset, :labels => theselabels
|
25
25
|
Index.sync_message m
|
26
|
-
UpdateManager.relay self, :
|
26
|
+
UpdateManager.relay self, :added, m
|
27
27
|
my_message = m if thisoffset == offset
|
28
28
|
end
|
29
29
|
|
30
30
|
my_message
|
31
31
|
end
|
32
32
|
|
33
|
-
def discard
|
34
|
-
docid, entry = Index.load_entry_for_id
|
35
|
-
raise ArgumentError, "can't find entry for draft: #{
|
36
|
-
raise ArgumentError, "not a draft: source id #{entry[:source_id].inspect}, should be #{DraftManager.source_id.inspect} for #{
|
33
|
+
def discard m
|
34
|
+
docid, entry = Index.load_entry_for_id m.id
|
35
|
+
raise ArgumentError, "can't find entry for draft: #{m.id.inspect}" unless entry
|
36
|
+
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
37
|
Index.drop_entry docid
|
38
38
|
File.delete @source.fn_for_offset(entry[:source_info])
|
39
|
-
UpdateManager.relay self, :
|
39
|
+
UpdateManager.relay self, :single_message_deleted, m
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
@@ -99,6 +99,12 @@ class DraftLoader < Source
|
|
99
99
|
ret
|
100
100
|
end
|
101
101
|
|
102
|
+
def each_raw_message_line offset
|
103
|
+
File.open(fn_for_offset(offset)) do |f|
|
104
|
+
yield f.gets until f.eof?
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
102
108
|
def raw_message offset
|
103
109
|
IO.readlines(fn_for_offset(offset)).join
|
104
110
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class HorizontalSelector
|
4
|
+
attr_accessor :label
|
5
|
+
|
6
|
+
def initialize label, vals, labels, base_color=:horizontal_selector_unselected_color, selected_color=:horizontal_selector_selected_color
|
7
|
+
@label = label
|
8
|
+
@vals = vals
|
9
|
+
@labels = labels
|
10
|
+
@base_color = base_color
|
11
|
+
@selected_color = selected_color
|
12
|
+
@selection = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_to val; @selection = @vals.index(val) end
|
16
|
+
|
17
|
+
def val; @vals[@selection] end
|
18
|
+
|
19
|
+
def line width=nil
|
20
|
+
label =
|
21
|
+
if width
|
22
|
+
sprintf "%#{width}s ", @label
|
23
|
+
else
|
24
|
+
"#{@label} "
|
25
|
+
end
|
26
|
+
|
27
|
+
[[@base_color, label]] +
|
28
|
+
(0 ... @labels.length).inject([]) do |array, i|
|
29
|
+
array + [
|
30
|
+
if i == @selection
|
31
|
+
[@selected_color, @labels[i]]
|
32
|
+
else
|
33
|
+
[@base_color, @labels[i]]
|
34
|
+
end] + [[@base_color, " "]]
|
35
|
+
end + [[@base_color, ""]]
|
36
|
+
end
|
37
|
+
|
38
|
+
def roll_left
|
39
|
+
@selection = (@selection - 1) % @labels.length
|
40
|
+
end
|
41
|
+
|
42
|
+
def roll_right
|
43
|
+
@selection = (@selection + 1) % @labels.length
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/lib/sup/imap.rb
CHANGED
@@ -5,39 +5,39 @@ require 'time'
|
|
5
5
|
require 'rmail'
|
6
6
|
require 'cgi'
|
7
7
|
|
8
|
-
## fucking imap fucking sucks. what the FUCK kind of committee of
|
9
|
-
##
|
8
|
+
## fucking imap fucking sucks. what the FUCK kind of committee of dunces
|
9
|
+
## designed this shit.
|
10
10
|
##
|
11
11
|
## imap talks about 'unique ids' for messages, to be used for
|
12
|
-
## cross-session identification. great---just what sup needs! except
|
13
|
-
##
|
14
|
-
##
|
15
|
-
##
|
16
|
-
##
|
17
|
-
##
|
18
|
-
##
|
19
|
-
##
|
20
|
-
##
|
21
|
-
##
|
22
|
-
##
|
12
|
+
## cross-session identification. great---just what sup needs! except it
|
13
|
+
## turns out the uids can be invalidated every time the 'uidvalidity'
|
14
|
+
## value changes on the server, and 'uidvalidity' can change without
|
15
|
+
## restriction. it can change any time you log in. it can change EVERY
|
16
|
+
## time you log in. of course the imap spec "strongly recommends" that it
|
17
|
+
## never change, but there's nothing to stop people from just setting it
|
18
|
+
## to the current timestamp, and in fact that's exactly what the one imap
|
19
|
+
## server i have at my disposal does. thus the so-called uids are
|
20
|
+
## absolutely useless and imap provides no cross-session way of uniquely
|
21
|
+
## identifying a message. but thanks for the "strong recommendation",
|
22
|
+
## guys!
|
23
23
|
##
|
24
24
|
## so right now i'm using the 'internal date' and the size of each
|
25
25
|
## message to uniquely identify it, and i scan over the entire mailbox
|
26
26
|
## each time i open it to map those things to message ids. that can be
|
27
|
-
## slow for large mailboxes, and we'll just have to hope that there
|
28
|
-
##
|
27
|
+
## slow for large mailboxes, and we'll just have to hope that there are
|
28
|
+
## no collisions. ho ho! a perfectly reasonable solution!
|
29
29
|
##
|
30
30
|
## and here's another thing. check out RFC2060 2.2.2 paragraph 5:
|
31
31
|
##
|
32
|
-
## A client MUST be prepared to accept any server response at all
|
33
|
-
## This includes server data that was not requested.
|
32
|
+
## A client MUST be prepared to accept any server response at all
|
33
|
+
## times. This includes server data that was not requested.
|
34
34
|
##
|
35
|
-
## yeah. that totally makes a lot of sense. and once again, the idiocy
|
36
|
-
##
|
37
|
-
##
|
38
|
-
##
|
39
|
-
##
|
40
|
-
##
|
35
|
+
## yeah. that totally makes a lot of sense. and once again, the idiocy of
|
36
|
+
## the spec actually happens in practice. you'll request flags for one
|
37
|
+
## message, and get it interspersed with a random bunch of flags for some
|
38
|
+
## other messages, including a different set of flags for the same
|
39
|
+
## message! totally ok by the imap spec. totally retarded by any other
|
40
|
+
## metric.
|
41
41
|
##
|
42
42
|
## fuck you, imap committee. you managed to design something nearly as
|
43
43
|
## shitty as mbox but goddamn THIRTY YEARS LATER.
|
@@ -72,11 +72,7 @@ class IMAP < Source
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def self.suggest_labels_for path
|
75
|
-
|
76
|
-
[path.intern]
|
77
|
-
else
|
78
|
-
[]
|
79
|
-
end
|
75
|
+
path =~ /([^\/]*inbox[^\/]*)/i ? [$1.downcase.intern] : []
|
80
76
|
end
|
81
77
|
|
82
78
|
def host; @parsed_uri.host; end
|
@@ -141,6 +137,7 @@ class IMAP < Source
|
|
141
137
|
@ids << id
|
142
138
|
@imap_state[id] = { :id => v.seqno, :flags => v.attr["FLAGS"] }
|
143
139
|
end
|
140
|
+
Redwood::log "done fetching IMAP headers"
|
144
141
|
end
|
145
142
|
synchronized :scan_mailbox
|
146
143
|
|
@@ -208,11 +205,11 @@ private
|
|
208
205
|
def unsafe_connect
|
209
206
|
say "Connecting to IMAP server #{host}:#{port}..."
|
210
207
|
|
211
|
-
## apparently imap.rb does a lot of threaded stuff internally and
|
212
|
-
##
|
213
|
-
## calling thread. but i can't seem to catch that exception, so
|
214
|
-
##
|
215
|
-
##
|
208
|
+
## apparently imap.rb does a lot of threaded stuff internally and if
|
209
|
+
## an exception occurs, it will catch it and re-raise it on the
|
210
|
+
## calling thread. but i can't seem to catch that exception, so i've
|
211
|
+
## resorted to initializing it in its own thread. surely there's a
|
212
|
+
## better way.
|
216
213
|
exception = nil
|
217
214
|
::Thread.new do
|
218
215
|
begin
|
@@ -220,9 +217,9 @@ private
|
|
220
217
|
@imap = Net::IMAP.new host, port, ssl?
|
221
218
|
say "Logging in..."
|
222
219
|
|
223
|
-
## although RFC1730 claims that "If an AUTHENTICATE command
|
224
|
-
##
|
225
|
-
##
|
220
|
+
## although RFC1730 claims that "If an AUTHENTICATE command fails
|
221
|
+
## with a NO response, the client may try another", in practice
|
222
|
+
## it seems like they can also send a BAD response.
|
226
223
|
begin
|
227
224
|
raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=CRAM-MD5"
|
228
225
|
@imap.authenticate 'CRAM-MD5', @username, @password
|
@@ -273,7 +270,23 @@ private
|
|
273
270
|
imap_id = @imap_state[id][:id]
|
274
271
|
result = fetch(imap_id, (fields + ['RFC822.SIZE', 'INTERNALDATE']).uniq).first
|
275
272
|
got_id = make_id result
|
276
|
-
|
273
|
+
|
274
|
+
## I've turned off the following sanity check because Microsoft
|
275
|
+
## Exchange fails it. Exchange actually reports two different
|
276
|
+
## INTERNALDATEs for the exact same message when queried at different
|
277
|
+
## points in time.
|
278
|
+
##
|
279
|
+
## RFC2060 defines the semantics of INTERNALDATE for messages that
|
280
|
+
## arrive via SMTP for via various IMAP commands, but states that
|
281
|
+
## "All other cases are implementation defined.". Great, thanks guys,
|
282
|
+
## yet another useless field.
|
283
|
+
##
|
284
|
+
## Of course no OTHER imap server I've encountered returns DIFFERENT
|
285
|
+
## values for the SAME message. But it's Microsoft; what do you
|
286
|
+
## expect? If their programmers were any good they'd be working at
|
287
|
+
## Google.
|
288
|
+
|
289
|
+
# raise OutOfSyncSourceError, "IMAP message mismatch: requested #{id}, got #{got_id}." unless got_id == id
|
277
290
|
|
278
291
|
fields.map { |f| result.attr[f] or raise FatalSourceError, "empty response from IMAP server: #{f}" }
|
279
292
|
end
|
data/lib/sup/index.rb
CHANGED
@@ -171,17 +171,24 @@ EOS
|
|
171
171
|
end
|
172
172
|
|
173
173
|
to = (m.to + m.cc + m.bcc).map { |x| x.email }.join(" ")
|
174
|
+
snippet =
|
175
|
+
if m.snippet_contains_encrypted_content? && $config[:discard_snippets_from_encrypted_messages]
|
176
|
+
""
|
177
|
+
else
|
178
|
+
m.snippet
|
179
|
+
end
|
180
|
+
|
174
181
|
d = {
|
175
182
|
:message_id => m.id,
|
176
183
|
:source_id => source_id,
|
177
184
|
:source_info => m.source_info,
|
178
185
|
:date => m.date.to_indexable_s,
|
179
186
|
:body => m.content,
|
180
|
-
:snippet =>
|
187
|
+
:snippet => snippet,
|
181
188
|
:label => m.labels.uniq.join(" "),
|
182
189
|
:from => m.from ? m.from.email : "",
|
183
190
|
:to => (m.to + m.cc + m.bcc).map { |x| x.email }.join(" "),
|
184
|
-
:subject => wrap_subj(
|
191
|
+
:subject => wrap_subj(m.subj),
|
185
192
|
:refs => (m.refs + m.replytos).uniq.join(" "),
|
186
193
|
}
|
187
194
|
|
@@ -236,6 +243,7 @@ EOS
|
|
236
243
|
## true, stops loading any thread if a message with a :killed flag
|
237
244
|
## is found.
|
238
245
|
SAME_SUBJECT_DATE_LIMIT = 7
|
246
|
+
MAX_CLAUSES = 1000
|
239
247
|
def each_message_in_thread_for m, opts={}
|
240
248
|
#Redwood::log "Building thread for #{m.id}: #{m.subj}"
|
241
249
|
messages = {}
|
@@ -264,13 +272,16 @@ EOS
|
|
264
272
|
|
265
273
|
until pending.empty? || (opts[:limit] && messages.size >= opts[:limit])
|
266
274
|
q = Ferret::Search::BooleanQuery.new true
|
275
|
+
# this disappeared in newer ferrets... wtf.
|
276
|
+
# q.max_clause_count = 2048
|
267
277
|
|
268
|
-
pending.
|
278
|
+
lim = [MAX_CLAUSES / 2, pending.length].min
|
279
|
+
pending[0 ... lim].each do |id|
|
269
280
|
searched[id] = true
|
270
281
|
q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
|
271
282
|
q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
|
272
283
|
end
|
273
|
-
pending = []
|
284
|
+
pending = pending[lim .. -1]
|
274
285
|
|
275
286
|
q = build_query :qobj => q
|
276
287
|
|
@@ -313,7 +324,7 @@ EOS
|
|
313
324
|
"date" => Time.at(doc[:date].to_i),
|
314
325
|
"subject" => unwrap_subj(doc[:subject]),
|
315
326
|
"from" => doc[:from],
|
316
|
-
"to" => doc[:to],
|
327
|
+
"to" => doc[:to].split(/\s+/).join(", "), # reformat
|
317
328
|
"message-id" => doc[:message_id],
|
318
329
|
"references" => doc[:refs].split(/\s+/).map { |x| "<#{x}>" }.join(" "),
|
319
330
|
}
|
@@ -382,19 +393,67 @@ protected
|
|
382
393
|
|
383
394
|
## do any specialized parsing
|
384
395
|
## returns nil and flashes error message if parsing failed
|
385
|
-
def parse_user_query_string
|
386
|
-
|
396
|
+
def parse_user_query_string s
|
397
|
+
extraopts = {}
|
398
|
+
|
399
|
+
## this is a little hacky, but it works, at least until ferret changes
|
400
|
+
## its api. we parse the user query string with ferret twice: the first
|
401
|
+
## time we just turn the resulting object back into a string, which has
|
402
|
+
## the next effect of transforming the original string into a nice
|
403
|
+
## normalized form with + and - instead of AND, OR, etc. then we do some
|
404
|
+
## string substitutions which depend on this normalized form, re-parse
|
405
|
+
## the string with Ferret, and return the resulting query object.
|
406
|
+
|
407
|
+
norms = @qparser.parse(s).to_s
|
408
|
+
Redwood::log "normalized #{s.inspect} to #{norms.inspect}" unless s == norms
|
409
|
+
|
410
|
+
subs = norms.gsub(/\b(to|from):(\S+)\b/) do
|
387
411
|
field, name = $1, $2
|
388
412
|
if(p = ContactManager.contact_for(name))
|
389
413
|
[field, p.email]
|
414
|
+
elsif name == "me"
|
415
|
+
[field, "(" + AccountManager.user_emails.join("||") + ")"]
|
390
416
|
else
|
391
417
|
[field, name]
|
392
418
|
end.join(":")
|
393
419
|
end
|
394
|
-
|
420
|
+
|
421
|
+
## if we see a label:deleted or a label:spam term anywhere in the query
|
422
|
+
## string, we set the extra load_spam or load_deleted options to true.
|
423
|
+
## bizarre? well, because the query allows arbitrary parenthesized boolean
|
424
|
+
## expressions, without fully parsing the query, we can't tell whether
|
425
|
+
## the user is explicitly directing us to search spam messages or not.
|
426
|
+
## e.g. if the string is -(-(-(-(-label:spam)))), does the user want to
|
427
|
+
## search spam messages or not?
|
428
|
+
##
|
429
|
+
## so, we rely on the fact that turning these extra options ON turns OFF
|
430
|
+
## the adding of "-label:deleted" or "-label:spam" terms at the very
|
431
|
+
## final stage of query processing. if the user wants to search spam
|
432
|
+
## messages, not adding that is the right thing; if he doesn't want to
|
433
|
+
## search spam messages, then not adding it won't have any effect.
|
434
|
+
extraopts[:load_spam] = true if subs =~ /\blabel:spam\b/
|
435
|
+
extraopts[:load_deleted] = true if subs =~ /\blabel:deleted\b/
|
436
|
+
|
437
|
+
## gmail style "is" operator
|
438
|
+
subs = subs.gsub(/\b(is):(\S+)\b/) do
|
439
|
+
field, label = $1, $2
|
440
|
+
case label
|
441
|
+
when "read"
|
442
|
+
"-label:unread"
|
443
|
+
when "spam"
|
444
|
+
extraopts[:load_spam] = true
|
445
|
+
"label:spam"
|
446
|
+
when "deleted"
|
447
|
+
extraopts[:load_deleted] = true
|
448
|
+
"label:deleted"
|
449
|
+
else
|
450
|
+
"label:#{$2}"
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
395
454
|
if $have_chronic
|
396
455
|
chronic_failure = false
|
397
|
-
|
456
|
+
subs = subs.gsub(/\b(before|on|in|during|after):(\((.+?)\)\B|(\S+)\b)/) do
|
398
457
|
break if chronic_failure
|
399
458
|
field, datestr = $1, ($3 || $4)
|
400
459
|
realdate = Chronic.parse(datestr, :guess => false, :context => :none)
|
@@ -411,15 +470,19 @@ protected
|
|
411
470
|
"date:(<= #{sprintf "%012d", realdate.end.to_i}) date:(>= #{sprintf "%012d", realdate.begin.to_i})"
|
412
471
|
end
|
413
472
|
else
|
414
|
-
BufferManager.flash "
|
473
|
+
BufferManager.flash "Can't understand date #{datestr.inspect}!"
|
415
474
|
chronic_failure = true
|
416
475
|
end
|
417
476
|
end
|
418
|
-
|
477
|
+
subs = nil if chronic_failure
|
419
478
|
end
|
420
479
|
|
421
|
-
Redwood::log "translated #{
|
422
|
-
|
480
|
+
Redwood::log "translated #{norms.inspect} to #{subs.inspect}" unless subs == norms
|
481
|
+
if subs
|
482
|
+
[@qparser.parse(subs), extraopts]
|
483
|
+
else
|
484
|
+
nil
|
485
|
+
end
|
423
486
|
end
|
424
487
|
|
425
488
|
def build_query opts
|