sup 0.9.1 → 0.10

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 (46) hide show
  1. data/CONTRIBUTORS +10 -6
  2. data/History.txt +11 -0
  3. data/ReleaseNotes +10 -0
  4. data/bin/sup +55 -19
  5. data/bin/sup-add +18 -8
  6. data/bin/sup-config +2 -2
  7. data/bin/sup-convert-ferret-index +84 -0
  8. data/bin/sup-dump +4 -3
  9. data/bin/sup-sync +4 -3
  10. data/bin/sup-sync-back +3 -2
  11. data/bin/sup-tweak-labels +3 -3
  12. data/lib/sup.rb +35 -4
  13. data/lib/sup/buffer.rb +12 -6
  14. data/lib/sup/colormap.rb +1 -0
  15. data/lib/sup/crypto.rb +76 -55
  16. data/lib/sup/ferret_index.rb +6 -1
  17. data/lib/sup/index.rb +62 -8
  18. data/lib/sup/logger.rb +2 -1
  19. data/lib/sup/maildir.rb +4 -2
  20. data/lib/sup/mbox/loader.rb +4 -3
  21. data/lib/sup/message-chunks.rb +9 -7
  22. data/lib/sup/message.rb +29 -27
  23. data/lib/sup/mode.rb +11 -4
  24. data/lib/sup/modes/buffer-list-mode.rb +5 -0
  25. data/lib/sup/modes/console-mode.rb +4 -0
  26. data/lib/sup/modes/edit-message-mode.rb +4 -2
  27. data/lib/sup/modes/file-browser-mode.rb +1 -1
  28. data/lib/sup/modes/inbox-mode.rb +18 -1
  29. data/lib/sup/modes/label-list-mode.rb +44 -3
  30. data/lib/sup/modes/text-mode.rb +1 -1
  31. data/lib/sup/modes/thread-index-mode.rb +63 -52
  32. data/lib/sup/modes/thread-view-mode.rb +68 -7
  33. data/lib/sup/poll.rb +20 -5
  34. data/lib/sup/source.rb +1 -0
  35. data/lib/sup/thread.rb +1 -1
  36. data/lib/sup/util.rb +49 -11
  37. data/lib/sup/xapian_index.rb +151 -112
  38. metadata +4 -10
  39. data/lib/sup/hook.rb.BACKUP.8625.rb +0 -158
  40. data/lib/sup/hook.rb.BACKUP.8681.rb +0 -158
  41. data/lib/sup/hook.rb.BASE.8625.rb +0 -155
  42. data/lib/sup/hook.rb.BASE.8681.rb +0 -155
  43. data/lib/sup/hook.rb.LOCAL.8625.rb +0 -142
  44. data/lib/sup/hook.rb.LOCAL.8681.rb +0 -142
  45. data/lib/sup/hook.rb.REMOTE.8625.rb +0 -145
  46. data/lib/sup/hook.rb.REMOTE.8681.rb +0 -145
@@ -59,7 +59,7 @@ class Maildir < Source
59
59
  File.stat(tmp_path)
60
60
  rescue Errno::ENOENT #this is what we want.
61
61
  begin
62
- File.open(tmp_path, 'w') do |f|
62
+ File.open(tmp_path, 'wb') do |f|
63
63
  yield f #provide a writable interface for the caller
64
64
  f.fsync
65
65
  end
@@ -187,6 +187,8 @@ class Maildir < Source
187
187
  def mark_seen msg; maildir_mark_file msg, "S" unless seen? msg; end
188
188
  def mark_trashed msg; maildir_mark_file msg, "T" unless trashed? msg; end
189
189
 
190
+ def filename_for_id id; @ids_to_fns[id] end
191
+
190
192
  private
191
193
 
192
194
  def make_id fn
@@ -205,7 +207,7 @@ private
205
207
  def with_file_for id
206
208
  fn = @ids_to_fns[id] or raise OutOfSyncSourceError, "No such id: #{id.inspect}."
