sup 0.0.7 → 0.0.8

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.

@@ -3,12 +3,7 @@ require 'net/ssh'
3
3
  module Redwood
4
4
  module MBox
5
5
 
6
- ## this is slightly complicated because SSHFile (and thus @f or
7
- ## @loader) can throw a variety of exceptions, and we need to catch
8
- ## those, reraise them as SourceErrors, and set ourselves as broken.
9
-
10
6
  class SSHLoader < Source
11
- attr_reader_cloned :labels
12
7
  attr_accessor :username, :password
13
8
 
14
9
  def initialize uri, username=nil, password=nil, start_offset=nil, usual=true, archived=false, id=nil
@@ -32,7 +27,6 @@ class SSHLoader < Source
32
27
  ## heuristic: use the filename as a label, unless the file
33
28
  ## has a path that probably represents an inbox.
34
29
  @labels = [:unread]
35
- @labels << :inbox unless archived?
36
30
  @labels << File.basename(filename).intern unless File.dirname(filename) =~ /\b(var|usr|spool)\b/
37
31
  end
38
32
 
@@ -41,7 +35,6 @@ class SSHLoader < Source
41
35
  def filename; @parsed_uri.path[1..-1] end
42
36
 
43
37
  def next
44
- return if broken?
45
38
  safely do
46
39
  offset, labels = @loader.next
47
40
  self.cur_offset = @loader.cur_offset # superclass keeps @cur_offset which is used by yaml
@@ -62,11 +55,9 @@ class SSHLoader < Source
62
55
  def safely
63
56
  begin
64
57
  yield
65
- rescue Net::SSH::Exception, SocketError, SSHFileError, SystemCallError => e
58
+ rescue Net::SSH::Exception, SocketError, SSHFileError, SystemCallError, IOError => e
66
59
  m = "error communicating with SSH server #{host} (#{e.class.name}): #{e.message}"
67
- Redwood::log m
68
- self.broken_msg = @loader.broken_msg = m
69
- raise SourceError, m
60
+ raise FatalSourceError, m
70
61
  end
71
62
  end
72
63
 
@@ -138,7 +138,6 @@ class Message
138
138
  end
139
139
  private :read_header
140
140
 
141
- def broken?; @source.broken?; end
142
141
  def snippet; @snippet || chunks && @snippet; end
143
142
  def is_list_message?; !@list_address.nil?; end
144
143
  def is_draft?; DraftLoader === @source; end
@@ -148,8 +147,7 @@ class Message
148
147
  end
149
148
 
150
149
  def save index
151
- return if broken?
152
- index.update_message self if @dirty
150
+ index.sync_message self if @dirty
153
151
  @dirty = false
154
152
  end
155
153
 
@@ -177,8 +175,8 @@ class Message
177
175
  ## this is called when the message body needs to actually be loaded.
178
176
  def load_from_source!
179
177
  @chunks ||=
180
- if @source.broken?
181
- [Text.new(error_message(@source.broken_msg.split("\n")))]
178
+ if @source.has_errors?
179
+ [Text.new(error_message(@source.error.message.split("\n")))]
182
180
  else
183
181
  begin
184
182
  ## we need to re-read the header because it contains information
@@ -192,6 +190,9 @@ class Message
192
190
  read_header @source.load_header(@source_info)
193
191
  message_to_chunks @source.load_message(@source_info)
194
192
  rescue SourceError, SocketError, MessageFormatError => e
193
+ ## we need force_to_top here otherwise this window will cover
194
+ ## up the error message one
195
+ Redwood::report_broken_sources :force_to_top => true
195
196
  [Text.new(error_message(e.message))]
196
197
  end
197
198
  end
@@ -202,8 +203,13 @@ class Message
202
203
  #@snippet...
203
204
 
204
205
  ***********************************************************************
