sup 0.3 → 0.4

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 (52) hide show
  1. data/HACKING +31 -9
  2. data/History.txt +7 -0
  3. data/Manifest.txt +2 -0
  4. data/Rakefile +9 -5
  5. data/bin/sup +81 -57
  6. data/bin/sup-config +1 -1
  7. data/bin/sup-sync +3 -0
  8. data/bin/sup-tweak-labels +127 -0
  9. data/doc/TODO +23 -12
  10. data/lib/sup.rb +13 -11
  11. data/lib/sup/account.rb +25 -12
  12. data/lib/sup/buffer.rb +61 -41
  13. data/lib/sup/colormap.rb +2 -0
  14. data/lib/sup/contact.rb +28 -18
  15. data/lib/sup/crypto.rb +86 -31
  16. data/lib/sup/draft.rb +12 -6
  17. data/lib/sup/horizontal-selector.rb +47 -0
  18. data/lib/sup/imap.rb +50 -37
  19. data/lib/sup/index.rb +76 -13
  20. data/lib/sup/keymap.rb +27 -8
  21. data/lib/sup/maildir.rb +1 -1
  22. data/lib/sup/mbox/loader.rb +1 -1
  23. data/lib/sup/message-chunks.rb +43 -15
  24. data/lib/sup/message.rb +67 -31
  25. data/lib/sup/mode.rb +40 -9
  26. data/lib/sup/modes/completion-mode.rb +1 -1
  27. data/lib/sup/modes/compose-mode.rb +3 -3
  28. data/lib/sup/modes/contact-list-mode.rb +12 -8
  29. data/lib/sup/modes/edit-message-mode.rb +100 -36
  30. data/lib/sup/modes/file-browser-mode.rb +1 -0
  31. data/lib/sup/modes/forward-mode.rb +43 -8
  32. data/lib/sup/modes/inbox-mode.rb +8 -5
  33. data/lib/sup/modes/label-search-results-mode.rb +12 -1
  34. data/lib/sup/modes/line-cursor-mode.rb +4 -7
  35. data/lib/sup/modes/reply-mode.rb +59 -54
  36. data/lib/sup/modes/resume-mode.rb +6 -6
  37. data/lib/sup/modes/scroll-mode.rb +4 -3
  38. data/lib/sup/modes/search-results-mode.rb +8 -5
  39. data/lib/sup/modes/text-mode.rb +19 -2
  40. data/lib/sup/modes/thread-index-mode.rb +109 -40
  41. data/lib/sup/modes/thread-view-mode.rb +180 -49
  42. data/lib/sup/person.rb +3 -3
  43. data/lib/sup/poll.rb +9 -8
  44. data/lib/sup/rfc2047.rb +7 -1
  45. data/lib/sup/sent.rb +1 -1
  46. data/lib/sup/tagger.rb +10 -4
  47. data/lib/sup/textfield.rb +7 -7
  48. data/lib/sup/thread.rb +86 -49
  49. data/lib/sup/update.rb +11 -0
  50. data/lib/sup/util.rb +74 -34
  51. data/test/test_message.rb +441 -0
  52. metadata +136 -117
@@ -35,17 +35,12 @@ class Mode
35
35
  @buffer = nil
36
36
  end
37
37
 
38
- ## turns an input keystroke into an action symbol
39
38
  def resolve_input c
40
- ## try all keymaps in order of age
41
- action = nil
42
- klass = self.class
43
-
44
- ancestors.each do |klass|
45
- action = @@keymaps.member?(klass) && @@keymaps[klass].action_for(c)
39
+ ancestors.each do |klass| # try all keymaps in order of ancestry
40
+ next unless @@keymaps.member?(klass)
41
+ action = BufferManager.resolve_input_with_keymap c, @@keymaps[klass]
46
42
  return action if action
47
43
  end
48
-
49
44
  nil
50
45
  end
51
46
 
@@ -75,7 +70,8 @@ EOS
75
70
  end.compact.join "\n"
76
71
  end
77
72
 
78
- ## helper function
73
+ ### helper functions
74
+
79
75
  def save_to_file fn
80
76
  if File.exists? fn
81
77
  return unless BufferManager.ask_yes_or_no "File exists. Overwrite?"
@@ -87,6 +83,41 @@ EOS
87
83
  BufferManager.flash "Error writing to file: #{e.message}"
88
84
  end
89
85
  end
