sup 0.13.2.1 → 0.14.0

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.

@@ -32,14 +32,17 @@ class DraftLoader < Source
32
32
  attr_accessor :dir
33
33
  yaml_properties
34
34
 
35
- def initialize
36
- dir = Redwood::DRAFT_DIR
35
+ def initialize dir=Redwood::DRAFT_DIR
37
36
  Dir.mkdir dir unless File.exists? dir
38
37
  super DraftManager.source_name, true, false
39
38
  @dir = dir
40
39
  @cur_offset = 0
41
40
  end
42
41
 
42
+ def properly_initialized?
43
+ !!(@dir && @cur_offset)
44
+ end
45
+
43
46
  def id; DraftManager.source_id; end
44
47
  def to_s; DraftManager.source_name; end
45
48
  def uri; DraftManager.source_name; end
@@ -1,4 +1,5 @@
1
1
  ENV["XAPIAN_FLUSH_THRESHOLD"] = "1000"
2
+ ENV["XAPIAN_CJK_NGRAM"] = "1"
2
3
 
3
4
  require 'xapian'
4
5
  require 'set'
@@ -6,13 +7,21 @@ require 'fileutils'
6
7
  require 'monitor'
7
8
  require 'chronic'
8
9
 
10
+ require "sup/util/query"
9
11
  require "sup/interactive_lock"
10
12
  require "sup/hook"
11
13
  require "sup/logger/singleton"
12
14
 
13
15
 
14
- if ([Xapian.major_version, Xapian.minor_version, Xapian.revision] <=> [1,2,1]) < 0
15
- fail "Xapian version 1.2.1 or higher required"
16
+ if ([Xapian.major_version, Xapian.minor_version, Xapian.revision] <=> [1,2,15]) < 0
17
+ fail <<-EOF
18
+ \n
19
+ Xapian version 1.2.15 or higher required.
20
+ If you have xapian-full-alaveteli installed,
21
+ Please remove it by running `gem uninstall xapian-full-alaveteli`
22
+ since it's been replaced by the xapian-ruby gem.
23
+
24
+ EOF
16
25
  end
17
26
 
18
27
  module Redwood
@@ -312,6 +321,48 @@ EOS
312
321
 
313
322
  class ParseError < StandardError; end
314
323
 
324
+ # Stemmed
325
+ NORMAL_PREFIX = {
326
+ 'subject' => {:prefix => 'S', :exclusive => false},
327
+ 'body' => {:prefix => 'B', :exclusive => false},
328
+ 'from_name' => {:prefix => 'FN', :exclusive => false},
329
+ 'to_name' => {:prefix => 'TN', :exclusive => false},
330
+ 'name' => {:prefix => %w(FN TN), :exclusive => false},
331
+ 'attachment' => {:prefix => 'A', :exclusive => false},
332
+ 'email_text' => {:prefix => 'E', :exclusive => false},
333
+ '' => {:prefix => %w(S B FN TN A E), :exclusive => false},
334
+ }
335
+
336
+ # Unstemmed
337
+ BOOLEAN_PREFIX = {
338
+ 'type' => {:prefix => 'K', :exclusive => true},
339
+ 'from_email' => {:prefix => 'FE', :exclusive => false},
340
+ 'to_email' => {:prefix => 'TE', :exclusive => false},
341
+ 'email' => {:prefix => %w(FE TE), :exclusive => false},
342
+ 'date' => {:prefix => 'D', :exclusive => true},
343
+ 'label' => {:prefix => 'L', :exclusive => false},
344
+ 'source_id' => {:prefix => 'I', :exclusive => true},
345
+ 'attachment_extension' => {:prefix => 'O', :exclusive => false},
346
+ 'msgid' => {:prefix => 'Q', :exclusive => true},
347
+ 'id' => {:prefix => 'Q', :exclusive => true},
348
+ 'thread' => {:prefix => 'H', :exclusive => false},
349
+ 'ref' => {:prefix => 'R', :exclusive => false},
350
+ 'location' => {:prefix => 'J', :exclusive => false},
351
+ }
352
+
353
+ PREFIX = NORMAL_PREFIX.merge BOOLEAN_PREFIX
354
+
355
+ COMPL_OPERATORS = %w[AND OR NOT]
356
+ COMPL_PREFIXES = (
357
+ %w[
358
+ from to
359
+ is has label
360
+ filename filetypem
361
+ before on in during after
362
+ limit
363
+ ] + NORMAL_PREFIX.keys + BOOLEAN_PREFIX.keys
364
+ ).map{|p|"#{p}:"} + COMPL_OPERATORS
365
+
315
366
  ## parse a query string from the user. returns a query object