207
209
  begin
208
- File.open(fn) { |f| yield f }
210
+ File.open(fn, 'rb') { |f| yield f }
209
211
  rescue SystemCallError, IOError => e
210
212
  raise FatalSourceError, "Problem reading file for id #{id.inspect}: #{fn.inspect}: #{e.message}."
211
213
  end
@@ -12,7 +12,7 @@ class Loader < Source
12
12
  attr_reader :labels
13
13
 
14
14
  ## uri_or_fp is horrific. need to refactor.
15
- def initialize uri_or_fp, start_offset=0, usual=true, archived=false, id=nil, labels=nil
15
+ def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=nil
16
16
  @mutex = Mutex.new
17
17
  @labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
18
18
 
@@ -22,13 +22,14 @@ class Loader < Source
22
22
  raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
23
23
  raise ArgumentError, "mbox URI ('#{uri}') cannot have a host: #{uri.host}" if uri.host
24
24
  raise ArgumentError, "mbox URI must have a path component" unless uri.path
25
- @f = File.open uri.path
25
+ @f = File.open uri.path, 'rb'
26
26
  @path = uri.path
27
27
  else
28
28
  @f = uri_or_fp
29
29
  @path = uri_or_fp.path
30
30
  end
31
31
 
32
+ start_offset ||= 0
32
33
  super uri_or_fp, start_offset, usual, archived, id
33
34
  end
34
35
 
@@ -114,7 +115,7 @@ class Loader < Source
114
115
 
115
116
  def store_message date, from_email, &block
116
117
  need_blank = File.exists?(@filename) && !File.zero?(@filename)
117
- File.open(@filename, "a") do |f|
118
+ File.open(@filename, "ab") do |f|
118
119
  f.puts if need_blank
119
120
  f.puts "From #{from_email} #{date.rfc2822}"
120
121
  yield f
@@ -41,8 +41,6 @@ end
41
41
 
42
42
  module Redwood
43
43
  module Chunk
44
- WRAP_LEN = 80 # wrap messages and text attachments at this width
45
-
46
44
  class Attachment
47
45
  HookManager.register "mime-decode", <<EOS
48
46
  Decodes a MIME attachment into text form. The text will be displayed
@@ -99,7 +97,7 @@ EOS
99
97
 
100
98
  text = case @content_type
101
99
  when /^text\/plain\b/
102
- Iconv.easy_decode $encoding, encoded_content.charset || $encoding, @raw_content
100
+ @raw_content
103
101
  else
104
102
  HookManager.run "mime-decode", :content_type => content_type,
105
103
  :filename => lambda { write_to_disk },
@@ -109,8 +107,8 @@ EOS
109
107
 
110
108
  @lines = nil
111
109
  if text
110
+ text = text.transcode(encoded_content.charset || $encoding)
112
111
  @lines = text.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
113
- @lines = lines.map {|l| l.chomp.wrap WRAP_LEN}.flatten
114
112
  @quotable = true
115
113
  end
116
114
  end
@@ -132,7 +130,12 @@ EOS
132
130
  def initial_state; :open end
133
131
  def viewable?; @lines.nil? end
134
132
  def view_default! path
135
- cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}'"
133
+ case Config::CONFIG['arch']
134
+ when /darwin/
135
+ cmd = "open '#{path}'"
136
+ else
137
+ cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}'"
138
+ end
136
139
  debug "running: #{cmd.inspect}"
137
140
  BufferManager.shell_out(cmd)
138
141
  $? == 0
@@ -162,8 +165,7 @@ EOS
162
165
 
163
166
  attr_reader :lines
164
167
  def initialize lines
165
- @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten # wrap
166
-
168
+ @lines = lines
167
169
  ## trim off all empty lines except one
168
170
  @lines.pop while @lines.length > 1 && @lines[-1] =~ /^\s*$/ && @lines[-2] =~ /^\s*$/
169
171
  end
