sup 0.0.6 → 0.0.7

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.

@@ -29,7 +29,6 @@ class Loader < Source
29
29
 
30
30
  def start_offset; 0; end
31
31
  def end_offset; File.size @f; end
32
- def pct_done; 100.0 * cur_offset.to_f / end_offset.to_f; end
33
32
 
34
33
  def load_header offset
35
34
  header = nil
@@ -92,6 +92,9 @@ class SSHFile
92
92
  REASONABLE_TRANSFER_SIZE = 1024 * 32
93
93
  SIZE_CHECK_INTERVAL = 60 * 1 # seconds
94
94
 
95
+ ## upon these errors we'll try to rereconnect a few times
96
+ RECOVERABLE_ERRORS = [ Errno::EPIPE, Errno::ETIMEDOUT ]
97
+
95
98
  @@shells = {}
96
99
  @@shells_mutex = Mutex.new
97
100
 
@@ -105,46 +108,15 @@ class SSHFile
105
108
  @say_id = nil
106
109
  @broken_msg = nil
107
110
  @shell = nil
108
- @shell_mutex = Mutex.new
111
+ @shell_mutex = nil
109
112
  @buf_mutex = Mutex.new
110
113
  end
111
114
 
112
- def to_s; "mbox+ssh://#@host/#@fn"; end ## TODO: remove thisis EVILness
115
+ def to_s; "mbox+ssh://#@host/#@fn"; end ## TODO: remove this EVILness
113
116
  def broken?; !@broken_msg.nil?; end
114
117
 
115
- ## TODO: share this code with imap
116
- def say s
117
- @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
118
- Redwood::log s
119
- end
120
- def shutup
121
- BufferManager.clear @say_id if BufferManager.instantiated? && @say_id
122
- @say_id = nil
123
- end
124
- private :say, :shutup
125
-
126
118
  def connect
127
- raise SSHFileError, @broken_msg if broken?
128
- return if @shell
129
-
130
- @key = [@host, @ssh_opts[:username]]
131
- begin
132
- @shell = @@shells_mutex.synchronize do
133
- unless @@shells.member? @key
134
- say "Opening SSH connection to #{@host} for #@fn..."
135
- #raise SSHFileError, "simulated SSH file error"
136
- session = Net::SSH.start @host, @ssh_opts
137
- say "Starting SSH shell..."
138
- @@shells[@key] = session.shell.sync
139
- end
140
- @@shells[@key]
141
- end
142
-
143
- say "Checking for #@fn..."
144
- @shell_mutex.synchronize { raise Errno::ENOENT, @fn unless @shell.test("-e #@fn").status == 0 }
145
- ensure
146
- shutup
147
- end
119
+ do_remote nil
148
120
  end
149
121
 
150
122
  def eof?; @offset >= size; end
@@ -180,32 +152,71 @@ class SSHFile
180
152
 
181
153
  private
182
154
 
155
+ ## TODO: share this code with imap
156
+ def say s
157
+ @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
158
+ Redwood::log s
159
+ end
160
+
161
+ def shutup
162
+ BufferManager.clear @say_id if BufferManager.instantiated? && @say_id
163
+ @say_id = nil
164
+ end
165
+
166
+ def unsafe_connect
167
+ raise SSHFileError, @broken_msg if broken?
168
+ return if @shell
169
+
170
+ @key = [@host, @ssh_opts[:username]]
171
+ begin
172
+ @shell, @shell_mutex = @@shells_mutex.synchronize do
173
+ unless @@shells.member? @key
174
+ say "Opening SSH connection to #{@host} for #@fn..."
175
+ #raise SSHFileError, "simulated SSH file error"
176
+ session = Net::SSH.start @host, @ssh_opts
177
+ say "Starting SSH shell..."
178
+ @@shells[@key] = [session.shell.sync, Mutex.new]
179
+ end
180
+ @@shells[@key]
181
+ end
182
+
183
+ say "Checking for #@fn..."
184
+ @shell_mutex.synchronize { raise Errno::ENOENT, @fn unless @shell.test("-e #@fn").status == 0 }
185
+ ensure
186
+ shutup
187
+ end
188
+ end
189
+
183
190
  def do_remote cmd, expected_size=0
191
+ retries = 0
192
+ result = nil
184
193
  begin
185
- retries = 0
186
- connect
187
- # MBox::debug "sending command: #{cmd.inspect}"
188
194
  begin
