sup 0.11 → 0.12

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.

@@ -0,0 +1,92 @@
1
+ require 'sup/protocol'
2
+
3
+ module Redwood
4
+
5
+ class Client < EM::P::RedwoodClient
6
+ def initialize *a
7
+ @next_tag = 1
8
+ @cbs = {}
9
+ super *a
10
+ end
11
+
12
+ def mktag &b
13
+ @next_tag.tap do |x|
14
+ @cbs[x] = b
15
+ @next_tag += 1
16
+ end
17
+ end
18
+
19
+ def rmtag tag
20
+ @cbs.delete tag
21
+ end
22
+
23
+ def query qstr, offset, limit, raw, &b
24
+ tag = mktag do |type,tag,args|
25
+ if type == 'message'
26
+ b.call args
27
+ else
28
+ fail unless type == 'done'
29
+ b.call nil
30
+ rmtag tag
31
+ end
32
+ end
33
+ send_message 'query', tag,
34
+ 'query' => qstr,
35
+ 'offset' => offset,
36
+ 'limit' => limit,
37
+ 'raw' => raw
38
+ end
39
+
40
+ def count qstr, &b
41
+ tag = mktag do |type,tag,args|
42
+ b.call args['count']
43
+ rmtag tag
44
+ end
45
+ send_message 'count', tag,
46
+ 'query' => qstr
47
+ end
48
+
49
+ def label qstr, add, remove, &b
50
+ tag = mktag do |type,tag,args|
51
+ b.call
52
+ rmtag tag
53
+ end
54
+ send_message 'label', tag,
55
+ 'query' => qstr,
56
+ 'add' => add,
57
+ 'remove' => remove
58
+ end
59
+
60
+ def add raw, labels, &b
61
+ tag = mktag do |type,tag,args|
62
+ b.call
63
+ rmtag tag
64
+ end
65
+ send_message 'add', tag,
66
+ 'raw' => raw,
67
+ 'labels' => labels
68
+ end
69
+
70
+ def thread msg_id, raw, &b
71
+ tag = mktag do |type,tag,args|
72
+ if type == 'message'
73
+ b.call args
74
+ else
75
+ fail unless type == 'done'
76
+ b.call nil
77
+ rmtag tag
78
+ end
79
+ end
80
+
81
+ send_message 'thread', tag,
82
+ 'message_id' => msg_id,
83
+ 'raw' => raw
84
+ end
85
+
86
+ def receive_message type, tag, args
87
+ cb = @cbs[tag] or fail "invalid tag #{tag.inspect}"
88
+ cb[type, tag, args]
89
+ end
90
+ end
91
+
92
+ end
@@ -45,14 +45,15 @@ EOS
45
45
 
46
46
  sig_fn = Tempfile.new "redwood.signature"; sig_fn.close
47
47
 
48
- message = run_gpg "--output #{sig_fn.path} --yes --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}", :interactive => true
48
+ sign_user_opts = gen_sign_user_opts from
49
+ message = run_gpg "--output #{sig_fn.path} --yes --armor --detach-sign --textmode --digest-algo sha256 #{sign_user_opts} #{payload_fn.path}", :interactive => true
49
50
  unless $?.success?
50
51
  info "Error while running gpg: #{message}"
51
52
  raise Error, "GPG command failed. See log for details."
52
53
  end
53
54
 
54
55
  envelope = RMail::Message.new
55
- envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha1'
56
+ envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha256'
56
57
 
57
58
  envelope.add_part payload
58
59
  signature = RMail::Message.make_attachment IO.read(sig_fn.path), "application/pgp-signature", nil, "signature.asc"
@@ -68,7 +69,8 @@ EOS
68
69
  encrypted_fn = Tempfile.new "redwood.encrypted"; encrypted_fn.close
69
70
 
70
71
  recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ")