@@ -31,6 +31,7 @@ class Message
31
31
  MAX_SIG_DISTANCE = 15 # lines from the end
32
32
  DEFAULT_SUBJECT = ""
33
33
  DEFAULT_SENDER = "(missing sender)"
34
+ MAX_HEADER_VALUE_SIZE = 4096
34
35
 
35
36
  attr_reader :id, :date, :from, :subj, :refs, :replytos, :to, :source,
36
37
  :cc, :bcc, :labels, :attachments, :list_address, :recipient_email, :replyto,
@@ -59,13 +60,15 @@ class Message
59
60
  #parse_header(opts[:header] || @source.load_header(@source_info))
60
61
  end
61
62
 
62
- def parse_header header
63
- ## forcibly decode these headers from and to the current encoding,
64
- ## which serves to strip out characters that aren't displayable
65
- ## (and which would otherwise be screwing up the display)
66
- %w(from to subject cc bcc).each do |f|
67
- header[f] = Iconv.easy_decode($encoding, $encoding, header[f]) if header[f]
68
- end
63
+ def decode_header_field v
64
+ return unless v
65
+ return v unless v.is_a? String
66
+ return unless v.size < MAX_HEADER_VALUE_SIZE # avoid regex blowup on spam
67
+ Rfc2047.decode_to $encoding, Iconv.easy_decode($encoding, 'ASCII', v)
68
+ end
69
+
70
+ def parse_header encoded_header
71
+ header = SavingHash.new { |k| decode_header_field encoded_header[k] }
69
72
 
70
73
  @id = if header["message-id"]
71
74
  mid = header["message-id"] =~ /<(.+?)>/ ? $1 : header["message-id"]
@@ -100,7 +103,7 @@ class Message
100
103
  Time.now
101
104
  end
102
105
 
103
- @subj = header.member?("subject") ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
106
+ @subj = header["subject"] ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
104
107
  @to = Person.from_address_list header["to"]
105
108
  @cc = Person.from_address_list header["cc"]
106
109
  @bcc = Person.from_address_list header["bcc"]