189
- result = @shell_mutex.synchronize { x = @shell.send_command cmd; sleep 0.25; x }
190
- raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{(result.stderr || result.stdout || "")[0 .. 100]}" unless result.status == 0
191
- rescue Net::SSH::Exception # these happen occasionally for no apparent reason. gotta love that nondeterminism!
192
- retry if (retries += 1) <= 3
193
- raise
194
- rescue Errno::EPIPE
195
- if (retries += 1) <= e
195
+ unsafe_connect
196
+ if cmd
197
+ # MBox::debug "sending command: #{cmd.inspect}"
198
+ result = @shell_mutex.synchronize { x = @shell.send_command cmd; sleep 0.25; x }
199
+ raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{(result.stderr || result.stdout || "")[0 .. 100]}" unless result.status == 0
200
+ end
201
+
202
+ ## Net::SSH::Exceptions seem to happen every once in a while for
203
+ ## no good reason.
204
+ rescue Net::SSH::Exception, *RECOVERABLE_ERRORS
205
+ if (retries += 1) <= 3
196
206
  @@shells_mutex.synchronize do
197
207
  @shell = nil
198
208
  @@shells[@key] = nil
199
209
  end
200
- connect
201
210
  retry
202
211
  end
212
+ raise
203
213
  end
204
- rescue Net::SSH::Exception, SSHFileError, Errno::ENOENT => e
214
+ rescue Net::SSH::Exception, SSHFileError, SystemCallError => e
205
215
  @broken_msg = e.message
206
216
  raise
207
217
  end
208
- result.stdout
218
+
219
+ result.stdout if cmd
209
220
  end
210
221
 
211
222
  def get_bytes offset, size
@@ -36,26 +36,21 @@ class SSHLoader < Source
36
36
  @labels << File.basename(filename).intern unless File.dirname(filename) =~ /\b(var|usr|spool)\b/
37
37
  end
38
38
 
39
+ def connect; safely { @f.connect }; end
39
40
  def host; @parsed_uri.host; end
40
41
  def filename; @parsed_uri.path[1..-1] end
41
42
 
42
43
  def next
43
44
  return if broken?
44
- begin
45
+ safely do
45
46
  offset, labels = @loader.next
46
47
  self.cur_offset = @loader.cur_offset # superclass keeps @cur_offset which is used by yaml
47
48
  [offset, (labels + @labels).uniq] # add our labels
48
- rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
49
- recover_from e
50
49
  end
51
50
  end
52
51
 
53
52
  def end_offset
54
- begin
55
- @f.size
56
- rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
57
- recover_from e
58
- end
53
+ safely { @f.size }
59
54
  end
60
55
 
61
56
  def cur_offset= o; @cur_offset = @loader.cur_offset = o; @dirty = true; end
@@ -64,21 +59,19 @@ class SSHLoader < Source
64
59
  # def cur_offset; @loader.cur_offset; end # think we'll be ok without this
65
60
  def to_s; @parsed_uri.to_s; end
66
61
 
67
- def recover_from e
68
- m = "error communicating with SSH server #{host} (#{e.class.name}): #{e.message}"
69
- Redwood::log m
70
- self.broken_msg = @loader.broken_msg = m
71
- raise SourceError, m
62
+ def safely
63
+ begin
64
+ yield
65
+ rescue Net::SSH::Exception, SocketError, SSHFileError, SystemCallError => e
66
+ 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
70
+ end
72
71
  end
73
72
 
74
73
  [:start_offset, :load_header, :load_message, :raw_header, :raw_full_message].each do |meth|
75
- define_method meth do |*a|
76
- begin
77
- @loader.send meth, *a
78
- rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
79
- recover_from e
80
- end
81
- end
74
+ define_method(meth) { |*a| safely { @loader.send meth, *a } }
82
75
  end
83
76
  end
84
77
 
data/lib/sup/message.rb CHANGED
@@ -17,6 +17,7 @@ class MessageFormatError < StandardError; end
17
17
  ## appropriately.
18
18
  class Message
19
19
  SNIPPET_LEN = 80
20
+ WRAP_LEN = 80 # wrap at this width
20
21
  RE_PATTERN = /^((re|re[\[\(]\d[\]\)]):\s*)+/i
21
22
 
22
23
  ## some utility methods
@@ -45,6 +46,7 @@ class Message
45
46
 