205
- * An error occurred while loading this message. It is possible that *
206
- * the source has changed, or (in the case of remote sources) is down. *
206
+ An error occurred while loading this message. It is possible that
207
+ the source has changed, or (in the case of remote sources) is down.
208
+ You can check the log for errors, though hopefully an error window
209
+ should have popped up at some point.
210
+
211
+ The message location was:
212
+ #@source##@source_info
207
213
  ***********************************************************************
208
214
 
209
215
  The error message was:
@@ -259,7 +265,7 @@ private
259
265
  ret = [] <<
260
266
  case m.header.content_type
261
267
  when "text/plain", nil
262
- m.body && body = m.decode or raise MessageFormatError, "for some bizarre reason, RubyMail was unable to parse this message."
268
+ m.body && body = m.decode or raise MessageFormatError, "For some bizarre reason, RubyMail was unable to parse this message."
263
269
  text_to_chunks body.normalize_whitespace.split("\n")
264
270
  when /^multipart\//
265
271
  nil
@@ -81,7 +81,7 @@ EOS
81
81
  begin
82
82
  File.open(fn, "w") { |f| yield f }
83
83
  BufferManager.flash "Successfully wrote #{fn}."
84
- rescue SystemCallError => e
84
+ rescue SystemCallError, IOError => e
85
85
  BufferManager.flash "Error writing to file: #{e.message}"
86
86
  end
87
87
  end
@@ -158,7 +158,7 @@ EOS
158
158
  end
159
159
 
160
160
  f.puts
161
- f.puts @body
161
+ f.puts @body.map { |l| l =~ /^From / ? ">#{l}" : l }
162
162
  end
163
163
  end
164
164
 
@@ -21,7 +21,10 @@ class InboxMode < ThreadIndexMode
21
21
  end
22
22
 
23
23
  def multi_archive threads
24
- threads.each { |t| remove_label_and_hide_thread t, :inbox }
24
+ threads.each do |t|
25
+ t.remove_label :inbox
26
+ hide_thread t
27
+ end
25
28
  regen_text
26
29
  end
27
30
 
@@ -61,7 +61,7 @@ protected
61
61
  return false unless @curpos < lines - 1
62
62
  if @curpos >= botline - 1
63
63
  page_down
64
- set_cursor_pos [topline + 1, botline].min
64
+ set_cursor_pos topline
65
65
  else
66
66
  @curpos += 1
67
67
  unless buffer.dirty?
@@ -77,8 +77,9 @@ protected
77
77
  def cursor_up
78
78
  return false unless @curpos > @cursor_top
79
79
  if @curpos == topline
80
+ old_topline = topline
80
81
  page_up
81
- set_cursor_pos [botline - 2, topline].max
82
+ set_cursor_pos [old_topline - 1, topline].max
82
83
  else
83
84
  @curpos -= 1
84
85
  unless buffer.dirty?
@@ -119,7 +119,7 @@ protected
119
119
  def reply_body_lines m
120
120
  lines = ["Excerpts from #{@m.from.name}'s message of #{@m.date}:"] +
121
121
  m.basic_body_lines.map { |l| "> #{l}" }
122
- lines.pop while lines.last !~ /[:alpha:]/
122
+ lines.pop while lines.last =~ /^\s*$/
123
123
  lines
124
124
  end
125
125
 
@@ -56,11 +56,7 @@ class ThreadIndexMode < LineCursorMode
56
56
 
57
57
  ## open up a thread view window
58
58
  def select t=nil
59
- t ||= @threads[curpos]
60
-
61
- ## this isn't working entirely. TODO:figure out why
62
- # t = t.clone # required so that messages added later on don't completely
63
- # screw everything up
59
+ t ||= @threads[curpos] or return
64
60
 
65
61
  ## TODO: don't regen text completely
66
62
  Redwood::reporting_thread do
@@ -74,6 +70,10 @@ class ThreadIndexMode < LineCursorMode
74
70
  BufferManager.draw_screen # lame TODO: make this unnecessary
75
71
  ## the first draw_screen is needed before topline and botline
76
72
  ## are set, and the second to show the cursor having moved