316
367
  ## that can be passed to any index method with a 'query'
317
368
  ## argument.
@@ -441,7 +492,7 @@ EOS
441
492
  raise ParseError, "xapian query parser error: #{e}"
442
493
  end
443
494
 
444
- debug "parsed xapian query: #{xapian_query.description}"
495
+ debug "parsed xapian query: #{Util::Query.describe(xapian_query)}"
445
496
 
446
497
  raise ParseError if xapian_query.nil? or xapian_query.empty?
447
498
  query[:qobj] = xapian_query
@@ -482,37 +533,6 @@ EOS
482
533
 
483
534
  private
484
535
 
485
- # Stemmed
486
- NORMAL_PREFIX = {
487
- 'subject' => {:prefix => 'S', :exclusive => false},
488
- 'body' => {:prefix => 'B', :exclusive => false},
489
- 'from_name' => {:prefix => 'FN', :exclusive => false},
490
- 'to_name' => {:prefix => 'TN', :exclusive => false},
491
- 'name' => {:prefix => %w(FN TN), :exclusive => false},
492
- 'attachment' => {:prefix => 'A', :exclusive => false},
493
- 'email_text' => {:prefix => 'E', :exclusive => false},
494
- '' => {:prefix => %w(S B FN TN A E), :exclusive => false},
495
- }
496
-
497
- # Unstemmed
498
- BOOLEAN_PREFIX = {
499
- 'type' => {:prefix => 'K', :exclusive => true},
500
- 'from_email' => {:prefix => 'FE', :exclusive => false},
501
- 'to_email' => {:prefix => 'TE', :exclusive => false},
502
- 'email' => {:prefix => %w(FE TE), :exclusive => false},
503
- 'date' => {:prefix => 'D', :exclusive => true},
504
- 'label' => {:prefix => 'L', :exclusive => false},
505
- 'source_id' => {:prefix => 'I', :exclusive => true},
506
- 'attachment_extension' => {:prefix => 'O', :exclusive => false},
507
- 'msgid' => {:prefix => 'Q', :exclusive => true},
508
- 'id' => {:prefix => 'Q', :exclusive => true},
509
- 'thread' => {:prefix => 'H', :exclusive => false},
510
- 'ref' => {:prefix => 'R', :exclusive => false},
511
- 'location' => {:prefix => 'J', :exclusive => false},
512
- }
513
-
514
- PREFIX = NORMAL_PREFIX.merge BOOLEAN_PREFIX
515
-
516
536
  MSGID_VALUENO = 0
517
537
  THREAD_VALUENO = 1
518
538
  DATE_VALUENO = 2
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Redwood
2
4
 
3
5
  class LabelManager
@@ -77,7 +79,7 @@ class LabelManager
77
79
 
78
80
  def save
79
81
  return unless @modified
80
- File.open(@fn, "w") { |f| f.puts @labels.keys.sort_by { |l| l.to_s } }
82
+ File.open(@fn, "w:UTF-8") { |f| f.puts @labels.keys.sort_by { |l| l.to_s } }
81
83
  @new_labels = {}
82
84
  end
83
85
  end
@@ -69,7 +69,9 @@ class Message
69
69
  return unless v
70
70
  return v unless v.is_a? String
71
71
  return unless v.size < MAX_HEADER_VALUE_SIZE # avoid regex blowup on spam
72
- Rfc2047.decode_to $encoding, Iconv.easy_decode($encoding, 'ASCII', v)
72
+ d = v.dup
73
+ d = d.transcode($encoding, 'ASCII')
74
+ Rfc2047.decode_to $encoding, d
73
75
  end
74
76
 
75
77
  def parse_header encoded_header
@@ -109,7 +111,9 @@ class Message
109
111
  Time.now
110
112
  end
111
113
 
112
- @subj = header["subject"] ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
114
+ subj = header["subject"]
115
+ subj = subj ? subj.fix_encoding : nil
116
+ @subj = subj ? subj.gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
113
117
  @to = Person.from_address_list header["to"]
114
118
  @cc = Person.from_address_list header["cc"]
115
119
  @bcc = Person.from_address_list header["bcc"]
@@ -260,6 +264,8 @@ class Message
260
264
  rescue SourceError, SocketError, RMail::EncodingUnsupportedError => e
261
265
  warn "problem reading message #{id}"
