sup 0.20.0 → 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -1
  3. data/.travis.yml +11 -6
  4. data/CONTRIBUTORS +27 -15
  5. data/Gemfile +2 -1
  6. data/History.txt +84 -0
  7. data/README.md +26 -5
  8. data/Rakefile +0 -1
  9. data/ReleaseNotes +7 -0
  10. data/bin/sup +17 -30
  11. data/bin/sup-add +15 -16
  12. data/bin/sup-config +30 -45
  13. data/bin/sup-dump +2 -3
  14. data/bin/sup-import-dump +5 -6
  15. data/bin/sup-sync +3 -4
  16. data/bin/sup-sync-back-maildir +3 -4
  17. data/bin/sup-tweak-labels +6 -7
  18. data/contrib/colorpicker.rb +0 -2
  19. data/contrib/completion/_sup.bash +102 -0
  20. data/devel/profile.rb +0 -1
  21. data/ext/mkrf_conf_xapian.rb +1 -1
  22. data/lib/sup.rb +8 -8
  23. data/lib/sup/colormap.rb +5 -2
  24. data/lib/sup/contact.rb +4 -2
  25. data/lib/sup/crypto.rb +58 -16
  26. data/lib/sup/draft.rb +8 -8
  27. data/lib/sup/hook.rb +9 -9
  28. data/lib/sup/index.rb +20 -7
  29. data/lib/sup/label.rb +1 -1
  30. data/lib/sup/logger.rb +1 -1
  31. data/lib/sup/maildir.rb +2 -2
  32. data/lib/sup/mbox.rb +2 -2
  33. data/lib/sup/message.rb +26 -10
  34. data/lib/sup/message_chunks.rb +7 -4
  35. data/lib/sup/mode.rb +34 -28
  36. data/lib/sup/modes/contact_list_mode.rb +1 -0
  37. data/lib/sup/modes/edit_message_mode.rb +1 -1
  38. data/lib/sup/modes/forward_mode.rb +22 -3
  39. data/lib/sup/modes/line_cursor_mode.rb +1 -1
  40. data/lib/sup/modes/reply_mode.rb +3 -1
  41. data/lib/sup/modes/text_mode.rb +6 -1
  42. data/lib/sup/modes/thread_index_mode.rb +6 -2
  43. data/lib/sup/modes/thread_view_mode.rb +63 -18
  44. data/lib/sup/person.rb +68 -61
  45. data/lib/sup/search.rb +1 -1
  46. data/lib/sup/sent.rb +1 -1
  47. data/lib/sup/source.rb +1 -1
  48. data/lib/sup/util.rb +15 -94
  49. data/lib/sup/util/axe.rb +17 -0
  50. data/lib/sup/util/locale_fiddler.rb +24 -0
  51. data/lib/sup/util/ncurses.rb +3 -3
  52. data/lib/sup/version.rb +10 -1
  53. data/sup.gemspec +12 -10
  54. data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
  55. data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
  56. data/test/fixtures/blank-header-fields.eml +71 -0
  57. data/test/fixtures/contacts.txt +1 -0
  58. data/test/fixtures/mailing-list-header.eml +80 -0
  59. data/test/fixtures/malicious-attachment-names.eml +55 -0
  60. data/test/fixtures/missing-from-to.eml +18 -0
  61. data/test/{messages → fixtures}/missing-line.eml +0 -0
  62. data/test/fixtures/multi-part-2.eml +72 -0
  63. data/test/fixtures/multi-part.eml +61 -0
  64. data/test/fixtures/no-body.eml +18 -0
  65. data/test/fixtures/simple-message.eml +29 -0
  66. data/test/fixtures/text-attachments-with-charset.eml +46 -0
  67. data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
  68. data/test/gnupg_test_home/gpg.conf +2 -1
  69. data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
  70. data/test/gnupg_test_home/pubring.gpg +0 -0
  71. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  72. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  73. data/test/gnupg_test_home/regen_keys.sh +70 -16
  74. data/test/gnupg_test_home/secring.gpg +0 -0
  75. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -22
  76. data/test/integration/test_maildir.rb +1 -1
  77. data/test/integration/test_mbox.rb +1 -1
  78. data/test/test_crypto.rb +14 -2
  79. data/test/test_header_parsing.rb +1 -1
  80. data/test/test_helper.rb +6 -3
  81. data/test/test_message.rb +115 -341
  82. data/test/test_messages_dir.rb +4 -28
  83. data/test/test_yaml_regressions.rb +1 -1
  84. data/test/unit/test_contact.rb +33 -0
  85. data/test/unit/test_locale_fiddler.rb +15 -0
  86. data/test/unit/test_person.rb +37 -0
  87. data/test/unit/util/test_query.rb +10 -4
  88. data/test/unit/util/test_string.rb +6 -0
  89. metadata +107 -43
  90. data/test/gnupg_test_home/key1.gen +0 -15
  91. data/test/gnupg_test_home/key2.gen +0 -15