86
+
87
+ def pipe_to_process command
88
+ Open3.popen3(command) do |input, output, error|
89
+ err, data, * = IO.select [error], [input], nil
90
+
91
+ unless err.empty?
92
+ message = err.first.read
93
+ if message =~ /^\s*$/
94
+ Redwood::log "error running #{command} (but no error message)"
95
+ BufferManager.flash "Error running #{command}!"
96
+ else
97
+ Redwood::log "error running #{command}: #{message}"
98
+ BufferManager.flash "Error: #{message}"
99
+ end
100
+ return
101
+ end
102
+
103
+ data = data.first
104
+ data.sync = false # buffer input
105
+
106
+ yield data
107
+ data.close # output will block unless input is closed
108
+
109
+ ## BUG?: shows errors or output but not both....
110
+ data, * = IO.select [output, error], nil, nil
111
+ data = data.first
112
+
113
+ if data.eof
114
+ BufferManager.flash "'#{command}' done!"
115
+ nil
116
+ else
117
+ data.read
118
+ end
119
+ end
120
+ end
90
121
  end
91
122
 
92
123
  end
@@ -28,7 +28,7 @@ private
28
28
  def update_lines
29
29
  width = buffer.content_width
30
30
  max_length = @list.max_of { |s| s.length }
31
- num_per = buffer.content_width / (max_length + INTERSTITIAL.length)
31
+ num_per = [1, buffer.content_width / (max_length + INTERSTITIAL.length)].max
32
32
  @lines = [@header].compact
33
33
  @list.each_with_index do |s, i|
34
34
  if @prefix_len
@@ -21,9 +21,9 @@ class ComposeMode < EditMessageMode
21
21
 
22
22
  def self.spawn_nicely opts={}
23
23
  to = opts[:to] || BufferManager.ask_for_contacts(:people, "To: ") or return
24
- cc = opts[:cc] || BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc]
25
- bcc = opts[:bcc] || BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc]
26
- subj = opts[:subj] || BufferManager.ask(:subject, "Subject: ") or return if $config[:ask_for_subject]
24
+ cc = opts[:cc] || (BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc])
25
+ bcc = opts[:bcc] || (BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc])
26
+ subj = opts[:subj] || (BufferManager.ask(:subject, "Subject: ") or return if $config[:ask_for_subject])
27
27
 
28
28
  mode = ComposeMode.new :from => opts[:from], :to => to, :cc => cc, :bcc => bcc, :subj => subj
29
29
  BufferManager.spawn "New Message", mode
@@ -2,12 +2,16 @@ module Redwood
2
2
 
3
3
  module CanAliasContacts
4
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
5
+ aalias = BufferManager.ask(:alias, "Alias for #{p.longname}: ", ContactManager.alias_for(p))
6
+ return if aalias.nil?
7
+ aalias = nil if aalias.empty? # allow empty aliases
8
+
9
+ name = BufferManager.ask(:name, "Name for #{p.longname}: ", p.name)
10
+ return if name.nil? || name.empty? # don't allow empty names
11
+ p.name = name
12
+
13
+ ContactManager.update_alias p, aalias
14
+ BufferManager.flash "Contact updated!"
11
15
  end
12
16
  end
13
17
 
@@ -17,7 +21,7 @@ class ContactListMode < LineCursorMode
17
21
  register_keymap do |k|
18
22
  k.add :load_more, "Load #{LOAD_MORE_CONTACTS_NUM} more contacts", 'M'
19
23
  k.add :reload, "Drop contact list and reload", 'D'
20
- k.add :alias, "Edit nickname/alias for contact", 'a'
24
+ k.add :alias, "Edit alias/or name for contact", 'a', 'i'
21
25
  k.add :toggle_tagged, "Tag/untag current line", 't'
22
26
  k.add :apply_to_tagged, "Apply next command to all tagged items", ';'
23
27
  k.add :search, "Search for messages from particular people", 'S'
@@ -103,7 +107,7 @@ class ContactListMode < LineCursorMode
103
107
 
104
108
  def load
105
109
  @num ||= buffer.content_height
106
- @user_contacts = ContactManager.contacts
110
+ @user_contacts = ContactManager.contacts_with_aliases
107
111
  num = [@num - @user_contacts.length, 0].max
108
112
  BufferManager.say("Loading #{num} contacts from index...") do
109
113
  recentc = Index.load_contacts AccountManager.user_emails, :num => num
@@ -8,6 +8,8 @@ module Redwood
8
8
  class SendmailCommandFailed < StandardError; end
9
9
 