262
266
  [Chunk::Text.new(error_message.split("\n"))]
267
+
268
+ debug "could not load message: #{location.inspect}, exception: #{e.inspect}"
263
269
  end
264
270
  end
265
271
 
@@ -524,7 +530,7 @@ private
524
530
  ## if there's no charset, use the current encoding as the charset.
525
531
  ## this ensures that the body is normalized to avoid non-displayable
526
532
  ## characters
527
- body = Iconv.easy_decode($encoding, m.charset || $encoding, m.decode)
533
+ body = m.decode.transcode($encoding, m.charset)
528
534
  else
529
535
  body = ""
530
536
  end
@@ -546,7 +552,7 @@ private
546
552
  msg = RMail::Message.new
547
553
  msg.body = gpg.join("\n")
548
554
 
549
- body = Iconv.easy_decode(encoding_to, encoding_from, body)
555
+ body = body.transcode(encoding_to, encoding_from)
550
556
  lines = body.split("\n")
551
557
  sig = lines.between(GPG_SIGNED_START, GPG_SIG_START)
552
558
  startidx = lines.index(GPG_SIGNED_START)
@@ -1,6 +1,5 @@
1
1
  require 'tempfile'
2
2
  require 'rbconfig'
3
- require 'shellwords'
4
3
 
5
4
  ## Here we define all the "chunks" that a message is parsed
6
5
  ## into. Chunks are used by ThreadViewMode to render a message. Chunks
@@ -60,8 +59,6 @@ end
60
59
  module Redwood
61
60
  module Chunk
62
61
  class Attachment
63
- ## please see note in write_to_disk on important usage
64
- ## of quotes to avoid remote command injection.
65
62
  HookManager.register "mime-decode", <<EOS
66
63
  Decodes a MIME attachment into text form. The text will be displayed
67
64
  directly in Sup. For attachments that you wish to use a separate program
@@ -78,9 +75,6 @@ Return value:
78
75
  The decoded text of the attachment, or nil if not decoded.
79
76
  EOS
80
77
 
81
-
82
- ## please see note in write_to_disk on important usage
83
- ## of quotes to avoid remote command injection.
84
78
  HookManager.register "mime-view", <<EOS
85
79
  Views a non-text MIME attachment. This hook allows you to run
86
80
  third-party programs for attachments that require such a thing (e.g.
@@ -106,18 +100,8 @@ EOS
106
100
  attr_reader :content_type, :filename, :lines, :raw_content
107
101
  bool_reader :quotable
108
102
 
109
- ## store tempfile objects as class variables so that they
110
- ## are not removed when the viewing process returns. they
111
- ## should be garbage collected when the class variable is removed.
112
- @@view_tempfiles = []
113
-
114
103
  def initialize content_type, filename, encoded_content, sibling_types
115
104
  @content_type = content_type.downcase
116
- if Shellwords.escape(@content_type) != @content_type
117
- warn "content_type #{@content_type} is not safe, changed to application/octet-stream"
118
- @content_type = 'application/octet-stream'
119
- end
120
-
121
105
  @filename = filename
122
106
  @quotable = false # changed to true if we can parse it through the
123
107
  # mime-decode hook, or if it's plain text
@@ -132,9 +116,7 @@ EOS
132
116
  when /^text\/plain\b/
133
117
  @raw_content
134
118
  else
135
- ## please see note in write_to_disk on important usage
136
- ## of quotes to avoid remote command injection.
137
- HookManager.run "mime-decode", :content_type => @content_type,
119
+ HookManager.run "mime-decode", :content_type => content_type,
138
120
  :filename => lambda { write_to_disk },
139
121
  :charset => encoded_content.charset,
140
122
  :sibling_types => sibling_types
@@ -142,7 +124,7 @@ EOS
142
124
 
143
125
  @lines = nil
144
126
  if text
145
- text = text.transcode(encoded_content.charset || $encoding)
127
+ text = text.transcode(encoded_content.charset || $encoding, text.encoding)
146
128
  @lines = text.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
147
129
  @quotable = true
148
130
  end
@@ -165,13 +147,11 @@ EOS
165
147
  def initial_state; :open end
166
148
  def viewable?; @lines.nil? end
167
149
  def view_default! path
168
- ## please see note in write_to_disk on important usage
169
- ## of quotes to avoid remote command injection.
170
150
  case RbConfig::CONFIG['arch']
171
151
  when /darwin/
172
- cmd = "open #{path}"
152
+ cmd = "open '#{path}'"
173
153
  else
174
- cmd = "/usr/bin/run-mailcap --action=view #{@content_type}:#{path}"
154
+ cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}'"
175
155
  end