73
+
74
+ t.remove_label :unread
75
+ update_text_for_line curpos
76
+ UpdateManager.relay self, :read, t
77
77
  end
78
78
  end
79
79
 
@@ -275,7 +275,7 @@ class ThreadIndexMode < LineCursorMode
275
275
  def apply_to_tagged; @tags.apply_to_tagged; end
276
276
 
277
277
  def edit_labels
278
- thread = @threads[curpos]
278
+ thread = @threads[curpos] or return
279
279
  speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
280
280
  keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
281
281
  label_string = modifyl.join(" ")
@@ -3,7 +3,7 @@ module Redwood
3
3
  class ThreadViewMode < LineCursorMode
4
4
  ## this holds all info we need to lay out a message
5
5
  class Layout
6
- attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color
6
+ attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new
7
7
  end
8
8
 
9
9
  DATE_FORMAT = "%B %e %Y %l:%M%P"
@@ -26,6 +26,7 @@ class ThreadViewMode < LineCursorMode
26
26
  k.add :edit_as_new, "Edit message as new", 'D'
27
27
  k.add :save_to_disk, "Save message/attachment to disk", 's'
28
28
  k.add :search, "Search for messages from particular people", 'S'
29
+ k.add :compose, "Compose message to person", 'm'
29
30
  k.add :archive_and_kill, "Archive thread and kill buffer", 'A'
30
31
  end
31
32
 
@@ -52,6 +53,8 @@ class ThreadViewMode < LineCursorMode
52
53
  @layout[m] = Layout.new
53
54
  @layout[m].state = initial_state_for m
54
55
  @layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color
56
+ @layout[m].star_color = altcolor ? :alternate_starred_patina_color : :starred_patina_color
57
+ @layout[m].orig_new = m.has_label? :unread
55
58
  altcolor = !altcolor
56
59
  if latest_date.nil? || m.date > latest_date
57
60
  latest_date = m.date
@@ -111,10 +114,22 @@ class ThreadViewMode < LineCursorMode
111
114
  def search
112
115
  p = @person_lines[curpos] or return
113
116
  mode = PersonSearchResultsMode.new [p]
114
- BufferManager.spawn "search for #{p.name}", mode
117
+ BufferManager.spawn "Search for #{p.name}", mode
115
118
  mode.load_threads :num => mode.buffer.content_height
116
119
  end
117
120
 
121
+ def compose
122
+ p = @person_lines[curpos]
123
+ mode =
124
+ if p
125
+ ComposeMode.new :to => [p]
126
+ else
127
+ ComposeMode.new
128
+ end
129
+ BufferManager.spawn "Compose message", mode
130
+ mode.edit
131
+ end
132
+
118
133
  def toggle_starred
119
134
  m = @message_lines[curpos] or return
120
135
  if m.has_label? :starred
@@ -231,7 +246,7 @@ class ThreadViewMode < LineCursorMode
231
246
  end
232
247
 
233
248
  def collapse_non_new_messages
234
- @layout.each { |m, l| l.state = m.has_label?(:unread) ? :open : :closed }
249
+ @layout.each { |m, l| l.state = l.orig_new ? :open : :closed if m.is_a? Message }
235
250
  update
236
251
  end
237
252
 
@@ -246,9 +261,7 @@ class ThreadViewMode < LineCursorMode
246
261
  end
247
262
 
248
263
  def cleanup
249
- @thread.remove_label :unread
250
- UpdateManager.relay self, :read, @thread
251
- @layout = @text = nil
264
+ @layout = @text = nil # for good luck
252
265
  end
253
266
 
254
267
  def archive_and_kill
@@ -290,7 +303,7 @@ private
290
303
  l = @layout[m] or next # TODO: figure out why this is nil sometimes
291
304
 
292
305
  ## build the patina
293
- text = chunk_to_lines m, l.state, @text.length, depth, parent, @layout[m].color
306
+ text = chunk_to_lines m, l.state, @text.length, depth, parent, @layout[m].color, @layout[m].star_color
294
307
 