46
47
  ## TODO: handle unknown mime-types
47
48
  system "/usr/bin/run-mailcap --action=view #{@content_type}:#{@file.path}"
49
+ $? == 0
48
50
  end
49
51
 
50
52
  def to_s; @part.decode; end
@@ -54,7 +56,7 @@ class Message
54
56
  attr_reader :lines
55
57
  def initialize lines
56
58
  ## do some wrapping
57
- @lines = lines.map { |l| l.chomp.wrap 80 }.flatten
59
+ @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten
58
60
  end
59
61
  end
60
62
 
@@ -75,14 +77,14 @@ class Message
75
77
  QUOTE_PATTERN = /^\s{0,4}[>|\}]/
76
78
  BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
77
79
  QUOTE_START_PATTERN = /(^\s*Excerpts from)|(^\s*In message )|(^\s*In article )|(^\s*Quoting )|((wrote|writes|said|says)\s*:\s*$)/
78
- SIG_PATTERN = /(^-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)/
80
+ SIG_PATTERN = /(^-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)|(^\s*--~--~-)/
79
81
  MAX_SIG_DISTANCE = 15 # lines from the end
80
82
  DEFAULT_SUBJECT = "(missing subject)"
81
83
  DEFAULT_SENDER = "(missing sender)"
82
84
 
83
85
  attr_reader :id, :date, :from, :subj, :refs, :replytos, :to, :source,
84
86
  :cc, :bcc, :labels, :list_address, :recipient_email, :replyto,
85
- :source_info
87
+ :source_info, :chunks
86
88
 
87
89
  bool_reader :dirty, :source_marked_read
88
90
 
@@ -95,6 +97,7 @@ class Message
95
97
  @have_snippet = !opts[:snippet].nil?
96
98
  @labels = opts[:labels] || []
97
99
  @dirty = false
100
+ @chunks = nil
98
101
 
99
102
  read_header(opts[:header] || @source.load_header(@source_info))
100
103
  end
@@ -130,13 +133,13 @@ class Message
130
133
  nil
131
134
  end
132
135
 
133
- @recipient_email = header["x-original-to"] || header["envelope-to"] || header["delivered-to"]
136
+ @recipient_email = header["envelope-to"] || header["x-original-to"] || header["delivered-to"]
134
137
  @source_marked_read = header["status"] == "RO"
135
138
  end
136
139
  private :read_header
137
140
 
138
141
  def broken?; @source.broken?; end
139
- def snippet; @snippet || to_chunks && @snippet; end
142
+ def snippet; @snippet || chunks && @snippet; end
140
143
  def is_list_message?; !@list_address.nil?; end
141
144
  def is_draft?; DraftLoader === @source; end
142
145
  def draft_filename
@@ -172,7 +175,7 @@ class Message
172
175
  end
173
176
 
174
177
  ## this is called when the message body needs to actually be loaded.
175
- def to_chunks
178
+ def load_from_source!
176
179
  @chunks ||=
177
180
  if @source.broken?
178
181
  [Text.new(error_message(@source.broken_msg.split("\n")))]
@@ -184,6 +187,8 @@ class Message
184
187
  ## i could just store that in the index, but i think there might
185
188
  ## be other things like that in the future, and i'd rather not
186
189
  ## bloat the index.
190
+ ## actually, it's also the differentiation between to/cc/bcc,
191
+ ## so i will keep this.
187
192
  read_header @source.load_header(@source_info)
188
193
  message_to_chunks @source.load_message(@source_info)
189
194
  rescue SourceError, SocketError, MessageFormatError => e
@@ -223,18 +228,19 @@ EOS
223
228
  end
224
229
 
225
230
  def content
231
+ load_from_source!
226
232
  [
227
233
  from && "#{from.name} #{from.email}",
228
234
  to.map { |p| "#{p.name} #{p.email}" },
229
235
  cc.map { |p| "#{p.name} #{p.email}" },
230
236
  bcc.map { |p| "#{p.name} #{p.email}" },
231
- to_chunks.select { |c| c.is_a? Text }.map { |c| c.lines },
237
+ chunks.select { |c| c.is_a? Text }.map { |c| c.lines },
232
238
  Message.normalize_subj(subj),
233
239
  ].flatten.compact.join " "
234
240
  end
235
241
 
236
242
  def basic_body_lines
