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.
- data/CONTRIBUTORS +16 -5
- data/History.txt +9 -0
- data/ReleaseNotes +9 -0
- data/bin/sup +9 -24
- data/bin/sup-add +3 -3
- data/bin/sup-config +1 -1
- data/bin/sup-dump +22 -9
- data/bin/sup-recover-sources +1 -11
- data/bin/sup-sync +65 -130
- data/bin/sup-sync-back +6 -5
- data/bin/sup-tweak-labels +5 -6
- data/lib/sup.rb +89 -71
- data/lib/sup/account.rb +3 -2
- data/lib/sup/buffer.rb +28 -16
- data/lib/sup/client.rb +92 -0
- data/lib/sup/crypto.rb +91 -49
- data/lib/sup/draft.rb +14 -17
- data/lib/sup/hook.rb +10 -5
- data/lib/sup/index.rb +72 -28
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +55 -112
- data/lib/sup/mbox.rb +151 -6
- data/lib/sup/message-chunks.rb +20 -4
- data/lib/sup/message.rb +183 -76
- data/lib/sup/modes/compose-mode.rb +2 -1
- data/lib/sup/modes/console-mode.rb +4 -1
- data/lib/sup/modes/edit-message-mode.rb +50 -5
- data/lib/sup/modes/line-cursor-mode.rb +1 -0
- data/lib/sup/modes/reply-mode.rb +17 -11
- data/lib/sup/modes/thread-index-mode.rb +10 -9
- data/lib/sup/modes/thread-view-mode.rb +48 -2
- data/lib/sup/poll.rb +56 -60
- data/lib/sup/protocol.rb +161 -0
- data/lib/sup/sent.rb +8 -11
- data/lib/sup/server.rb +116 -0
- data/lib/sup/source.rb +15 -33
- data/lib/sup/thread.rb +6 -0
- data/lib/sup/util.rb +44 -39
- metadata +126 -88
- data/lib/sup/connection.rb +0 -63
- data/lib/sup/imap.rb +0 -349
- data/lib/sup/mbox/loader.rb +0 -180
- data/lib/sup/mbox/ssh-file.rb +0 -254
- data/lib/sup/mbox/ssh-loader.rb +0 -74
data/lib/sup/client.rb
ADDED
@@ -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
|
data/lib/sup/crypto.rb
CHANGED
@@ -45,14 +45,15 @@ EOS
|
|
45
45
|
|
46
46
|
sig_fn = Tempfile.new "redwood.signature"; sig_fn.close
|
47
47
|
|
48
|
-
|
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-
|
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 =
|
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=
|
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
|
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
|
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
|
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
|
-
##
|
147
|
-
##
|
148
|
-
|
149
|
-
sig =
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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")
|
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
|
data/lib/sup/draft.rb
CHANGED
@@ -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 =
|
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
|
33
|
+
yaml_properties
|
41
34
|
|
42
|
-
def initialize
|
35
|
+
def initialize
|
43
36
|
dir = Redwood::DRAFT_DIR
|
44
37
|
Dir.mkdir dir unless File.exists? dir
|
45
|
-
super DraftManager.source_name,
|
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
|
47
|
+
def poll
|
54
48
|
ids = get_ids
|
55
49
|
ids.each do |id|
|
56
|
-
if id >= cur_offset
|
57
|
-
|
58
|
-
yield
|
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 =
|
61
|
+
i = 0
|
65
62
|
while File.exists? fn_for_offset(i)
|
66
63
|
i += 1
|
67
64
|
end
|
data/lib/sup/hook.rb
CHANGED
@@ -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 #{
|
104
|
+
Have #{HookManager.descs.size} registered hooks:
|
100
105
|
|
101
106
|
EOS
|
102
107
|
|
103
|
-
|
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
|
data/lib/sup/index.rb
CHANGED
@@ -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 = '
|
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
|
108
|
-
info "Upgrading index format
|
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
|
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
|
-
|
197
|
-
|
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 :
|
200
|
-
:labels => entry[:labels],
|
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
|
-
|
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
|
-
:
|
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
|