@@ -1,4 +1,3 @@
1
- require 'rubygems'
2
1
  require 'ruby-prof'
3
2
  require "redwood"
4
3
 
@@ -16,7 +16,7 @@ begin
16
16
  if !RbConfig::CONFIG['arch'].include?('openbsd')
17
17
  # update version in Gemfile as well
18
18
  name = "xapian-ruby"
19
- version = "~> 1.2.15"
19
+ version = "~> 1.2"
20
20
 
21
21
  begin
22
22
  # try to load gem
data/lib/sup.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'rubygems'
4
3
  require 'yaml'
5
4
  require 'zlib'
6
5
  require 'thread'
@@ -105,7 +104,7 @@ module Redwood
105
104
  o
106
105
  end
107
106
 
108
- mode = if File.exists? fn
107
+ mode = if File.exist? fn
109
108
  File.stat(fn).mode
110
109
  else
111
110
  0600
@@ -113,7 +112,7 @@ module Redwood
113
112
 
114
113
  if backup
115
114
  backup_fn = fn + '.bak'
116
- if File.exists?(fn) && File.size(fn) > 0
115
+ if File.exist?(fn) && File.size(fn) > 0
117
116
  File.open(backup_fn, "w", mode) do |f|
118
117
  File.open(fn, "r") { |old_f| FileUtils.copy_stream old_f, f }
119
118
  f.fsync
@@ -139,7 +138,7 @@ module Redwood
139
138
  end
140
139
 
141
140
  def load_yaml_obj fn, compress=false
142
- o = if File.exists? fn
141
+ o = if File.exist? fn
143
142
  if compress
144
143
  Zlib::GzipReader.open(fn) { |f| YAML::load f }
145
144
  else
@@ -180,7 +179,7 @@ module Redwood
180
179
  return if bypass_sync_check
181
180
 
182
181
  if $config[:sync_back_to_maildir]
183
- if not File.exists? Redwood::SYNC_OK_FN
182
+ if not File.exist? Redwood::SYNC_OK_FN
184
183
  Redwood.warn_syncback <<EOS
185
184
  It appears that the "sync_back_to_maildir" option has been changed
186
185
  from false to true since the last execution of sup.
@@ -191,14 +190,14 @@ Should I complain about this again? (Y/n)
191
190
  EOS
192
191
  File.open(Redwood::SYNC_OK_FN, 'w') {|f| f.write(Redwood::MAILDIR_SYNC_CHECK_SKIPPED) } if STDIN.gets.chomp.downcase == 'n'
193
192
  end
194
- elsif not $config[:sync_back_to_maildir] and File.exists? Redwood::SYNC_OK_FN
193
+ elsif not $config[:sync_back_to_maildir] and File.exist? Redwood::SYNC_OK_FN
195
194
  File.delete(Redwood::SYNC_OK_FN)
196
195
  end
197
196
  end
198
197
 
199
198
  def check_syncback_settings
200
199
  # don't check if syncback was never performed
201
- return unless File.exists? Redwood::SYNC_OK_FN
200
+ return unless File.exist? Redwood::SYNC_OK_FN
202
201
  active_sync_sources = File.readlines(Redwood::SYNC_OK_FN).collect { |e| e.strip }.find_all { |e| not e.empty? }
203
202
  return if active_sync_sources.length == 1 and active_sync_sources[0] == Redwood::MAILDIR_SYNC_CHECK_SKIPPED
204
203
  sources = SourceManager.sources
@@ -332,13 +331,14 @@ EOM
332
331
  :poll_interval => 300,
333
332
  :wrap_width => 0,
334
333
  :slip_rows => 0,
334
+ :indent_spaces => 2,
335
335
  :col_jump => 2,
336
336
  :stem_language => "english",
337
337
  :sync_back_to_maildir => false,
338
338
  :continuous_scroll => false,
339
339
  :always_edit_async => false,
340
340
  }