10
10
  class EditMessageMode < LineCursorMode
11
+ DECORATION_LINES = 1
12
+
11
13
  FORCE_HEADERS = %w(From To Cc Bcc Subject)
12
14
  MULTI_HEADERS = %w(To Cc Bcc)
13
15
  NON_EDITABLE_HEADERS = %w(Message-Id Date)
@@ -48,6 +50,8 @@ EOS
48
50
  k.add :save_as_draft, "Save as draft", 'P'
49
51
  k.add :attach_file, "Attach a file", 'a'
50
52
  k.add :delete_attachment, "Delete an attachment", 'd'
53
+ k.add :move_cursor_right, "Move selector to the right", :right, 'l'
54
+ k.add :move_cursor_left, "Move selector to the left", :left, 'h'
51
55
  end
52
56
 
53
57
  def initialize opts={}
@@ -55,12 +59,26 @@ EOS
55
59
  @header_lines = []
56
60
 
57
61
  @body = opts.delete(:body) || []
58
- @body += sig_lines if $config[:edit_signature]
62
+ @body += sig_lines if $config[:edit_signature] && !opts.delete(:have_signature)
63
+
64
+ if opts[:attachments]
65
+ @attachments = opts[:attachments].values
66
+ @attachment_names = opts[:attachments].keys
67
+ else
68
+ @attachments = []
69
+ @attachment_names = []
70
+ end
59
71
 
60
- @attachments = []
61
72
  @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>"
62
73
  @edited = false
63
- @skip_top_rows = opts[:skip_top_rows] || 0
74
+ @selectors = []
75
+ @selector_label_width = 0
76
+
77
+ @crypto_selector =
78
+ if CryptoManager.have_crypto?
79
+ HorizontalSelector.new "Crypto:", [:none] + CryptoManager::OUTGOING_MESSAGE_OPERATIONS.keys, ["None"] + CryptoManager::OUTGOING_MESSAGE_OPERATIONS.values
80
+ end
81
+ add_selector @crypto_selector if @crypto_selector
64
82
 
65
83
  HookManager.run "before-edit", :header => @header, :body => @body
66
84
 
@@ -68,17 +86,31 @@ EOS
68
86
  regen_text
69
87
  end
70
88
 
71
- def lines; @text.length end
72
- def [] i; @text[i] end
89
+ def lines; @text.length + (@selectors.empty? ? 0 : (@selectors.length + DECORATION_LINES)) end
90
+
91
+ def [] i
92
+ if @selectors.empty?
93
+ @text[i]
94
+ elsif i < @selectors.length
95
+ @selectors[i].line @selector_label_width
96
+ elsif i == @selectors.length
97
+ ""
98
+ else
99
+ @text[i - @selectors.length - DECORATION_LINES]
100
+ end
101
+ end
73
102
 
74
- ## a hook
103
+ ## hook for subclasses. i hate this style of programming.
75
104
  def handle_new_text header, body; end
76
105
 
77
106
  def edit_message_or_field
78
- if (curpos - @skip_top_rows) >= @header_lines.length
107
+ lines = DECORATION_LINES + @selectors.size
108
+ if lines > curpos
109
+ return
110
+ elsif (curpos - lines) >= @header_lines.length
79
111
  edit_message
80
112
  else
81
- edit_field @header_lines[curpos - @skip_top_rows]
113
+ edit_field @header_lines[curpos - lines]
82
114
  end
83
115
  end
84
116
 
@@ -116,20 +148,45 @@ EOS
116
148
  def attach_file
117
149
  fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
118
150
  return unless fn
119
- @attachments << Pathname.new(fn)
151
+ @attachments << RMail::Message.make_file_attachment(fn)
152
+ @attachment_names << fn
120
153
  update
121
154
  end
122
155
 
123
156
  def delete_attachment
124
- i = (curpos - @skip_top_rows) - @attachment_lines_offset
125
- if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachments[i]}?")
157
+ i = curpos - @attachment_lines_offset - DECORATION_LINES - 1
158
+ if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachment_names[i]}?")
126
159
  @attachments.delete_at i
160
+ @attachment_names.delete_at i
127
161
  update
128
162
  end
129
163
  end
130
164
 
131
165
  protected
132
166
 