237
- to_chunks.find_all { |c| c.is_a?(Text) || c.is_a?(Quote) }.map { |c| c.lines }.flatten
243
+ chunks.find_all { |c| c.is_a?(Text) || c.is_a?(Quote) }.map { |c| c.lines }.flatten
238
244
  end
239
245
 
240
246
  def basic_header_lines
@@ -300,7 +306,7 @@ private
300
306
  when :quote
301
307
  newstate = nil
302
308
 
303
- if line =~ QUOTE_PATTERN || line =~ QUOTE_START_PATTERN || line =~ /^\s*$/
309
+ if line =~ QUOTE_PATTERN || line =~ QUOTE_START_PATTERN #|| line =~ /^\s*$/
304
310
  chunk_lines << line
305
311
  elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE
306
312
  newstate = :sig
data/lib/sup/mode.rb CHANGED
@@ -48,12 +48,9 @@ class Mode
48
48
  end
49
49
 
50
50
  def handle_input c
51
- if(action = resolve_input c)
52
- send action
53
- true
54
- else
55
- false
56
- end
51
+ action = resolve_input(c) or return false
52
+ send action
53
+ true
57
54
  end
58
55
 
59
56
  def help_text
@@ -75,6 +72,19 @@ EOS
75
72
  s
76
73
  end.compact.join "\n"
77
74
  end
75
+
76
+ ## helper function
77
+ def save_to_file fn
78
+ if File.exists? fn
79
+ return unless BufferManager.ask_yes_or_no "File exists. Overwrite?"
80
+ end
81
+ begin
82
+ File.open(fn, "w") { |f| yield f }
83
+ BufferManager.flash "Successfully wrote #{fn}."
84
+ rescue SystemCallError => e
85
+ BufferManager.flash "Error writing to file: #{e.message}"
86
+ end
87
+ end
78
88
  end
79
89
 
80
90
  end
@@ -3,15 +3,19 @@ module Redwood
3
3
  class ComposeMode < EditMessageMode
4
4
  attr_reader :body, :header
5
5
 
6
- def initialize h={}
6
+ def initialize opts={}
7
7
  super()
8
8
  @header = {
9
9
  "From" => AccountManager.default_account.full_address,
10
10
  "Message-Id" => gen_message_id,
11
11
  }
12
12
 
13
- @header["To"] = [h[:to]].flatten.compact.map { |p| p.full_address }
14
- @body = sig_lines
13
+ @header["To"] = opts[:to].map { |p| p.full_address }.join(", ") if opts[:to]
14
+ @header["Cc"] = opts[:cc].map { |p| p.full_address }.join(", ") if opts[:cc]
15
+ @header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
16
+ @header["Subject"] = opts[:subj] if opts[:subj]
17
+
18
+ @body = (opts[:body] || []) + sig_lines
15
19
  regen_text
16
20
  end
17
21
 
@@ -1,12 +1,23 @@
1
1
  module Redwood
2
2
 
3
+ module CanAliasContacts
4
+ def alias_contact p
5
+ a = BufferManager.ask(:alias, "Nickname for #{p.longname}: ", ContactManager.alias_for(p)) or return
6
+ if a.empty?
7
+ ContactManager.drop_contact p
8
+ else
9
+ ContactManager.set_contact p, a
10
+ end
11
+ end
12
+ end
13
+
3
14
  class ContactListMode < LineCursorMode
4
15
  LOAD_MORE_CONTACTS_NUM = 10
5
16
 
6
17
  register_keymap do |k|
7
18
  k.add :load_more, "Load #{LOAD_MORE_CONTACTS_NUM} more contacts", 'M'
8
- k.add :reload, "Reload contacts", 'R'
9
- k.add :alias, "Edit alias for contact", 'a'
19
+ k.add :reload, "Drop contact list and reload", 'D'
20
+ k.add :alias, "Edit nickname/alias for contact", 'a'
10
21
  k.add :toggle_tagged, "Tag/untag current line", 't'
11
22
  k.add :apply_to_tagged, "Apply next command to all tagged items", ';'
12
23
  k.add :search, "Search for messages from particular people", 'S'
@@ -15,10 +26,18 @@ class ContactListMode < LineCursorMode
15
26
  def initialize mode = :regular
16
27
  @mode = mode
17
28
  @tags = Tagger.new self
18
- @num = 0
29
+ @num = nil
30
+ @text = []
19
31
  super()