341
- if File.exists? filename
341
+ if File.exist? filename
342
342
  config = Redwood::load_yaml_obj filename
343
343
  abort "#{filename} is not a valid configuration file (it's a #{config.class}, not a hash)" unless config.is_a?(Hash)
344
344
  default_config.merge config
@@ -17,6 +17,9 @@ module Ncurses
17
17
 
18
18
  ## xterm 24-shade grayscale
19
19
  24.times { |x| color! "g#{x}", (16+6*6*6) + x }
20
+ elsif Ncurses::NUM_COLORS == -1
21
+ ## Terminal emulator doesn't appear to support colors
22
+ fail "sup must be run in a terminal with color support, please check your TERM variable."
20
23
  end
21
24
  end
22
25
 
@@ -186,13 +189,13 @@ class Colormap
186
189
  ## Try to use the user defined colors, in case of an error fall back
187
190
  ## to the default ones.
188
191
  def populate_colormap
189
- user_colors = if File.exists? Redwood::COLOR_FN
192
+ user_colors = if File.exist? Redwood::COLOR_FN
190
193
  debug "loading user colors from #{Redwood::COLOR_FN}"
191
194
  Redwood::load_yaml_obj Redwood::COLOR_FN
192
195
  end
193
196
 
194
197
  ## Set attachment sybmol to sane default for existing colorschemes
195
- if user_colors and user_colors.has_key? :to_me
198
+ if user_colors and user_colors.has_key? :to_me
196
199
  user_colors[:with_attachment] = user_colors[:to_me] unless user_colors.has_key? :with_attachment
197
200
  end
198
201
 
@@ -16,7 +16,7 @@ class ContactManager
16
16
  @a2p = {} # alias to person
17
17
  @e2p = {} # email to person
18
18
 
19
- if File.exists? fn
19
+ if File.exist? fn
20
20
  IO.foreach(fn) do |l|
21
21
  l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
22
22
  aalias, addr = $1, $2
@@ -29,11 +29,13 @@ class ContactManager
29
29
  def contacts_with_aliases; @a2p.values.uniq end
30
30
 
31
31
  def update_alias person, aalias=nil
32
+ ## Deleting old data if it exists
32
33
  old_aalias = @p2a[person]
33
- if(old_aalias != nil and old_aalias != "") # remove old alias
34
+ if old_aalias
34
35
  @a2p.delete old_aalias
35
36
  @e2p.delete person.email
36
37
  end
38
+ ## Update with new data
37
39
  @p2a[person] = aalias
38
40
  unless aalias.nil? || aalias.empty?
39
41
  @a2p[aalias] = person
@@ -10,11 +10,14 @@ class CryptoManager
10
10
 
11
11
  class Error < StandardError; end
12
12
 
13
- OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
14
- [:sign, "Sign"],
15
- [:sign_and_encrypt, "Sign and encrypt"],
16
- [:encrypt, "Encrypt only"]
17
- )
13
+ OUTGOING_MESSAGE_OPERATIONS = {
14
+ sign: "Sign",
15
+ sign_and_encrypt: "Sign and encrypt",
16
+ encrypt: "Encrypt only"
17
+ }
18
+
19
+ KEY_PATTERN = /(-----BEGIN PGP PUBLIC KEY BLOCK.*-----END PGP PUBLIC KEY BLOCK)/m
20
+ KEYSERVER_URL = "http://pool.sks-keyservers.net:11371/pks/lookup"
18
21
 
19
22
  HookManager.register "gpg-options", <<EOS
20
23
  Runs before gpg is called, allowing you to modify the options (most
@@ -124,18 +127,27 @@ EOS
124
127
  def sign from, to, payload
125
128
  return unknown_status(@not_working_reason) unless @not_working_reason.nil?
126
129
 
130
+ # We grab this from the GPG::Ctx below after signing, so that we can set
131
+ # micalg in Content-Type to match the hash algorithm GPG decided to use.
132
+ hash_algo = nil
133
+
127
134
  gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true}
128
135
  gpg_opts.merge!(gen_sign_user_opts(from))
129
136
  gpg_opts = HookManager.run("gpg-options",
130
137
  {:operation => "sign", :options => gpg_opts}) || gpg_opts
131
138
  begin