167
+ def move_cursor_left
168
+ if curpos < @selectors.length
169
+ @selectors[curpos].roll_left
170
+ buffer.mark_dirty
171
+ else
172
+ col_left
173
+ end
174
+ end
175
+
176
+ def move_cursor_right
177
+ if curpos < @selectors.length
178
+ @selectors[curpos].roll_right
179
+ buffer.mark_dirty
180
+ else
181
+ col_right
182
+ end
183
+ end
184
+
185
+ def add_selector s
186
+ @selectors << s
187
+ @selector_label_width = [@selector_label_width, s.label.length].max
188
+ end
189
+
133
190
  def update
134
191
  regen_text
135
192
  buffer.mark_dirty if buffer
@@ -145,7 +202,7 @@ protected
145
202
  unless @attachments.empty?
146
203
  @text += [""]
147
204
  @attachment_lines_offset = @text.length
148
- @text += @attachments.map { |f| [[:attachment_color, "+ Attachment: #{f} (#{f.human_size})"]] }
205
+ @text += (0 ... @attachments.size).map { |i| [[:attachment_color, "+ Attachment: #{@attachment_names[i]} (#{@attachments[i].body.size.to_human_size})"]] }
149
206
  end
150
207
  end
151
208
 
@@ -208,7 +265,6 @@ protected
208
265
  return false if $config[:confirm_no_attachments] && mentions_attachments? && @attachments.size == 0 && !BufferManager.ask_yes_or_no("You haven't added any attachments. Really send?")#" stupid ruby-mode
209
266
  return false if $config[:confirm_top_posting] && top_posting? && !BufferManager.ask_yes_or_no("You're top-posting. That makes you a bad person. Really send?") #" stupid ruby-mode
210
267
 
211
- date = Time.now
212
268
  from_email =
213
269
  if @header["From"] =~ /<?(\S+@(\S+?))>?$/
214
270
  $1
@@ -220,13 +276,15 @@ protected
220
276
  BufferManager.flash "Sending..."
221
277
 
222
278
  begin
223
- IO.popen(acct.sendmail, "w") { |p| write_full_message_to p, date, false }
279
+ date = Time.now
280
+ m = build_message date
281
+ IO.popen(acct.sendmail, "w") { |p| p.puts m }
224
282
  raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
225
- SentManager.write_sent_message(date, from_email) { |f| write_full_message_to f, date, true }
283
+ SentManager.write_sent_message(date, from_email) { |f| f.puts sanitize_body(m.to_s) }
226
284
  BufferManager.kill_buffer buffer
227
285
  BufferManager.flash "Message sent!"
228
286
  true
229
- rescue SystemCallError, SendmailCommandFailed => e
287
+ rescue SystemCallError, SendmailCommandFailed, CryptoManager::Error => e
230
288
  Redwood::log "Problem sending mail: #{e.message}"
231
289
  BufferManager.flash "Problem sending mail: #{e.message}"
232
290
  false
@@ -239,8 +297,32 @@ protected
239
297
  BufferManager.flash "Saved for later editing."
240
298
  end
241
299
 
242
- def write_full_message_to f, date=Time.now, escape=false
300
+ def build_message date
243
301
  m = RMail::Message.new
302
+ m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
303
+ m.body = @body.join
304
+ m.body = m.body
305
+ m.body += sig_lines.join("\n") unless $config[:edit_signature]
306
+
307
+ ## there are attachments, so wrap body in an attachment of its own
308
+ unless @attachments.empty?
309
+ body_m = m
310
+ body_m.header["Content-Disposition"] = "inline"
311
+ m = RMail::Message.new
312
+
313
+ m.add_part body_m
314
+ @attachments.each { |a| m.add_part a }
315
+ end
316
+
317
+ ## do whatever crypto transformation is necessary
318
+ if @crypto_selector && @crypto_selector.val != :none
319
+ from_email = PersonManager.person_for(@header["From"]).email
320
+ to_email = (@header["To"] + @header["Cc"] + @header["Bcc"]).map { |p| PersonManager.person_for(p).email }
321
+
322
+ m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
323
+ end
324
+
325
+ ## finally, set the top-level headers
244
326
  @header.each do |k, v|
245
327
  next if v.nil? || v.empty?
246
328
  m.header[k] =
@@ -251,28 +333,10 @@ protected
251
333
  v.join ", "
252
334
  end
253
335
  end
254
-
255
336
  m.header["Date"] = date.rfc2822
256
337
  m.header["Message-Id"] = @message_id
257
338
  m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