176
156
  debug "running: #{cmd.inspect}"
177
157
  BufferManager.shell_out(cmd)
@@ -179,31 +159,17 @@ EOS
179
159
  end
180
160
 
181
161
  def view!
182
- ## please see note in write_to_disk on important usage
183
- ## of quotes to avoid remote command injection.
184
- write_to_disk do |file|
185
-
186
- @@view_tempfiles.push file # make sure the tempfile is not garbage collected before sup stops
187
-
188
- ret = HookManager.run "mime-view", :content_type => @content_type,
189
- :filename => file.path
190
- ret || view_default!(file.path)
191
- end
162
+ path = write_to_disk
163
+ ret = HookManager.run "mime-view", :content_type => @content_type,
164
+ :filename => path
165
+ ret || view_default!(path)
192
166
  end
193
167
 
194
- ## note that the path returned from write_to_disk is
195
- ## Shellwords.escaped and is intended to be used without single
196
- ## or double quotes. the use of either opens sup up for remote
197
- ## code injection through the file name.
198
168
  def write_to_disk
199
- begin
200
- file = Tempfile.new(["sup", Shellwords.escape(@filename.gsub("/", "_")) || "sup-attachment"])
201
- file.print @raw_content
202
- yield file if block_given?
203
- return file.path
204
- ensure
205
- file.close
206
- end
169
+ file = Tempfile.new(["sup", @filename.gsub("/", "_") || "sup-attachment"])
170
+ file.print @raw_content
171
+ file.close
172
+ file.path
207
173
  end
208
174
 
209
175
  ## used when viewing the attachment as text
@@ -263,7 +229,7 @@ EOS
263
229
  class EnclosedMessage
264
230
  attr_reader :lines
265
231
  def initialize from, to, cc, date, subj
266
- @from = from ? "unknown sender" : from.full_address
232
+ @from = from ? "unknown sender" : from.full_adress
267
233
  @to = to ? "" : to.map { |p| p.full_address }.join(", ")
268
234
  @cc = cc ? "" : cc.map { |p| p.full_address }.join(", ")
269
235
  if date
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Redwood
2
4
 
3
5
  class ComposeMode < EditMessageMode
@@ -484,10 +484,10 @@ protected
484
484
  m = build_message date
485
485
 
486
486
  if HookManager.enabled? "sendmail"
487
- if not HookManager.run "sendmail", :message => m, :account => acct
488
- warn "Sendmail hook was not successful"
489
- return false
490
- end
487
+ if not HookManager.run "sendmail", :message => m, :account => acct
488
+ warn "Sendmail hook was not successful"
489
+ return false
490
+ end
491
491
  else
492
492
  IO.popen(acct.sendmail, "w") { |p| p.puts m }
493
493
  raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
@@ -1,4 +1,4 @@
1
- require 'sup'
1
+ require "sup/modes/thread_index_mode"
2
2
 
3
3
  module Redwood
4
4
 
@@ -856,14 +856,14 @@ protected
856
856
 
857
857
  abbrev =
858
858
  if cur_width + name.display_length > from_width
859
- name[0 ... (from_width - cur_width - 1)] + "."
859
+ name.slice_by_display_length(from_width - cur_width - 1) + "."
860
860
  elsif cur_width + name.display_length == from_width
861
- name[0 ... (from_width - cur_width)]
861
+ name.slice_by_display_length(from_width - cur_width)
862
862
  else
863
863
  if last
864
- name[0 ... (from_width - cur_width)]
864
+ name.slice_by_display_length(from_width - cur_width)
865
865
  else
866
- name[0 ... (from_width - cur_width - 1)] + ","
866
+ name.slice_by_display_length(from_width - cur_width - 1) + ","
867
867
  end
868
868
  end
869
869
 
@@ -846,6 +846,10 @@ private
846
846
  else
847
847
  width = buffer.content_width
848
848
  end
849
+ # lines can apparently be both String and Array, convert to Array for map.
850
+ if lines.kind_of? String
851
+ lines = lines.lines.to_a
852
+ end
849
853
  lines = lines.map { |l| l.chomp.wrap width if l }.flatten
850
854
  end
851
855
  return lines
@@ -171,7 +171,7 @@ EOS
171
171
  ## labels and locations set correctly. The Messages are saved to or removed
172
172
  ## from the index after being yielded.
173
173
  def poll_from source, opts={}