132
- if GPGME.respond_to?('detach_sign')
133
- sig = GPGME.detach_sign(format_payload(payload), gpg_opts)
134
- else
135
- crypto = GPGME::Crypto.new
136
- gpg_opts[:mode] = GPGME::SIG_MODE_DETACH
137
- sig = crypto.sign(format_payload(payload), gpg_opts).read
139
+ input = GPGME::Data.new(format_payload(payload))
140
+ output = GPGME::Data.new()
141
+ GPGME::Ctx.new(gpg_opts) do |ctx|
142
+ if gpg_opts[:signer]
143
+ signers = GPGME::Key.find(:secret, gpg_opts[:signer], :sign)
144
+ ctx.add_signer(*signers)
145
+ end
146
+ ctx.sign(input, output, GPGME::SIG_MODE_DETACH)
147
+ hash_algo = GPGME::hash_algo_name(ctx.sign_result.signatures[0].hash_algo)
138
148
  end
149
+ output.seek(0)
150
+ sig = output.read
139
151
  rescue GPGME::Error => exc
140
152
  raise Error, gpgme_exc_msg(exc.message)
141
153
  end
@@ -147,7 +159,7 @@ EOS
147
159
  end
148
160
 
149
161
  envelope = RMail::Message.new
150
- envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature'
162
+ envelope.header["Content-Type"] = "multipart/signed; protocol=application/pgp-signature; micalg=pgp-#{hash_algo.downcase}"
151
163
 
152
164
  envelope.add_part payload
153
165
  signature = RMail::Message.make_attachment sig, "application/pgp-signature", nil, "signature.asc"
@@ -212,9 +224,10 @@ EOS
212
224
  unknown = false
213
225
  all_output_lines = []
214
226
  all_trusted = true
227
+ unknown_fingerprint = nil
215
228
 
216
229
  verify_result.signatures.each do |signature|
217
- output_lines, trusted = sig_output_lines signature
230
+ output_lines, trusted, unknown_fingerprint = sig_output_lines signature
218
231
  all_output_lines << output_lines
219
232
  all_output_lines.flatten!
220
233
  all_trusted &&= trusted
@@ -229,7 +242,7 @@ EOS
229
242
  end
230
243
 
231
244
  if valid || !unknown
232
- summary_line = simplify_sig_line(verify_result.signatures[0].to_s, all_trusted)
245
+ summary_line = simplify_sig_line(verify_result.signatures[0].to_s.dup, all_trusted)
233
246
  end
234
247
 
235
248
  if all_output_lines.length == 0
@@ -242,6 +255,8 @@ EOS
242
255
  end
243
256
  elsif !unknown
244
257
  Chunk::CryptoNotice.new(:invalid, summary_line, all_output_lines)
258
+ elsif unknown_fingerprint
259
+ Chunk::CryptoNotice.new(:unknown_key, "Unable to determine validity of cryptographic signature", all_output_lines, unknown_fingerprint)
245
260
  else
246
261
  unknown_status all_output_lines
247
262
  end
@@ -351,6 +366,31 @@ EOS
351
366
  [notice, sig, msg]
352
367
  end
353
368
 
369
+ def retrieve fingerprint
370
+ require 'net/http'
371
+ uri = URI($config[:keyserver_url] || KEYSERVER_URL)
372
+ unless uri.scheme == "http" and not uri.host.nil?
373
+ return "Invalid url: #{uri}"
374
+ end
375
+
376
+ fingerprint = "0x" + fingerprint unless fingerprint[0..1] == "0x"
377
+ params = {op: "get", search: fingerprint}
378
+ uri.query = URI.encode_www_form(params)
379
+
380
+ begin
381
+ res = Net::HTTP.get_response(uri)
382
+ rescue SocketError # Host doesn't exist or we couldn't connect
383
+ end
384
+ return "Couldn't get key from keyserver at this address: #{uri}" unless res.is_a?(Net::HTTPSuccess)
385
+
386
+ match = KEY_PATTERN.match(res.body)
387
+ return "No key found" unless match && match.length > 0
388
+
389
+ GPGME::Key.import(match[0])
390
+
391
+ return nil
392
+ end
393
+
354
394
  private
355
395
 
356
396
  def unknown_status lines=[]
@@ -394,6 +434,7 @@ private
394
434
  rescue EOFError
395
435
  from_key = nil
396
436
  first_sig = "No public key available for #{signature.fingerprint}"
437
+ unknown_fpr = signature.fingerprint
397
438
  end
398
439
 
399
440
  time_line = "Signature made " + signature.timestamp.strftime("%a %d %b %Y %H:%M:%S %Z") +