20
32
  end
21
33
 
34
+ include CanAliasContacts
35
+ def alias
36
+ p = @contacts[curpos] or return
37
+ alias_contact p
38
+ update
39
+ end
40
+
22
41
  def lines; @text.length; end
23
42
  def [] i; @text[i]; end
24
43
 
@@ -31,15 +50,15 @@ class ContactListMode < LineCursorMode
31
50
 
32
51
  def multi_toggle_tagged threads
33
52
  @tags.drop_all_tags
34
- regen_text
53
+ update
35
54
  end
36
55
 
37
56
  def apply_to_tagged; @tags.apply_to_tagged; end
38
57
 
39
- def load; regen_text; end
40
58
  def load_more num=LOAD_MORE_CONTACTS_NUM
41
59
  @num += num
42
- regen_text
60
+ load
61
+ update
43
62
  BufferManager.flash "Added #{num} contacts."
44
63
  end
45
64
 
@@ -59,7 +78,7 @@ class ContactListMode < LineCursorMode
59
78
 
60
79
  def multi_search people
61
80
  mode = PersonSearchResultsMode.new people
62
- BufferManager.spawn "personal search results", mode
81
+ BufferManager.spawn "search for #{people.map { |p| p.name }.join(', ')}", mode
63
82
  mode.load_threads :num => mode.buffer.content_height
64
83
  end
65
84
 
@@ -70,49 +89,55 @@ class ContactListMode < LineCursorMode
70
89
 
71
90
  def reload
72
91
  @tags.drop_all_tags
92
+ @num = nil
73
93
  load
74
94
  end
75
95
 
76
- def alias
77
- p = @contacts[curpos] or return
78
- a = BufferManager.ask(:alias, "alias for #{p.longname}: ", @user_contacts[p]) or return
79
- if a.empty?
80
- ContactManager.drop_contact p
81
- else
82
- ContactManager.set_contact p, a
83
- @user_contacts[p] = a
84
- update_text_for_line curpos
96
+ def load_in_background
97
+ Redwood::reporting_thread do
98
+ load
99
+ update
100
+ BufferManager.draw_screen
85
101
  end
86
102
  end
87
103
 
104
+ def load
105
+ @num ||= buffer.content_height
106
+ @user_contacts = ContactManager.contacts
107
+ num = [@num - @user_contacts.length, 0].max
108
+ BufferManager.say("Loading #{num} contacts from index...") do
109
+ recentc = Index.load_contacts AccountManager.user_emails, :num => num
110
+ @contacts = (@user_contacts + recentc).sort_by { |p| p.sort_by_me }.uniq
111
+ end
112
+ end
113
+
88
114
  protected
89
115
 
116
+ def update
117
+ regen_text
118
+ buffer.mark_dirty if buffer
119
+ end
120
+
90
121
  def update_text_for_line line
91
122
  @text[line] = text_for_contact @contacts[line]
92
- buffer.mark_dirty
123
+ buffer.mark_dirty if buffer
93
124
  end
94
125
 
95
126
  def text_for_contact p
96
- aalias = @user_contacts[p] || ""
127
+ aalias = ContactManager.alias_for(p) || ""
97
128
  [[:tagged_color, @tags.tagged?(p) ? ">" : " "],
98
129
  [:none, sprintf("%-#{@awidth}s %-#{@nwidth}s %s", aalias, p.name, p.email)]]
99
130
  end
100
131
 
101
132
  def regen_text
102
- @user_contacts = ContactManager.contacts.invert
103
- recent = Index.load_contacts AccountManager.user_emails, :num => [@num - @user_contacts.length, 0].max
104
-
105
- @contacts = (@user_contacts.keys + recent.select { |p| !@user_contacts[p] }).sort_by { |p| p.sort_by_me + (p.name || "") + p.email }.remove_successive_dupes
106
-
107
133
  @awidth, @nwidth = 0, 0
108
134
  @contacts.each do |p|
109
- aalias = @user_contacts[p]
135
+ aalias = ContactManager.alias_for(p)
110
136
  @awidth = aalias.length if aalias && aalias.length > @awidth
111
137
  @nwidth = p.name.length if p.name && p.name.length > @nwidth
112
138
  end
113
139
 
114
140
  @text = @contacts.map { |p| text_for_contact p }
115
- buffer.mark_dirty if buffer
116
141
  end
117
142
  end
118
143