71
- sign_opts = sign ? "--sign --local-user '#{from}'" : ""
72
+ sign_opts = ""
73
+ sign_opts = "--sign --digest-algo sha256 " + gen_sign_user_opts(from) if sign
72
74
  message = run_gpg "--output #{encrypted_fn.path} --yes --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}", :interactive => true
73
75
  unless $?.success?
74
76
  info "Error while running gpg: #{message}"
@@ -86,7 +88,7 @@ EOS
86
88
  control.body = "Version: 1\n"
87
89
 
88
90
  envelope = RMail::Message.new
89
- envelope.header["Content-Type"] = 'multipart/encrypted; protocol="application/pgp-encrypted"'
91
+ envelope.header["Content-Type"] = 'multipart/encrypted; protocol=application/pgp-encrypted'
90
92
 
91
93
  envelope.add_part control
92
94
  envelope.add_part encrypted_payload
@@ -97,43 +99,57 @@ EOS
97
99
  encrypt from, to, payload, true
98
100
  end
99
101
 
100
- def verify payload, signature # both RubyMail::Message objects
101
- return unknown_status(cant_find_binary) unless @cmd
102
-
103
- payload_fn = Tempfile.new "redwood.payload"
104
- payload_fn.write format_payload(payload)
105
- payload_fn.close
106
-
107
- signature_fn = Tempfile.new "redwood.signature"
108
- signature_fn.write signature.decode
109
- signature_fn.close
110
-
111
- output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}"
102
+ def verified_ok? output, rc
112
103
  output_lines = output.split(/\n/)
113
104
 
114
105
  if output =~ /^gpg: (.* signature from .*$)/
115
- if $? == 0
106
+ if rc == 0
116
107
  Chunk::CryptoNotice.new :valid, $1, output_lines
117
108
  else
118
109
  Chunk::CryptoNotice.new :invalid, $1, output_lines
119
110
  end
111
+ elsif output_lines.length == 0 && rc == 0
112
+ # the message wasn't signed
113
+ Chunk::CryptoNotice.new :valid, "Encrypted message wasn't signed", output_lines
120
114
  else
121
115
  unknown_status output_lines
122
116
  end
123
117
  end
124
118
 
119
+ def verify payload, signature, detached=true # both RubyMail::Message objects
120
+ return unknown_status(cant_find_binary) unless @cmd
121
+
122
+ if detached
123
+ payload_fn = Tempfile.new "redwood.payload"
124
+ payload_fn.write format_payload(payload)
125
+ payload_fn.close
126
+ end
127
+
128
+ signature_fn = Tempfile.new "redwood.signature"
129
+ signature_fn.write signature.decode
130
+ signature_fn.close
131
+
132
+ if detached
133
+ output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}"
134
+ else
135
+ output = run_gpg "--verify #{signature_fn.path}"
136
+ end
137
+
138
+ self.verified_ok? output, $?
139
+ end
140
+
125
141
  ## returns decrypted_message, status, desc, lines
126
- def decrypt payload # a RubyMail::Message object
142
+ def decrypt payload, armor=false # a RubyMail::Message object
127
143
  return unknown_status(cant_find_binary) unless @cmd
128
144
 
129
- payload_fn = Tempfile.new "redwood.payload"
145
+ payload_fn = Tempfile.new(["redwood.payload", ".asc"])
130
146
  payload_fn.write payload.to_s
131
147
  payload_fn.close
132
148
 
133
149
  output_fn = Tempfile.new "redwood.output"
134
150
  output_fn.close
135
151
 
136
- message = run_gpg "--output #{output_fn.path} --yes --decrypt #{payload_fn.path}", :interactive => true
152
+ message = run_gpg "--output #{output_fn.path} --skip-verify --yes --decrypt #{payload_fn.path}", :interactive => true
137
153
 
138
154
  unless $?.success?
139
155
  info "Error while running gpg: #{message}"
@@ -143,34 +159,43 @@ EOS
143
159
  output = IO.read output_fn.path
144
160
  output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding
145
161
 