@@ -422,7 +463,7 @@ private
422
463
  output_lines << HookManager.run("sig-output",
423
464
  {:signature => signature, :from_key => from_key})
424
465
  end
425
- return output_lines, trusted
466
+ return output_lines, trusted, unknown_fpr
426
467
  end
427
468
 
428
469
  def key_type key, fpr
@@ -435,6 +476,7 @@ private
435
476
  when GPGME::PK_DSA then "DSA "
436
477
  when GPGME::PK_ELG then "ElGamel "
437
478
  when GPGME::PK_ELG_E then "ElGamel "
479
+ else "unknown key type (#{subkey.pubkey_algo}) "
438
480
  end
439
481
  end
440
482
 
@@ -443,7 +485,7 @@ private
443
485
  # elsif only one account, then leave blank so gpg default will be user
444
486
  # else set --local-user from_email_address
445
487
  # NOTE: multiple signers doesn't seem to work with gpgme (2.0.2, 1.0.8)
446
- #
488
+ #
447
489
  def gen_sign_user_opts from
448
490
  account = AccountManager.account_for from
449
491
  account ||= AccountManager.default_account
@@ -16,7 +16,7 @@ class DraftManager
16
16
  def write_draft
17
17
  offset = @source.gen_offset
18
18
  fn = @source.fn_for_offset offset
19
- File.open(fn, "w") { |f| yield f }
19
+ File.open(fn, "w:UTF-8") { |f| yield f }
20
20
  PollManager.poll_from @source
21
21
  end
22
22
 
@@ -33,7 +33,7 @@ class DraftLoader < Source
33
33
  yaml_properties
34
34
 
35
35
  def initialize dir=Redwood::DRAFT_DIR
36
- Dir.mkdir dir unless File.exists? dir
36
+ Dir.mkdir dir unless File.exist? dir
37
37
  super DraftManager.source_name, true, false
38
38
  @dir = dir
39
39
  @cur_offset = 0
@@ -61,8 +61,8 @@ class DraftLoader < Source
61
61
  end
62
62
 
63
63
  def gen_offset
64
- i = 0
65
- while File.exists? fn_for_offset(i)
64
+ i = @cur_offset
65
+ while File.exist? fn_for_offset(i)
66
66
  i += 1
67
67
  end
68
68
  i
@@ -75,7 +75,7 @@ class DraftLoader < Source
75
75
  end
76
76
 
77
77
  def load_message offset
78
- raise SourceError, "Draft not found" unless File.exists? fn_for_offset(offset)
78
+ raise SourceError, "Draft not found" unless File.exist? fn_for_offset(offset)
79
79
  File.open fn_for_offset(offset) do |f|
80
80
  RMail::Mailbox::MBoxReader.new(f).each_message do |input|
81
81
  return RMail::Parser.read(input)
@@ -85,7 +85,7 @@ class DraftLoader < Source
85
85
 
86
86
  def raw_header offset
87
87
  ret = ""
88
- File.open fn_for_offset(offset) do |f|
88
+ File.open(fn_for_offset(offset), "r:UTF-8") do |f|
89
89
  until f.eof? || (l = f.gets) =~ /^$/
90
90
  ret += l
91
91
  end
@@ -94,13 +94,13 @@ class DraftLoader < Source
94
94
  end
95
95
 
96
96
  def each_raw_message_line offset
97
- File.open(fn_for_offset(offset)) do |f|
97
+ File.open(fn_for_offset(offset), "r:UTF-8") do |f|
98
98
  yield f.gets until f.eof?
99
99
  end
100
100
  end
101
101
 
102
102
  def raw_message offset
103
- IO.read(fn_for_offset(offset))
103
+ IO.read(fn_for_offset(offset), :encoding => "UTF-8")
104
104
  end
105
105
 
106
106
  def start_offset; 0; end
@@ -83,7 +83,7 @@ class HookManager
83
83
  @contexts = {}
84
84
  @tags = {}
85
85
 
86
- Dir.mkdir dir unless File.exists? dir
86
+ Dir.mkdir dir unless File.exist? dir
87
87
  end
88
88
 
89
89
  attr_reader :tags
@@ -109,20 +109,20 @@ class HookManager
109
109
  @descs[name] = desc
110
110
  end
111
111
 