174
- debug "trying to acquiring poll lock for: #{source}.."
174
+ debug "trying to acquire poll lock for: #{source}.."
175
175
  if source.poll_lock.try_lock
176
176
  debug "lock acquired for: #{source}."
177
177
  begin
@@ -16,8 +16,6 @@
16
16
  #
17
17
  # This file is distributed under the same terms as Ruby.
18
18
 
19
- require 'iconv'
20
-
21
19
  module Rfc2047
22
20
  WORD = %r{=\?([!\#$%&'*+-/0-9A-Z\\^\`a-z{|}~]+)\?([BbQq])\?([!->@-~]+)\?=} # :nodoc: 'stupid ruby-mode
23
21
  WORDSEQ = %r{(#{WORD.source})\s+(?=#{WORD.source})}
@@ -52,7 +50,7 @@ module Rfc2047
52
50
  # WORD.
53
51
  end
54
52
 
55
- Iconv.easy_decode(target, charset, text)
53
+ text.transcode(target, charset)
56
54
  end
57
55
  end
58
56
  end
@@ -25,8 +25,13 @@ class SentManager
25
25
  end
26
26
 
27
27
  def write_sent_message date, from_email, &block
28
- @source.store_message date, from_email, &block
29
- PollManager.poll_from @source
28
+ ::Thread.new do
29
+ debug "store the sent message (locking sent source..)"
30
+ @source.poll_lock.synchronize do
31
+ @source.store_message date, from_email, &block
32
+ end
33
+ PollManager.poll_from @source
34
+ end
30
35
  end
31
36
  end
32
37
 
@@ -22,30 +22,23 @@ class Source
22
22
  ## read, delete them, or anything else. (Well, it's nice to be able
23
23
  ## to delete them, but that is optional.)
24
24
  ##
25
- ## On the other hand, Sup assumes that you can assign each message a
26
- ## unique integer id, such that newer messages have higher ids than
27
- ## earlier ones, and that those ids stay constant across sessions
28
- ## (in the absence of some other client going in and fucking
29
- ## everything up). For example, for mboxes I use the file offset of
30
- ## the start of the message. If a source does NOT have that
31
- ## capability, e.g. IMAP, then you have to do a little more work to
32
- ## simulate it.
25
+ ## Messages are identified internally based on the message id, and stored
26
+ ## with an unique document id. Along with the message, source information
27
+ ## that can contain arbitrary fields (set up by the source) is stored. This
28
+ ## information will be passed back to the source when a message in the
29
+ ## index (Sup database) needs to be identified to its source, e.g. when
30
+ ## re-reading or modifying a unique message.
33
31
  ##
34
32
  ## To write a new source, subclass this class, and implement:
35
33
  ##
36
- ## - start_offset
37
- ## - end_offset (exclusive!) (or, #done?)
34
+ ## - initialize
38
35
  ## - load_header offset
39
36
  ## - load_message offset
40
37
  ## - raw_header offset
41
38
  ## - raw_message offset
42
- ## - check (optional)
39
+ ## - store_message (optional)
40
+ ## - poll (loads new messages)
43
41
  ## - go_idle (optional)
44
- ## - next (or each, if you prefer): should return a message and an
45
- ## array of labels.
46
- ##
47
- ## ... where "offset" really means unique id. (You can tell I
48
- ## started with mbox.)
49
42
  ##
50
43
  ## All exceptions relating to accessing the source must be caught
51
44
  ## and rethrown as FatalSourceErrors or OutOfSyncSourceErrors.
@@ -57,8 +50,7 @@ class Source
57
50
  ## Finally, be sure the source is thread-safe, since it WILL be
58
51
  ## pummelled from multiple threads at once.
59
52
  ##
60
- ## Examples for you to look at: mbox/loader.rb, imap.rb, and
61
- ## maildir.rb.
53
+ ## Examples for you to look at: mbox.rb and maildir.rb.
62
54
 
63
55
  bool_accessor :usual, :archived
64
56
  attr_reader :uri
@@ -211,9 +203,9 @@ class SourceManager
211
203
  end
212
204
  end
213
205
 
214
- def save_sources fn=Redwood::SOURCE_FN
206
+ def save_sources fn=Redwood::SOURCE_FN, force=false
215
207
  @source_mutex.synchronize do
216
- if @sources_dirty
208
+ if @sources_dirty || force
217
209
  Redwood::save_yaml_obj sources, fn, false, true
218
210
  end
219
211
  @sources_dirty = false