146
- ## there's probably a better way to do this, but we're using the output to
147
- ## look for a valid signature being present.
148
-
149
- sig = case message
150
- when /^gpg: (Good signature from .*$)/i
151
- Chunk::CryptoNotice.new :valid, $1, message.split("\n")
152
- when /^gpg: (Bad signature from .*$)/i
153
- Chunk::CryptoNotice.new :invalid, $1, message.split("\n")
154
- end
155
-
156
- # This is gross. This decrypted payload could very well be a multipart
157
- # element itself, as opposed to a simple payload. For example, a
158
- # multipart/signed element, like those generated by Mutt when encrypting
159
- # and signing a message (instead of just clearsigning the body).
160
- # Supposedly, decrypted_payload being a multipart element ought to work
161
- # out nicely because Message::multipart_encrypted_to_chunks() runs the
162
- # decrypted message through message_to_chunks() again to get any
163
- # children. However, it does not work as intended because these inner
164
- # payloads need not carry a MIME-Version header, yet they are fed to
165
- # RMail as a top-level message, for which the MIME-Version header is
166
- # required. This causes for the part not to be detected as multipart,
167
- # hence being shown as an attachment. If we detect this is happening,
168
- # we force the decrypted payload to be interpreted as MIME.
169
- msg = RMail::Parser.read output
170
- if msg.header.content_type =~ %r{^multipart/} && !msg.multipart?
171
- output = "MIME-Version: 1.0\n" + output
172
- output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding
162
+ ## check for a valid signature in an extra run because gpg aborts if the
163
+ ## signature cannot be verified (but it is still able to decrypt)
164
+ sigoutput = run_gpg "#{payload_fn.path}"
165
+ sig = self.verified_ok? sigoutput, $?
166
+
167
+ if armor
168
+ msg = RMail::Message.new
169
+ # Look for Charset, they are put before the base64 crypted part
170
+ charsets = payload.body.split("\n").grep(/^Charset:/)
171
+ if !charsets.empty? and charsets[0] =~ /^Charset: (.+)$/
172
+ output = Iconv.easy_decode($encoding, $1, output)
173
+ end
174
+ msg.body = output
175
+ else
176
+ # It appears that some clients use Windows new lines - CRLF - but RMail
177
+ # splits the body and header on "\n\n". So to allow the parse below to
178
+ # succeed, we will convert the newlines to what RMail expects
179
+ output = output.gsub(/\r\n/, "\n")
180
+ # This is gross. This decrypted payload could very well be a multipart
181
+ # element itself, as opposed to a simple payload. For example, a
182
+ # multipart/signed element, like those generated by Mutt when encrypting
183
+ # and signing a message (instead of just clearsigning the body).
184
+ # Supposedly, decrypted_payload being a multipart element ought to work
185
+ # out nicely because Message::multipart_encrypted_to_chunks() runs the
186
+ # decrypted message through message_to_chunks() again to get any
187
+ # children. However, it does not work as intended because these inner
188
+ # payloads need not carry a MIME-Version header, yet they are fed to
189
+ # RMail as a top-level message, for which the MIME-Version header is
190
+ # required. This causes for the part not to be detected as multipart,
191
+ # hence being shown as an attachment. If we detect this is happening,
192
+ # we force the decrypted payload to be interpreted as MIME.
173
193
  msg = RMail::Parser.read output
194
+ if msg.header.content_type =~ %r{^multipart/} && !msg.multipart?
195
+ output = "MIME-Version: 1.0\n" + output
196
+ output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding
197
+ msg = RMail::Parser.read output
198
+ end
174
199
  end
175
200
  notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
176
201
  [notice, sig, msg]
@@ -189,12 +214,29 @@ private
189
214
  ## here's where we munge rmail output into the format that signed/encrypted
190
215
  ## PGP/GPG messages should be
191
216
  def format_payload payload
