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.

Files changed (52) hide show
  1. data/HACKING +31 -9
  2. data/History.txt +7 -0
  3. data/Manifest.txt +2 -0
  4. data/Rakefile +9 -5
  5. data/bin/sup +81 -57
  6. data/bin/sup-config +1 -1
  7. data/bin/sup-sync +3 -0
  8. data/bin/sup-tweak-labels +127 -0
  9. data/doc/TODO +23 -12
  10. data/lib/sup.rb +13 -11
  11. data/lib/sup/account.rb +25 -12
  12. data/lib/sup/buffer.rb +61 -41
  13. data/lib/sup/colormap.rb +2 -0
  14. data/lib/sup/contact.rb +28 -18
  15. data/lib/sup/crypto.rb +86 -31
  16. data/lib/sup/draft.rb +12 -6
  17. data/lib/sup/horizontal-selector.rb +47 -0
  18. data/lib/sup/imap.rb +50 -37
  19. data/lib/sup/index.rb +76 -13
  20. data/lib/sup/keymap.rb +27 -8
  21. data/lib/sup/maildir.rb +1 -1
  22. data/lib/sup/mbox/loader.rb +1 -1
  23. data/lib/sup/message-chunks.rb +43 -15
  24. data/lib/sup/message.rb +67 -31
  25. data/lib/sup/mode.rb +40 -9
  26. data/lib/sup/modes/completion-mode.rb +1 -1
  27. data/lib/sup/modes/compose-mode.rb +3 -3
  28. data/lib/sup/modes/contact-list-mode.rb +12 -8
  29. data/lib/sup/modes/edit-message-mode.rb +100 -36
  30. data/lib/sup/modes/file-browser-mode.rb +1 -0
  31. data/lib/sup/modes/forward-mode.rb +43 -8
  32. data/lib/sup/modes/inbox-mode.rb +8 -5
  33. data/lib/sup/modes/label-search-results-mode.rb +12 -1
  34. data/lib/sup/modes/line-cursor-mode.rb +4 -7
  35. data/lib/sup/modes/reply-mode.rb +59 -54
  36. data/lib/sup/modes/resume-mode.rb +6 -6
  37. data/lib/sup/modes/scroll-mode.rb +4 -3
  38. data/lib/sup/modes/search-results-mode.rb +8 -5
  39. data/lib/sup/modes/text-mode.rb +19 -2
  40. data/lib/sup/modes/thread-index-mode.rb +109 -40
  41. data/lib/sup/modes/thread-view-mode.rb +180 -49
  42. data/lib/sup/person.rb +3 -3
  43. data/lib/sup/poll.rb +9 -8
  44. data/lib/sup/rfc2047.rb +7 -1
  45. data/lib/sup/sent.rb +1 -1
  46. data/lib/sup/tagger.rb +10 -4
  47. data/lib/sup/textfield.rb +7 -7
  48. data/lib/sup/thread.rb +86 -49
  49. data/lib/sup/update.rb +11 -0
  50. data/lib/sup/util.rb +74 -34
  51. data/test/test_message.rb +441 -0
  52. metadata +136 -117
@@ -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
- # returns a cryptosignature
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.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
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
- cmd = "#{@cmd} --verify #{signature_fn.path} #{payload_fn.path} 2> /dev/null"
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 gpg_output =~ /^gpg: (.* signature from .*$)/
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
- # returns decrypted_message, status, desc, lines
53
- def decrypt payload # RubyMail::Message objects
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
- cmd = "#{@cmd} --decrypt #{payload_fn.path} 2> /dev/null"
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 $? == 0 # successful decryption
118
+ if $?.success?
76
119
  decrypted_payload, sig_lines =
77
- if gpg_output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
120
+ if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
78
121
  [$1, $2]
79
122
  else
80
- [gpg_output, nil]
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", gpg_output.split("\n")
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 or pgp binary in path"]
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
@@ -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, :add, m
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 mid
34
- docid, entry = Index.load_entry_for_id mid
35
- raise ArgumentError, "can't find entry for draft: #{mid.inspect}" unless entry
36
- raise ArgumentError, "not a draft: source id #{entry[:source_id].inspect}, should be #{DraftManager.source_id.inspect} for #{mid.inspect} / docno #{docid}" unless entry[:source_id].to_i == DraftManager.source_id
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, :delete, mid
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
@@ -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
- ## dunces designed this shit.
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
- ## it turns out the uids can be invalidated every time the
14
- ## 'uidvalidity' value changes on the server, and 'uidvalidity' can
15
- ## change without restriction. it can change any time you log in. it
16
- ## can change EVERY time you log in. of course the imap spec "strongly
17
- ## recommends" that it never change, but there's nothing to stop
18
- ## people from just setting it to the current timestamp, and in fact
19
- ## that's exactly what the one imap server i have at my disposal
20
- ## does. thus the so-called uids are absolutely useless and imap
21
- ## provides no cross-session way of uniquely identifying a
22
- ## message. but thanks for the "strong recommendation", guys!
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
- ## are no collisions. ho ho! a perfectly reasonable solution!
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 times.
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
- ## of the spec actually happens in practice. you'll request flags for
37
- ## one message, and get it interspersed with a random bunch of flags
38
- ## for some other messages, including a different set of flags for the
39
- ## same message! totally ok by the imap spec. totally retarded by any
40
- ## other metric.
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
- if path =~ /inbox/i
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
- ## if an exception occurs, it will catch it and re-raise it on the
213
- ## calling thread. but i can't seem to catch that exception, so
214
- ## i've resorted to initializing it in its own thread. surely
215
- ## there's a better way.
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
- ## fails with a NO response, the client may try another", in
225
- ## practice it seems like they can also send a BAD response.
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
- raise OutOfSyncSourceError, "IMAP message mismatch: requested #{id}, got #{got_id}." unless got_id == id
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
@@ -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 => m.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(Message.normalize_subj(m.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.each do |id|
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 str
386
- result = str.gsub(/\b(to|from):(\S+)\b/) do
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
- result = result.gsub(/\b(before|on|in|after):(\((.+?)\)\B|(\S+)\b)/) do
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 "Don't understand date #{datestr.inspect}!"
473
+ BufferManager.flash "Can't understand date #{datestr.inspect}!"
415
474
  chronic_failure = true
416
475
  end
417
476
  end
418
- result = nil if chronic_failure
477
+ subs = nil if chronic_failure
419
478
  end
420
479
 
421
- Redwood::log "translated #{str.inspect} to #{result}" unless result == str
422
- @qparser.parse result if result
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