112
- def print_hooks f=$stdout
113
- puts <<EOS
114
- Have #{HookManager.descs.size} registered hooks:
115
-
116
- EOS
117
-
118
- HookManager.descs.sort.each do |name, desc|
119
- f.puts <<EOS
112
+ def print_hooks pattern="", f=$stdout
113
+ matching_hooks = HookManager.descs.sort.keep_if {|name, desc| pattern.empty? or name.match(pattern)}.map do |name, desc|
114
+ <<EOS
120
115
  #{name}
121
116
  #{"-" * name.length}
122
117
  File: #{fn_for name}
123
118
  #{desc}
124
119
  EOS
125
120
  end
121
+
122
+ showing_str = matching_hooks.size == HookManager.descs.size ? "" : " (showing #{matching_hooks.size})"
123
+ f.puts "Have #{HookManager.descs.size} registered hooks#{showing_str}:"
124
+ f.puts
125
+ matching_hooks.each { |text| f.puts text }
126
126
  end
127
127
 
128
128
  def enabled? name; !hook_for(name).nil? end
@@ -105,7 +105,7 @@ EOS
105
105
 
106
106
  def save
107
107
  debug "saving index and sources..."
108
- FileUtils.mkdir_p @dir unless File.exists? @dir
108
+ FileUtils.mkdir_p @dir unless File.exist? @dir
109
109
  SourceManager.save_sources
110
110
  save_index
111
111
  end
@@ -116,7 +116,7 @@ EOS
116
116
 
117
117
  def load_index failsafe=false
118
118
  path = File.join(@dir, 'xapian')
119
- if File.exists? path
119
+ if File.exist? path
120
120
  @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_OPEN)
121
121
  db_version = @xapian.get_metadata 'version'
122
122
  db_version = '0' if db_version.empty?
@@ -516,19 +516,32 @@ EOS
516
516
  qp.stemmer = Xapian::Stem.new($config[:stem_language])
517
517
  qp.stemming_strategy = Xapian::QueryParser::STEM_SOME
518
518
  qp.default_op = Xapian::Query::OP_AND
519
- qp.add_valuerangeprocessor(Xapian::NumberValueRangeProcessor.new(DATE_VALUENO, 'date:', true))
520
- NORMAL_PREFIX.each { |k,info| info[:prefix].each { |v| qp.add_prefix k, v } }
521
- BOOLEAN_PREFIX.each { |k,info| info[:prefix].each { |v| qp.add_boolean_prefix k, v, info[:exclusive] } }
519
+ valuerangeprocessor = Xapian::NumberValueRangeProcessor.new(DATE_VALUENO,
520
+ 'date:', true)
521
+ qp.add_valuerangeprocessor(valuerangeprocessor)
522
+ NORMAL_PREFIX.each { |k,info| info[:prefix].each {
523
+ |v| qp.add_prefix k, v }
524
+ }
525
+ BOOLEAN_PREFIX.each { |k,info| info[:prefix].each {
526
+ |v| qp.add_boolean_prefix k, v, info[:exclusive] }
527
+ }
522
528
 
523
529
  begin
524
- xapian_query = qp.parse_query(subs, Xapian::QueryParser::FLAG_PHRASE|Xapian::QueryParser::FLAG_BOOLEAN|Xapian::QueryParser::FLAG_LOVEHATE|Xapian::QueryParser::FLAG_WILDCARD)
530
+ xapian_query = qp.parse_query(subs, Xapian::QueryParser::FLAG_PHRASE |
531
+ Xapian::QueryParser::FLAG_BOOLEAN |
532
+ Xapian::QueryParser::FLAG_LOVEHATE |
533
+ Xapian::QueryParser::FLAG_WILDCARD)
525
534
  rescue RuntimeError => e
526
535
  raise ParseError, "xapian query parser error: #{e}"
527
536
  end
528
537
 
529
538
  debug "parsed xapian query: #{Util::Query.describe(xapian_query, subs)}"
530
539
 
531
- raise ParseError if xapian_query.nil? or xapian_query.empty?
540
+ if xapian_query.nil? or xapian_query.empty?
541
+ raise ParseError, "couldn't parse \"#{s}\" as xapian query " \
542
+ "(special characters aren't indexed)"
543
+ end
544
+
532
545
  query[:qobj] = xapian_query
533
546
  query[:text] = s
534
547
  query
@@ -15,7 +15,7 @@ class LabelManager
15
15
  def initialize fn
16
16
  @fn = fn
17
17
  labels =
18
- if File.exists? fn
18
+ if File.exist? fn
19
19
  IO.readlines(fn).map { |x| x.chomp.intern }
20
20
  else
21
21
  []