192
- payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
217
+ payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n")
218
+ end
219
+
220
+ # logic is:
221
+ # if gpgkey set for this account, then use that
222
+ # elsif only one account, then leave blank so gpg default will be user
223
+ # else set --local-user from_email_address
224
+ def gen_sign_user_opts from
225
+ account = AccountManager.account_for from
226
+ if !account.gpgkey.nil?
227
+ opts = "--local-user '#{account.gpgkey}'"
228
+ elsif AccountManager.user_emails.length == 1
229
+ # only one account
230
+ opts = ""
231
+ else
232
+ opts = "--local-user '#{from}'"
233
+ end
234
+ opts
193
235
  end
194
236
 
195
237
  def run_gpg args, opts={}
196
238
  args = HookManager.run("gpg-args", { :args => args }) || args
197
- cmd = "#{@cmd} #{args}"
239
+ cmd = "LC_MESSAGES=C #{@cmd} #{args}"
198
240
  if opts[:interactive] && BufferManager.instantiated?
199
241
  output_fn = Tempfile.new "redwood.output"
200
242
  output_fn.close
@@ -11,20 +11,13 @@ class DraftManager
11
11
 
12
12
  def self.source_name; "sup://drafts"; end
13
13
  def self.source_id; 9999; end
14
- def new_source; @source = Recoverable.new DraftLoader.new; end
14
+ def new_source; @source = DraftLoader.new; end
15
15
 
16
16
  def write_draft
17
17
  offset = @source.gen_offset
18
18
  fn = @source.fn_for_offset offset
19
19
  File.open(fn, "w") { |f| yield f }
20
-
21
- my_message = nil
22
- PollManager.each_message_from(@source) do |m|
23
- PollManager.add_new_message m
24
- my_message = m
25
- end
26
-
27
- my_message
20
+ PollManager.poll_from @source
28
21
  end
29
22
 
30
23
  def discard m
@@ -37,31 +30,35 @@ end
37
30
 
38
31
  class DraftLoader < Source
39
32
  attr_accessor :dir
40
- yaml_properties :cur_offset
33
+ yaml_properties
41
34
 
42
- def initialize cur_offset=0
35
+ def initialize
43
36
  dir = Redwood::DRAFT_DIR
44
37
  Dir.mkdir dir unless File.exists? dir
45
- super DraftManager.source_name, cur_offset, true, false
38
+ super DraftManager.source_name, true, false
46
39
  @dir = dir
40
+ @cur_offset = 0
47
41
  end
48
42
 
49
43
  def id; DraftManager.source_id; end
50
44
  def to_s; DraftManager.source_name; end
51
45
  def uri; DraftManager.source_name; end
52
46
 
53
- def each
47
+ def poll
54
48
  ids = get_ids
55
49
  ids.each do |id|
56
- if id >= cur_offset
57
- self.cur_offset = id + 1
58
- yield [id, [:draft, :inbox]]
50
+ if id >= @cur_offset
51
+ @cur_offset = id + 1
52
+ yield :add,
53
+ :info => id,
54
+ :labels => [:draft, :inbox],
55
+ :progress => 0.0
59
56
  end
60
57
  end
61
58
  end
62
59
 
63
60
  def gen_offset
64
- i = cur_offset
61
+ i = 0
65
62
  while File.exists? fn_for_offset(i)
66
63
  i += 1
67
64
  end
@@ -61,10 +61,15 @@ class HookManager
61
61
 
62
62
  include Singleton
63
63
 
64
+ @descs = {}
65
+
66
+ class << self
67
+ attr_reader :descs
68
+ end
69
+
64
70
  def initialize dir
65
71
  @dir = dir
66
72
  @hooks = {}
67
- @descs = {}
68
73
  @contexts = {}
69
74
  @tags = {}
70
75
 
@@ -90,17 +95,17 @@ class HookManager
90
95
  result
91
96
  end
92
97
 
93
- def register name, desc
98
+ def self.register name, desc
94
99
  @descs[name] = desc
95
100
  end
96
101
 
97
102
  def print_hooks f=$stdout
98
103
  puts <<EOS