258
-
259
- if @attachments.empty?
260
- m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
261
- m.body = @body.join
262
- m.body = sanitize_body m.body if escape
263
- m.body += sig_lines.join("\n") unless $config[:edit_signature]
264
- else
265
- body_m = RMail::Message.new
266
- body_m.body = @body.join
267
- body_m.body = sanitize_body body_m.body if escape
268
- body_m.body += sig_lines.join("\n") unless $config[:edit_signature]
269
- body_m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
270
- body_m.header["Content-Disposition"] = "inline"
271
-
272
- m.add_part body_m
273
- @attachments.each { |fn| m.add_file_attachment fn.to_s }
274
- end
275
- f.puts m.to_s
339
+ m
276
340
  end
277
341
 
278
342
  ## TODO: remove this. redundant with write_full_message_to.
@@ -38,6 +38,7 @@ protected
38
38
 
39
39
  def reload
40
40
  regen_text
41
+ jump_to_start
41
42
  buffer.mark_dirty
42
43
  end
43
44
 
@@ -1,28 +1,63 @@
1
1
  module Redwood
2
2
 
3
3
  class ForwardMode < EditMessageMode
4
-
5
- ## todo: share some of this with reply-mode
6
- def initialize m, opts={}
4
+ ## TODO: share some of this with reply-mode
5
+ def initialize opts={}
7
6
  header = {
8
7
  "From" => AccountManager.default_account.full_address,
9
- "Subject" => "Fwd: #{m.subj}",
10
8
  }
11
9
 
10
+ header["Subject"] =
11
+ if opts[:message]
12
+ "Fwd: " + opts[:message].subj
13
+ elsif opts[:attachments]
14
+ "Fwd: " + opts[:attachments].keys.join(", ")
15
+ end
16
+
12
17
  header["To"] = opts[:to].map { |p| p.full_address }.join(", ") if opts[:to]
13
18
  header["Cc"] = opts[:cc].map { |p| p.full_address }.join(", ") if opts[:cc]
14
19
  header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
15
20
 
16
- super :header => header, :body => forward_body_lines(m)
21
+ body =
22
+ if opts[:message]
23
+ forward_body_lines(opts[:message])
24
+ elsif opts[:attachments]
25
+ ["Note: #{opts[:attachments].size.pluralize 'attachment'}."]
26
+ end
27
+
28
+ super :header => header, :body => body, :attachments => opts[:attachments]
17
29
  end
18
30
 
19
- def self.spawn_nicely m, opts={}
31
+ def self.spawn_nicely opts={}
20
32
  to = opts[:to] || BufferManager.ask_for_contacts(:people, "To: ") or return
21
33
  cc = opts[:cc] || BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc]
22
34
  bcc = opts[:bcc] || BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc]
23
35
 
24
- mode = ForwardMode.new m, :to => to, :cc => cc, :bcc => bcc
25
- BufferManager.spawn "Forwarding #{m.subj}", mode
36
+ attachment_hash = {}
37
+ attachments = opts[:attachments] || []
38
+
39
+ if(m = opts[:message])
40
+ m.load_from_source! # read the full message in. you know, maybe i should just make Message#chunks do this....
41
+ attachments += m.chunks.select { |c| c.is_a?(Chunk::Attachment) && !c.quotable? }
42
+ end
43
+
44
+ attachments.each do |c|
45
+ mime_type = MIME::Types[c.content_type].first || MIME::Types["application/octet-stream"]
46
+ attachment_hash[c.filename] = RMail::Message.make_attachment c.raw_content, mime_type.content_type, mime_type.encoding, c.filename
47
+ end
48
+
49
+ mode = ForwardMode.new :message => opts[:message], :to => to, :cc => cc, :bcc => bcc, :attachments => attachment_hash
50
+
51
+ title = "Forwarding " +
52
+ if opts[:message]
53
+ opts[:message].subj
54
+ elsif attachments
55
+ attachment_hash.keys.join(", ")
56
+ else
57
+ "something"
58
+ end
59
+
60
+ BufferManager.spawn title, mode
26
61
  mode.edit_message
27
62
  end
28
63
 
@@ -38,11 +38,14 @@ class InboxMode < ThreadIndexMode
38
38
  regen_text
39
39
  end
40
40
 
41
- def handle_archived_update sender, t
42
- if contains_thread? t
43
- hide_thread t
44
- regen_text
45
- end
41
+ def handle_unarchived_update sender, m
42
+ add_or_unhide m
43
+ end
44
+
45
+ def handle_archived_update sender, m
46
+ t = thread_containing(m) or return
47
+ hide_thread t
48
+ regen_text
46
49
  end
47
50
 
48
51
  def status