295
308
  l.top = @text.length
296
309
  l.bot = @text.length + text.length # updated below
@@ -327,7 +340,7 @@ private
327
340
  end
328
341
  end
329
342
 
330
- def message_patina_lines m, state, start, parent, prefix, color
343
+ def message_patina_lines m, state, start, parent, prefix, color, star_color
331
344
  prefix_widget = [color, prefix]
332
345
  widget =
333
346
  case state
@@ -338,7 +351,7 @@ private
338
351
  end
339
352
  imp_widget =
340
353
  if m.has_label?(:starred)
341
- [:starred_patina_color, "* "]
354
+ [star_color, "* "]
342
355
  else
343
356
  [color, " "]
344
357
  end
@@ -398,7 +411,7 @@ private
398
411
  p.longname + (ContactManager.is_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
399
412
  end
400
413
 
401
- def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil
414
+ def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
402
415
  prefix = " " * INDENT_SPACES * depth
403
416
  case chunk
404
417
  when :fake_root
@@ -406,7 +419,7 @@ private
406
419
  when nil
407
420
  [[[:missing_message_color, "#{prefix}<an unreceived message>"]]]
408
421
  when Message
409
- message_patina_lines(chunk, state, start, parent, prefix, color) +
422
+ message_patina_lines(chunk, state, start, parent, prefix, color, star_color) +
410
423
  (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
411
424
  when Message::Attachment
412
425
  [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
@@ -43,10 +43,17 @@ class PollManager
43
43
  @mutex.synchronize do
44
44
  Index.usual_sources.each do |source|
45
45
  # yield "source #{source} is done? #{source.done?} (cur_offset #{source.cur_offset} >= #{source.end_offset})"
46
- yield "Loading from #{source}... " unless source.done? || source.broken?
46
+ begin
47
+ yield "Loading from #{source}... " unless source.done? || source.has_errors?
48
+ rescue SourceError => e
49
+ Redwood::log "problem getting messages from #{source}: #{e.message}"
50
+ Redwood::report_broken_sources
51
+ next
52
+ end
53
+
47
54
  num = 0
48
55
  numi = 0
49
- add_new_messages_from source do |m, offset, entry|
56
+ add_messages_from source do |m, offset, entry|
50
57
  ## always preserve the labels on disk.
51
58
  m.labels = entry[:label].split(/\s+/).map { |x| x.intern } if entry
52
59
  yield "Found message at #{offset} with labels {#{m.labels * ', '}}"
@@ -69,47 +76,49 @@ class PollManager
69
76
  end
70
77
 
71
78
  ## this is the main mechanism for adding new messages to the
72
- ## index. it's called both by sup-import and by PollMode.
79
+ ## index. it's called both by sup-sync and by PollMode.
73
80
  ##
74
- ## for each new message in the source, this yields the message, the
75
- ## source offset, and the index entry on disk (if any). it expects
76
- ## the yield to return the message (possibly altered in some way),
77
- ## and then adds it (if new) or updates it (if previously seen).
81
+ ## for each message in the source, starting from the source's
82
+ ## starting offset, this methods yields the message, the source
83
+ ## offset, and the index entry on disk (if any). it expects the
84
+ ## yield to return the message (possibly altered in some way), and
85
+ ## then adds it (if new) or updates it (if previously seen).
78
86
  ##
79
- ## the labels of the yielded message are the source labels. it is
80
- ## likely that callers will want to replace these with the index
81
- ## labels, if they exist, so that state is not lost when e.g. a new
82
- ## version of a message from a mailing list comes in.
83
- def add_new_messages_from source
84
- return if source.done? || source.broken?
85
-
86
- source.each do |offset, labels|
87
- if source.broken?
88
- Redwood::log "error loading messages from #{source}: #{source.broken_msg}"
89
- return
90
- end
87
+ ## the labels of the yielded message are the default source
88
+ ## labels. it is likely that callers will want to replace these with
89
+ ## the index labels, if they exist, so that state is not lost when
90
+ ## e.g. a new version of a message from a mailing list comes in.
91
+ def add_messages_from source
92
+ begin
93
+ return if source.done? || source.has_errors?
91
94
 
92
- labels.each { |l| LabelManager << l }
93
-
94
- begin
95
- m = Message.new :source => source, :source_info => offset, :labels => labels
96
- if m.source_marked_read?
97
- m.remove_label :unread
98
- labels.delete :unread
95
+ source.each do |offset, labels|
96
+ if source.has_errors?
97
+ Redwood::log "error loading messages from #{source}: #{source.broken_msg}"
98
+ return
99
99
  end
100
+
101
+ labels.each { |l| LabelManager << l }
102
+ labels += [:inbox] unless source.archived?
103
+
104
+ begin
105
+ m = Message.new :source => source, :source_info => offset, :labels => labels
106
+ if m.source_marked_read?
107
+ m.remove_label :unread
108
+ labels.delete :unread
109
+ end
100
110
 
101
- docid, entry = Index.load_entry_for_id m.id
102
- m = yield m, offset, entry
103
- next unless m
104
- if entry
105
- Index.update_message m, docid, entry
106
- else
107
- Index.add_message m
108
- UpdateManager.relay self, :add, m
111
+ docid, entry = Index.load_entry_for_id m.id
112
+ m = yield(m, offset, entry) or next
113
+ Index.sync_message m, docid, entry
114
+ UpdateManager.relay self, :add, m unless entry
115
+ rescue MessageFormatError => e
116
+ Redwood::log "ignoring erroneous message at #{source}##{offset}: #{e.message}"
109
117
  end
110
- rescue MessageFormatError, SourceError => e
111
- Redwood::log "ignoring erroneous message at #{source}##{offset}: #{e.message}"
112
118
  end
119
+ rescue SourceError => e
120
+ Redwood::log "problem getting messages from #{source}: #{e.message}"
121
+ Redwood::report_broken_sources
113
122
  end
114
123
  end
115
124
  end
@@ -12,7 +12,7 @@ class SentManager
12
12
 
13
13
  def self.source_name; "sup://sent"; end
14
14
  def self.source_id; 9998; end
15
- def new_source; @source = SentLoader.new; end
15
+ def new_source; @source = Recoverable.new SentLoader.new; end
16
16
 
17
17
  def write_sent_message date, from_email
18
18
  need_blank = File.exists?(@fn) && !File.zero?(@fn)
@@ -22,8 +22,8 @@ class SentManager
22
22
  yield f
23
23
  end
24
24
  @source.each do |offset, labels|
25
- m = Message.new :source => @source, :source_info => offset, :labels => labels
26
- Index.add_message m
25
+ m = Message.new :source => @source, :source_info => offset, :labels => @source.labels
26
+ Index.sync_message m
27
27
  UpdateManager.relay self, :add, m
28
28
  end
29
29
  end
@@ -1,6 +1,8 @@
1
1
  module Redwood
2
2
 
3
3
  class SourceError < StandardError; end
4
+ class OutOfSyncSourceError < SourceError; end
5
+ class FatalSourceError < SourceError; end
4
6
 
5
7
  class Source
6
8
  ## Implementing a new source is typically quite easy, because Sup
@@ -30,31 +32,31 @@ class Source
30
32
  ## - load_message offset
31
33
  ## - raw_header offset
32
34
  ## - raw_full_message offset
35
+ ## - check
33
36
  ## - next (or each, if you prefer)
34
37
  ##
35
38
  ## ... where "offset" really means unique id. (You can tell I
36
39
  ## started with mbox.)
37
40
  ##
38
- ## You can throw SourceErrors from any of those, but we don't catch
39
- ## anything else, so make sure you catch *all* errors and reraise
40
- ## them as SourceErrors, and set broken_msg to something if the
41
- ## source needs to be rescanned.
41
+ ## All exceptions relating to accessing the source must be caught
42
+ ## and rethrown as FatalSourceErrors or OutOfSyncSourceErrors.
43
+ ## OutOfSyncSourceErrors should be used for problems that a call to
44
+ ## sup-sync will fix (namely someone's been playing with the source
45
+ ## from another client); FatalSourceErrors can be used for anything
46
+ ## else (e.g. the imap server is down or the maildir is missing.)
42
47
  ##
43
- ## Also, be sure to make the source thread-safe, since it WILL be
48
+ ## Finally, be sure the source is thread-safe, since it WILL be
44
49
  ## pummeled from multiple threads at once.
45
50
  ##
46
- ## Two examples for you to look at, though sadly neither of them is
47
- ## as simple as I'd like: mbox/loader.rb and imap.rb
51
+ ## Examples for you to look at: mbox/loader.rb, imap.rb, and
52
+ ## maildir.rb.
48
53
 
49
-
50
-
51
- ## dirty? described whether cur_offset has changed, which means the
52
- ## source info needs to be re-saved to sources.yaml.
54
+ ## let's begin!
53
55
  ##
54
- ## broken? means no message can be loaded, e.g. IMAP server is
55
- ## down, mbox file is corrupt and needs to be rescanned, etc.
56
+ ## dirty? means cur_offset has changed, so the source info needs to
57
+ ## be re-saved to sources.yaml.
56
58
  bool_reader :usual, :archived, :dirty
57
- attr_reader :uri, :cur_offset, :broken_msg
59
+ attr_reader :uri, :cur_offset
58
60
  attr_accessor :id
59
61
 
60
62
  def initialize uri, initial_offset=nil, usual=true, archived=false, id=nil
@@ -64,41 +66,26 @@ class Source
64
66
  @archived = archived
65
67
  @id = id
66
68
  @dirty = false
67
- @broken_msg = nil
68
69
  end
69
70
 
70
- def broken?; !@broken_msg.nil?; end
71
71
  def to_s; @uri.to_s; end
72
72
  def seek_to! o; self.cur_offset = o; end
73
- def reset!
74
- return if broken?
75
- begin
76
- seek_to! start_offset
77
- rescue SourceError
78
- end
79
- end
80
- def == o; o.to_s == to_s; end
81
- def done?;
82
- return true if broken?
83
- begin
84
- (self.cur_offset ||= start_offset) >= end_offset
85
- rescue SourceError => e
86
- true
87
- end
88
- end
73
+ def reset!; seek_to! start_offset; end
74
+ def == o; o.uri == uri; end
75
+ def done?; (self.cur_offset ||= start_offset) >= end_offset; end
89
76
  def is_source_for? uri; URI(self.uri) == URI(uri); end
90
77
 
78
+ ## check should throw a FatalSourceError or an OutOfSyncSourcError
79
+ ## if it can detect a problem. it is called when the sup starts up
80
+ ## to proactively notify the user of any source problems.
81
+ def check; end
82
+
91
83
  def each
92
- return if broken?
93
- begin
94
- self.cur_offset ||= start_offset
95
- until done? || broken? # just like life!
96
- n, labels = self.next
97
- raise "no message" unless n
98
- yield n, labels
99
- end
100
- rescue SourceError => e
101
- self.broken_msg = e.message
84
+ self.cur_offset ||= start_offset
85
+ until done?
86
+ n, labels = self.next
87
+ raise "no message" unless n
88
+ yield n, labels
102
89
  end
103
90
  end
104
91
 
@@ -108,11 +95,6 @@ protected
108
95
  @cur_offset = o
109
96
  @dirty = true
110
97
  end
111
-
112
- def broken_msg= m
113
- @broken_msg = m
114
- # Redwood::log "#{to_s}: #{m}"
115
- end
116
98
  end
117
99
 
118
100
  Redwood::register_yaml(Source, %w(uri cur_offset usual archived id))