99
- Have #{@descs.size} registered hooks:
104
+ Have #{HookManager.descs.size} registered hooks:
100
105
 
101
106
  EOS
102
107
 
103
- @descs.sort.each do |name, desc|
108
+ HookManager.descs.sort.each do |name, desc|
104
109
  f.puts <<EOS
105
110
  #{name}
106
111
  #{"-" * name.length}
@@ -112,7 +117,7 @@ EOS
112
117
 
113
118
  def enabled? name; !hook_for(name).nil? end
114
119
 
115
- def clear; @hooks.clear; end
120
+ def clear; @hooks.clear; BufferManager.flash "Hooks cleared" end
116
121
  def clear_one k; @hooks.delete k; end
117
122
 
118
123
  private
@@ -3,6 +3,7 @@ ENV["XAPIAN_FLUSH_THRESHOLD"] = "1000"
3
3
  require 'xapian'
4
4
  require 'set'
5
5
  require 'fileutils'
6
+ require 'monitor'
6
7
 
7
8
  begin
8
9
  require 'chronic'
@@ -21,7 +22,7 @@ class Index
21
22
  include InteractiveLock
22
23
 
23
24
  STEM_LANGUAGE = "english"
24
- INDEX_VERSION = '2'
25
+ INDEX_VERSION = '4'
25
26
 
26
27
  ## dates are converted to integers for xapian, and are used for document ids,
27
28
  ## so we must ensure they're reasonably valid. this typically only affect
@@ -48,6 +49,7 @@ EOS
48
49
 
49
50
  def initialize dir=BASE_DIR
50
51
  @dir = dir
52
+ FileUtils.mkdir_p @dir
51
53
  @lock = Lockfile.new lockfile, :retries => 0, :max_age => nil
52
54
  @sync_worker = nil
53
55
  @sync_queue = Queue.new
@@ -104,15 +106,16 @@ EOS
104
106
  @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_OPEN)
105
107
  db_version = @xapian.get_metadata 'version'
106
108
  db_version = '0' if db_version.empty?
107
- if db_version == '1'
108
- info "Upgrading index format 1 to 2"
109
+ if false
110
+ info "Upgrading index format #{db_version} to #{INDEX_VERSION}"
109
111
  @xapian.set_metadata 'version', INDEX_VERSION
110
112
  elsif db_version != INDEX_VERSION
111
- fail "This Sup version expects a v#{INDEX_VERSION} index, but you have an existing v#{db_version} index. Please downgrade to your previous version and dump your labels before upgrading to this version (then run sup-sync --restore)."
113
+ fail "This Sup version expects a v#{INDEX_VERSION} index, but you have an existing v#{db_version} index. Please run sup-dump to save your labels, move #{path} out of the way, and run sup-sync --restore."
112
114
  end
113
115
  else
114
116
  @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_CREATE)
115
117
  @xapian.set_metadata 'version', INDEX_VERSION
118
+ @xapian.set_metadata 'rescue-version', '0'
116
119
  end
117
120
  @enquire = Xapian::Enquire.new @xapian
118
121
  @enquire.weighting_scheme = Xapian::BoolWeight.new
@@ -193,11 +196,15 @@ EOS
193
196
  entry = synchronize { get_entry id }
194
197
  return unless entry
195
198
 
196
- source = SourceManager[entry[:source_id]]
197
- raise "invalid source #{entry[:source_id]}" unless source
199
+ locations = entry[:locations].map do |source_id,source_info|
200
+ source = SourceManager[source_id]
201
+ raise "invalid source #{source_id}" unless source
202
+ Location.new source, source_info
203
+ end
198
204
 
199
- m = Message.new :source => source, :source_info => entry[:source_info],
200
- :labels => entry[:labels], :snippet => entry[:snippet]
205
+ m = Message.new :locations => locations,
206
+ :labels => entry[:labels],
207
+ :snippet => entry[:snippet]
201
208
 
202
209
  mk_person = lambda { |x| Person.new(*x.reverse!) }