@@ -114,12 +117,16 @@ class Message
114
117
  @replytos = (header["in-reply-to"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first }
115
118
 
116
119
  @replyto = Person.from_address header["reply-to"]
117
- @list_address =
118
- if header["list-post"]
119
- @list_address = Person.from_address header["list-post"].gsub(/^<mailto:|>$/, "")
120
- else
121
- nil
120
+ @list_address = if header["list-post"]
121
+ address = if header["list-post"] =~ /mailto:(.*?)[>\s$]/
122
+ $1
123
+ elsif header["list-post"] =~ /@/
124
+ header["list-post"] # just try the whole fucking thing
122
125
  end
126
+ address && Person.from_address(address)
127
+ elsif header["x-mailing-list"]
128
+ Person.from_address header["x-mailing-list"]
129
+ end
123
130
 
124
131
  @recipient_email = header["envelope-to"] || header["x-original-to"] || header["delivered-to"]
125
132
  @source_marked_read = header["status"] == "RO"
@@ -182,11 +189,8 @@ class Message
182
189
  ## don't tempt me.
183
190
  def sanitize_message_id mid; mid.gsub(/(\s|[^\000-\177])+/, "")[0..254] end
184
191
 
185
- def save_state index
186
- return unless @dirty
187
- index.update_message_state self
192
+ def clear_dirty
188
193
  @dirty = false
189
- true
190
194
  end
191
195
 
192
196
  def has_label? t; @labels.member? t; end
@@ -235,8 +239,9 @@ class Message
235
239
  ## bloat the index.
236
240
  ## actually, it's also the differentiation between to/cc/bcc,
237
241
  ## so i will keep this.
238
- parse_header @source.load_header(@source_info)
239
- message_to_chunks @source.load_message(@source_info)
242
+ rmsg = @source.load_message(@source_info)
243
+ parse_header rmsg.header
244
+ message_to_chunks rmsg
240
245
  rescue SourceError, SocketError => e
241
246
  warn "problem getting messages from #{@source}: #{e.message}"
242
247
  ## we need force_to_top here otherwise this window will cover
@@ -442,15 +447,12 @@ private
442
447
  from = payload.header.from.first ? payload.header.from.first.format : ""
443
448
  to = payload.header.to.map { |p| p.format }.join(", ")
444
449
  cc = payload.header.cc.map { |p| p.format }.join(", ")
445
- subj = payload.header.subject
446
- subj = subj ? Message.normalize_subj(payload.header.subject.gsub(/\s+/, " ").gsub(/\s+$/, "")) : subj
447
- if Rfc2047.is_encoded? subj
448
- subj = Rfc2047.decode_to $encoding, subj
449
- end
450
+ subj = decode_header_field(payload.header.subject) || DEFAULT_SUBJECT
451
+ subj = Message.normalize_subj(subj.gsub(/\s+/, " ").gsub(/\s+$/, ""))
450
452
  msgdate = payload.header.date
451
- from_person = from ? Person.from_address(from) : nil
452
- to_people = to ? Person.from_address_list(to) : nil
453
- cc_people = cc ? Person.from_address_list(cc) : nil
453
+ from_person = from ? Person.from_address(decode_header_field from) : nil
454
+ to_people = to ? Person.from_address_list(decode_header_field to) : nil
455
+ cc_people = cc ? Person.from_address_list(decode_header_field cc) : nil
454
456
  [Chunk::EnclosedMessage.new(from_person, to_people, cc_people, msgdate, subj)] + message_to_chunks(payload, encrypted)
455
457
  else
456
458
  debug "no body for message/rfc822 enclosure; skipping"
@@ -74,15 +74,22 @@ EOS
74
74
 
75
75
  ### helper functions
76
76
 
77
- def save_to_file fn
77
+ def save_to_file fn, talk=true
78
78
  if File.exists? fn
79
- return unless BufferManager.ask_yes_or_no "File exists. Overwrite?"
79
+ unless BufferManager.ask_yes_or_no "File \"#{fn}\" exists. Overwrite?"
80
+ info "Not overwriting #{fn}"
81
+ return
82
+ end
80
83
  end
81
84
  begin
82
85
  File.open(fn, "w") { |f| yield f }
83
- BufferManager.flash "Successfully wrote #{fn}."
86
+ BufferManager.flash "Successfully wrote #{fn}." if talk
87
+ true
84
88
  rescue SystemCallError, IOError => e
85
- BufferManager.flash "Error writing to file: #{e.message}"
89
+ m = "Error writing file: #{e.message}"
90
+ info m
91
+ BufferManager.flash m
92
+ false
86
93
  end
87
94
  end
88
95
 
@@ -4,6 +4,7 @@ class BufferListMode < LineCursorMode
4
4
  register_keymap do |k|
5
5
  k.add :jump_to_buffer, "Jump to selected buffer", :enter
6
6
  k.add :reload, "Reload buffer list", "@"
7
+ k.add :kill_selected_buffer, "Kill selected buffer", "X"
7
8
  end
8
9
 
9
10
  def initialize
@@ -40,6 +41,10 @@ protected
40
41
  def jump_to_buffer
41
42
  BufferManager.raise_to_front @bufs[curpos][1]
42
43
  end
44
+
45
+ def kill_selected_buffer
46
+ reload if BufferManager.kill_buffer_safely @bufs[curpos][1]
47
+ end
43
48
  end
44
49
 
45
50
  end
@@ -21,6 +21,10 @@ class Console
21
21
 
22
22
  def xapian; Index.instance.instance_variable_get :@xapian; end
23
23
  def ferret; Index.instance.instance_variable_get :@index; end
24
+
25
+ def loglevel; Redwood::Logger.level; end
26
+ def set_loglevel(level); Redwood::Logger.level = level; end
27
+
24
28
  def special_methods; methods - Object.methods end
25
29
 
26
30
  ## files that won't cause problems when reloaded
@@ -162,8 +162,10 @@ EOS
162
162
  fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
163
163
  return unless fn
164
164
  begin
165
- @attachments << RMail::Message.make_file_attachment(fn)
166
- @attachment_names << fn
165
+ Dir[fn].each do |f|
166
+ @attachments << RMail::Message.make_file_attachment(f)
167
+ @attachment_names << f
168
+ end
167
169
  update
168
170
  rescue SystemCallError => e
169
171
  BufferManager.flash "Can't read #{fn}: #{e.message}"
@@ -47,7 +47,7 @@ protected
47
47
  return unless f && f.file?
48
48
 
49
49
  begin
50
- BufferManager.spawn f.to_s, TextMode.new(f.read)
50
+ BufferManager.spawn f.to_s, TextMode.new(f.read.ascii)
51
51
  rescue SystemCallError => e
52
52
  BufferManager.flash e.message
53
53
  end
@@ -7,6 +7,7 @@ class InboxMode < ThreadIndexMode
7
7
  ## overwrite toggle_archived with archive
8
8
  k.add :archive, "Archive thread (remove from inbox)", 'a'
9
9
  k.add :read_and_archive, "Archive thread (remove from inbox) and mark read", 'A'
10
+ k.add :refine_search, "Refine search", '|'
10
11
  end
11
12
 
12
13
  def initialize
@@ -17,6 +18,13 @@ class InboxMode < ThreadIndexMode
17
18
 
18
19
  def is_relevant? m; (m.labels & [:spam, :deleted, :killed, :inbox]) == Set.new([:inbox]) end
19
20
 
21
+ def refine_search
22
+ text = BufferManager.ask :search, "refine inbox with query: "
23
+ return unless text && text !~ /^\s*$/
24
+ text = "label:inbox -label:spam -label:deleted " + text
25
+ SearchResultsMode.spawn_from_query text
26
+ end
27
+
20
28
  ## label-list-mode wants to be able to raise us if the user selects
21
29
  ## the "inbox" label, so we need to keep our singletonness around
22
30
  def self.instance; @@instance; end
@@ -29,11 +37,13 @@ class InboxMode < ThreadIndexMode
29
37
  UndoManager.register "archiving thread" do
30
38
  thread.apply_label :inbox
31
39
  add_or_unhide thread.first
40
+ Index.save_thread thread
32
41
  end
33
42
 
34
43
  cursor_thread.remove_label :inbox
35
44
  hide_thread cursor_thread
36
45
  regen_text
46
+ Index.save_thread thread
37
47
  end
38
48
 
39
49
  def multi_archive threads
@@ -41,6 +51,7 @@ class InboxMode < ThreadIndexMode
41
51
  threads.map do |t|
42
52
  t.apply_label :inbox
43
53
  add_or_unhide t.first
54
+ Index.save_thread t
44
55
  end
45
56
  regen_text
46
57
  end
@@ -50,22 +61,26 @@ class InboxMode < ThreadIndexMode
50
61
  hide_thread t
51
62
  end
52
63
  regen_text
64
+ threads.each { |t| Index.save_thread t }
53
65
  end
54
66
 
55
67
  def read_and_archive
56
68
  return unless cursor_thread
57
69
  thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
58
70
 
71
+ was_unread = thread.labels.member? :unread
59
72
  UndoManager.register "reading and archiving thread" do
60
73
  thread.apply_label :inbox
61
- thread.apply_label :unread
74
+ thread.apply_label :unread if was_unread
62
75
  add_or_unhide thread.first
76
+ Index.save_thread thread
63
77
  end
64
78
 
65
79
  cursor_thread.remove_label :unread
66
80
  cursor_thread.remove_label :inbox
67
81
  hide_thread cursor_thread
68
82
  regen_text
83
+ Index.save_thread thread
69
84
  end
70
85
 
71
86
  def multi_read_and_archive threads
@@ -82,10 +97,12 @@ class InboxMode < ThreadIndexMode
82
97
  threads.zip(old_labels).each do |t, l|
83
98
  t.labels = l
84
99
  add_or_unhide t.first
100
+ Index.save_thread t
85
101
  end
86
102
  regen_text
87
103
  end
88
104
 
105
+ threads.each { |t| Index.save_thread t }
89
106
  end
90
107
 
91
108
  def handle_unarchived_update sender, m
@@ -8,14 +8,38 @@ class LabelListMode < LineCursorMode
8
8
  k.add :toggle_show_unread_only, "Toggle between showing all labels and those with unread mail", 'u'
9
9
  end
10
10
 
11
+ HookManager.register "label-list-filter", <<EOS
12
+ Filter the label list, typically to sort.
13
+ Variables:
14
+ counted: an array of counted labels.
15
+ Return value:
16
+ An array of counted labels with sort_by output structure.
17
+ EOS
18
+
19
+ HookManager.register "label-list-format", <<EOS
20
+ Create the sprintf format string for label-list-mode.
21
+ Variables:
22
+ width: the maximum label width
23
+ tmax: the maximum total message count
24
+ umax: the maximum unread message count
25
+ Return value:
26
+ A format string for sprintf
27
+ EOS
28
+
11
29
  def initialize
12
30
  @labels = []
13
31
  @text = []
14
32
  @unread_only = false
15
33
  super
34
+ UpdateManager.register self
16
35
  regen_text
17
36
  end
18
37
 
38
+ def cleanup
39
+ UpdateManager.unregister self
40
+ super
41
+ end
42
+
19
43
  def lines; @text.length end
20
44
  def [] i; @text[i] end
21
45
 
@@ -34,6 +58,10 @@ class LabelListMode < LineCursorMode
34
58
  reload # make sure unread message counts are up-to-date
35
59
  end
36
60
 
61
+ def handle_added_update sender, m
62
+ reload
63
+ end
64
+
37
65
  protected
38
66
 
39
67
  def toggle_show_unread_only
@@ -50,14 +78,22 @@ protected
50
78
  @text = []
51
79
  labels = LabelManager.all_labels
52
80
 
53
- counts = labels.map do |label|
81
+ counted = labels.map do |label|
54
82
  string = LabelManager.string_for label
55
83
  total = Index.num_results_for :label => label
56
84
  unread = (label == :unread)? total : Index.num_results_for(:labels => [label, :unread])
57
85
  [label, string, total, unread]
58
- end.sort_by { |l, s, t, u| s.downcase }
86
+ end
87
+
88
+ if HookManager.enabled? "label-list-filter"
89
+ counts = HookManager.run "label-list-filter", :counted => counted
90
+ else
91
+ counts = counted.sort_by { |l, s, t, u| s.downcase }
92
+ end
59
93
 
60
94
  width = counts.max_of { |l, s, t, u| s.length }
95
+ tmax = counts.max_of { |l, s, t, u| t }
96
+ umax = counts.max_of { |l, s, t, u| u }
61
97
 
62
98
  if @unread_only
63
99
  counts.delete_if { | l, s, t, u | u == 0 }
@@ -78,8 +114,13 @@ protected
78
114
  next
79
115
  end
80
116
 
117
+ fmt = HookManager.run "label-list-format", :width => width, :tmax => tmax, :umax => umax
118
+ if !fmt
119
+ fmt = "%#{width + 1}s %5d %s, %5d unread"
120
+ end
121
+
81
122
  @text << [[(unread == 0 ? :labellist_old_color : :labellist_new_color),
82
- sprintf("%#{width + 1}s %5d %s, %5d unread", string, total, total == 1 ? " message" : "messages", unread)]]
123
+ sprintf(fmt, string, total, total == 1 ? " message" : "messages", unread)]]
83
124
  @labels << [label, unread]
84
125
  yield i if block_given?
85
126
  end.compact