203
210
  entry[:from] = mk_person[entry[:from]]
@@ -260,6 +267,26 @@ EOS
260
267
  synchronize { get_entry(id)[:source_id] }
261
268
  end
262
269
 
270
+ ## Yields each tearm in the index that starts with prefix
271
+ def each_prefixed_term prefix
272
+ term = @xapian._dangerous_allterms_begin prefix
273
+ lastTerm = @xapian._dangerous_allterms_end prefix
274
+ until term.equals lastTerm
275
+ yield term.term
276
+ term.next
277
+ end
278
+ nil
279
+ end
280
+
281
+ ## Yields (in lexicographical order) the source infos of all locations from
282
+ ## the given source with the given source_info prefix
283
+ def each_source_info source_id, prefix='', &b
284
+ prefix = mkterm :location, source_id, prefix
285
+ each_prefixed_term prefix do |x|
286
+ yield x[prefix.length..-1]
287
+ end
288
+ end
289
+
263
290
  class ParseError < StandardError; end
264
291
 
265
292
  ## parse a query string from the user. returns a query object
@@ -288,22 +315,6 @@ EOS
288
315
  end
289
316
  end
290
317
 
291
- ## if we see a label:deleted or a label:spam term anywhere in the query
292
- ## string, we set the extra load_spam or load_deleted options to true.
293
- ## bizarre? well, because the query allows arbitrary parenthesized boolean
294
- ## expressions, without fully parsing the query, we can't tell whether
295
- ## the user is explicitly directing us to search spam messages or not.
296
- ## e.g. if the string is -(-(-(-(-label:spam)))), does the user want to
297
- ## search spam messages or not?
298
- ##
299
- ## so, we rely on the fact that turning these extra options ON turns OFF
300
- ## the adding of "-label:deleted" or "-label:spam" terms at the very
301
- ## final stage of query processing. if the user wants to search spam
302
- ## messages, not adding that is the right thing; if he doesn't want to
303
- ## search spam messages, then not adding it won't have any effect.
304
- query[:load_spam] = true if subs =~ /\blabel:spam\b/
305
- query[:load_deleted] = true if subs =~ /\blabel:deleted\b/
306
-
307
318
  ## gmail style "is" operator
308
319
  subs = subs.gsub(/\b(is|has):(\S+)\b/) do
309
320
  field, label = $1, $2
@@ -321,6 +332,29 @@ EOS
321
332
  end
322
333
  end
323
334
 
335
+ ## labels are stored lower-case in the index
336
+ subs = subs.gsub(/\blabel:(\S+)\b/) do
337
+ label = $1
338
+ "label:#{label.downcase}"
339
+ end
340
+
341
+ ## if we see a label:deleted or a label:spam term anywhere in the query
342
+ ## string, we set the extra load_spam or load_deleted options to true.
343
+ ## bizarre? well, because the query allows arbitrary parenthesized boolean
344
+ ## expressions, without fully parsing the query, we can't tell whether
345
+ ## the user is explicitly directing us to search spam messages or not.
346
+ ## e.g. if the string is -(-(-(-(-label:spam)))), does the user want to
347
+ ## search spam messages or not?
348
+ ##
349
+ ## so, we rely on the fact that turning these extra options ON turns OFF
350
+ ## the adding of "-label:deleted" or "-label:spam" terms at the very
351
+ ## final stage of query processing. if the user wants to search spam
352
+ ## messages, not adding that is the right thing; if he doesn't want to
353
+ ## search spam messages, then not adding it won't have any effect.
354
+ query[:load_spam] = true if subs =~ /\blabel:spam\b/
355
+ query[:load_deleted] = true if subs =~ /\blabel:deleted\b/
356
+ query[:load_killed] = true if subs =~ /\blabel:killed\b/
357
+
324
358
  ## gmail style attachments "filename" and "filetype" searches
325
359
  subs = subs.gsub(/\b(filename|filetype):(\((.+?)\)\B|(\S+)\b)/) do
326
360
  field, name = $1, ($3 || $4)
@@ -453,6 +487,7 @@ EOS
453
487
  'id' => 'Q',
454
488
  'thread' => 'H',
455
489
  'ref' => 'R',
490
+ 'location' => 'J',
456
491
  }
457
492
 
458
493
  PREFIX = NORMAL_PREFIX.merge BOOLEAN_PREFIX
@@ -514,7 +549,7 @@ EOS
514
549
 
515
550
  def get_entry id
516
551
  return unless doc = find_doc(id)
517
- Marshal.load doc.data
552
+ doc.entry
518
553
  end
519
554
 
520
555
  def thread_killed? thread_id
@@ -547,6 +582,7 @@ EOS
547
582
  pos_terms.concat(labels.map { |l| mkterm(:label,l) })
548
583
  pos_terms << opts[:qobj] if opts[:qobj]
549
584
  pos_terms << mkterm(:source_id, opts[:source_id]) if opts[:source_id]
585
+ pos_terms << mkterm(:location, *opts[:location]) if opts[:location]
550
586
 
551
587
  if opts[:participants]
552
588
  participant_terms = opts[:participants].map { |p| [:from,:to].map { |d| mkterm(:email, d, (Redwood::Person === p) ? p.email : p) } }.flatten
@@ -575,8 +611,7 @@ EOS
575
611
 
576
612
  entry = {
577
613
  :message_id => m.id,
578
- :source_id => m.source.id,
579
- :source_info => m.source_info,
614
+ :locations => m.locations.map { |x| [x.source.id, x.info] },
580
615
  :date => truncate_date(m.date),
581
616
  :snippet => snippet,
582
617
  :labels => m.labels.to_a,
@@ -595,6 +630,7 @@ EOS
595
630
  index_message_static m, doc, entry
596
631
  end
597
632
 
633
+ index_message_locations doc, entry, old_entry
598
634
  index_message_threading doc, entry, old_entry
599
635
  index_message_labels doc, entry[:labels], (do_index_static ? [] : old_entry[:labels])
600
636
  doc.entry = entry
@@ -637,7 +673,6 @@ EOS
637
673
  doc.add_term mkterm(:date, m.date) if m.date
638
674
  doc.add_term mkterm(:type, 'mail')
639
675
  doc.add_term mkterm(:msgid, m.id)
640
- doc.add_term mkterm(:source_id, m.source.id)
641
676
  m.attachments.each do |a|
642
677
  a =~ /\.(\w+)$/ or next
643
678
  doc.add_term mkterm(:attachment_extension, $1)
@@ -654,6 +689,13 @@ EOS
654
689
  doc.add_value DATE_VALUENO, date_value
655
690
  end
656
691
 
692
+ def index_message_locations doc, entry, old_entry
693
+ old_entry[:locations].map { |x| x[0] }.uniq.each { |x| doc.remove_term mkterm(:source_id, x) } if old_entry
694
+ entry[:locations].map { |x| x[0] }.uniq.each { |x| doc.add_term mkterm(:source_id, x) }
695
+ old_entry[:locations].each { |x| (doc.remove_term mkterm(:location, *x) rescue nil) } if old_entry
696
+ entry[:locations].each { |x| doc.add_term mkterm(:location, *x) }
697
+ end
698
+
657
699
  def index_message_labels doc, new_labels, old_labels
658
700
  return if new_labels == old_labels
659
701
  added = new_labels.to_a - old_labels.to_a
@@ -716,6 +758,8 @@ EOS
716
758
  end + args[1].to_s.downcase
717
759
  when :source_id
718
760
  PREFIX['source_id'] + args[0].to_s.downcase
761
+ when :location
762
+ PREFIX['location'] + [args[0]].pack('n') + args[1].to_s
719
763
  when :attachment_extension
720
764
  PREFIX['attachment_extension'] + args[0].to_s.downcase
721
765
  when :msgid, :ref, :thread