sup 0.12.1 → 0.13.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.

Files changed (56) hide show
  1. data.tar.gz.sig +1 -0
  2. data/CONTRIBUTORS +25 -12
  3. data/History.txt +6 -1
  4. data/README.md +70 -0
  5. data/ReleaseNotes +5 -0
  6. data/bin/sup +22 -15
  7. data/bin/sup-add +3 -3
  8. data/bin/sup-config +3 -4
  9. data/bin/sup-import-dump +1 -1
  10. data/bin/sup-sync +1 -1
  11. data/bin/sup-sync-back +2 -2
  12. data/bin/sup-tweak-labels +1 -1
  13. data/lib/sup.rb +39 -23
  14. data/lib/sup/account.rb +4 -0
  15. data/lib/sup/buffer.rb +4 -7
  16. data/lib/sup/colormap.rb +10 -2
  17. data/lib/sup/contact.rb +11 -5
  18. data/lib/sup/crypto.rb +278 -101
  19. data/lib/sup/draft.rb +3 -2
  20. data/lib/sup/horizontal-selector.rb +5 -2
  21. data/lib/sup/index.rb +47 -42
  22. data/lib/sup/label.rb +1 -1
  23. data/lib/sup/message-chunks.rb +4 -2
  24. data/lib/sup/message.rb +14 -3
  25. data/lib/sup/modes/buffer-list-mode.rb +1 -1
  26. data/lib/sup/modes/compose-mode.rb +1 -1
  27. data/lib/sup/modes/contact-list-mode.rb +2 -2
  28. data/lib/sup/modes/edit-message-async-mode.rb +109 -0
  29. data/lib/sup/modes/edit-message-mode.rb +148 -16
  30. data/lib/sup/modes/file-browser-mode.rb +2 -2
  31. data/lib/sup/modes/forward-mode.rb +4 -4
  32. data/lib/sup/modes/line-cursor-mode.rb +2 -2
  33. data/lib/sup/modes/reply-mode.rb +34 -30
  34. data/lib/sup/modes/resume-mode.rb +4 -1
  35. data/lib/sup/modes/scroll-mode.rb +8 -6
  36. data/lib/sup/modes/text-mode.rb +1 -1
  37. data/lib/sup/modes/thread-index-mode.rb +44 -25
  38. data/lib/sup/modes/thread-view-mode.rb +26 -24
  39. data/lib/sup/person.rb +18 -7
  40. data/lib/sup/poll.rb +1 -1
  41. data/lib/sup/rfc2047.rb +1 -1
  42. data/lib/sup/sent.rb +2 -2
  43. data/lib/sup/source.rb +1 -1
  44. data/lib/sup/textfield.rb +38 -1
  45. data/lib/sup/thread.rb +1 -1
  46. data/lib/sup/time.rb +83 -0
  47. data/lib/sup/util.rb +38 -74
  48. data/lib/sup/version.rb +3 -0
  49. metadata +333 -168
  50. metadata.gz.sig +0 -0
  51. data/README.txt +0 -128
  52. data/bin/sup-cmd +0 -138
  53. data/bin/sup-server +0 -44
  54. data/lib/sup/client.rb +0 -92
  55. data/lib/sup/protocol.rb +0 -161
  56. data/lib/sup/server.rb +0 -116
@@ -80,6 +80,7 @@ EOS
80
80
  k.add :edit_cc, "Edit Cc:", 'c'
81
81
  k.add :edit_subject, "Edit Subject", 's'
82
82
  k.add :edit_message, "Edit message", :enter
83
+ k.add :edit_message_async, "Edit message asynchronously", 'E'
83
84
  k.add :save_as_draft, "Save as draft", 'P'
84
85
  k.add :attach_file, "Attach a file", 'a'
85
86
  k.add :delete_attachment, "Delete an attachment", 'd'
@@ -88,11 +89,10 @@ EOS
88
89
  end
89
90
 
90
91
  def initialize opts={}
91
- @header = opts.delete(:header) || {}
92
+ @header = opts.delete(:header) || {}
92
93
  @header_lines = []
93
94
 
94
95
  @body = opts.delete(:body) || []
95
- @body += sig_lines if $config[:edit_signature] && !opts.delete(:have_signature)
96
96
 
97
97
  if opts[:attachments]
98
98
  @attachments = opts[:attachments].values
@@ -111,16 +111,42 @@ EOS
111
111
 
112
112
  @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{hostname}>"
113
113
  @edited = false
114
+ @sig_edited = false
114
115
  @selectors = []
115
116
  @selector_label_width = 0
117
+ @async_mode = nil
118
+
119
+ HookManager.run "before-edit", :header => @header, :body => @body
120
+
121
+ @account_selector = nil
122
+ # only show account selector if there is more than one email address
123
+ if $config[:account_selector] && AccountManager.user_emails.length > 1
124
+ ## Duplicate e-mail strings to prevent a "can't modify frozen
125
+ ## object" crash triggered by the String::display_length()
126
+ ## method in util.rb
127
+ user_emails_copy = []
128
+ AccountManager.user_emails.each { |e| user_emails_copy.push e.dup }
129
+
130
+ @account_selector =
131
+ HorizontalSelector.new "Account:", AccountManager.user_emails + [nil], user_emails_copy + ["Customized"]
132
+
133
+ if @header["From"] =~ /<?(\S+@(\S+?))>?$/
134
+ @account_selector.set_to $1
135
+ @account_user = ""
136
+ else
137
+ @account_selector.set_to nil
138
+ @account_user = @header["From"]
139
+ end
140
+
141
+ add_selector @account_selector
142
+ end
116
143
 
117
144
  @crypto_selector =
118
145
  if CryptoManager.have_crypto?
119
146
  HorizontalSelector.new "Crypto:", [:none] + CryptoManager::OUTGOING_MESSAGE_OPERATIONS.keys, ["None"] + CryptoManager::OUTGOING_MESSAGE_OPERATIONS.values
120
147
  end
121
148
  add_selector @crypto_selector if @crypto_selector
122
-
123
- HookManager.run "before-edit", :header => @header, :body => @body
149
+
124
150
  if @crypto_selector
125
151
  HookManager.run "crypto-mode", :header => @header, :body => @body, :crypto_selector => @crypto_selector
126
152
  end
@@ -130,7 +156,7 @@ EOS
130
156
  end
131
157
 
132
158
  def lines; @text.length + (@selectors.empty? ? 0 : (@selectors.length + DECORATION_LINES)) end
133
-
159
+
134
160
  def [] i
135
161
  if @selectors.empty?
136
162
  @text[i]
@@ -161,12 +187,41 @@ EOS
161
187
  def edit_cc; edit_field "Cc" end
162
188
  def edit_subject; edit_field "Subject" end
163
189
 
164
- def edit_message
165
- @file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}"
190
+ def save_message_to_file
191
+ sig = sig_lines.join("\n")
192
+ @file = Tempfile.new ["sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}", ".eml"]
166
193
  @file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
167
194
  @file.puts
168
195
  @file.puts @body.join("\n")
196
+ @file.puts sig if ($config[:edit_signature] and !@sig_edited)
169
197
  @file.close
198
+ end
199
+
200
+ def set_sig_edit_flag
201
+ sig = sig_lines.join("\n")
202
+ if $config[:edit_signature]
203
+ pbody = @body.join("\n")
204
+ blen = pbody.length
205
+ slen = sig.length
206
+
207
+ if blen > slen and pbody[blen-slen..blen] == sig
208
+ @sig_edited = false
209
+ @body = pbody[0..blen-slen].split("\n")
210
+ else
211
+ @sig_edited = true
212
+ end
213
+ end
214
+ end
215
+
216
+ def edit_message
217
+ old_from = @header["From"] if @account_selector
218
+
219
+ begin
220
+ save_message_to_file
221
+ rescue SystemCallError => e
222
+ BufferManager.flash "Can't save message to file: #{e.message}"
223
+ return
224
+ end
170
225
 
171
226
  editor = $config[:editor] || ENV['EDITOR'] || "/usr/bin/vi"
172
227
 
@@ -178,13 +233,67 @@ EOS
178
233
 
179
234
  header, @body = parse_file @file.path
180
235
  @header = header - NON_EDITABLE_HEADERS
236
+ set_sig_edit_flag
237
+
238
+ if @account_selector and @header["From"] != old_from
239
+ @account_user = @header["From"]
240
+ @account_selector.set_to nil
241
+ end
242
+
181
243
  handle_new_text @header, @body
244
+ rerun_crypto_selector_hook
182
245
  update
183
246
 
184
247
  @edited
185
248
  end
186
249
 
250
+ def edit_message_async
251
+ begin
252
+ save_message_to_file
253
+ rescue SystemCallError => e
254
+ BufferManager.flash "Can't save message to file: #{e.message}"
255
+ return
256
+ end
257
+
258
+ @mtime = File.mtime @file.path
259
+
260
+ # put up buffer saying you can now edit the message in another
261
+ # terminal or app, and continue to use sup in the meantime.
262
+ subject = @header["Subject"] || ""
263
+ @async_mode = EditMessageAsyncMode.new self, @file.path, subject
264
+ BufferManager.spawn "Waiting for message \"#{subject}\" to be finished", @async_mode
265
+
266
+ # hide ourselves, and wait for signal to resume from async mode ...
267
+ buffer.hidden = true
268
+ end
269
+
270
+ def edit_message_async_resume being_killed=false
271
+ buffer.hidden = false
272
+ @async_mode = nil
273
+ BufferManager.raise_to_front buffer if !being_killed
274
+
275
+ @edited = true if File.mtime(@file.path) > @mtime
276
+
277
+ header, @body = parse_file @file.path
278
+ @header = header - NON_EDITABLE_HEADERS
279
+ set_sig_edit_flag
280
+ handle_new_text @header, @body
281
+ update
282
+
283
+ true
284
+ end
285
+
187
286
  def killable?
287
+ if !@async_mode.nil?
288
+ return false if !@async_mode.killable?
289
+ if File.mtime(@file.path) > @mtime
290
+ @edited = true
291
+ header, @body = parse_file @file.path
292
+ @header = header - NON_EDITABLE_HEADERS
293
+ handle_new_text @header, @body
294
+ update
295
+ end
296
+ end
188
297
  !edited? || BufferManager.ask_yes_or_no("Discard message?")
189
298
  end
190
299
 
@@ -205,7 +314,7 @@ EOS
205
314
  end
206
315
 
207
316
  def delete_attachment
208
- i = curpos - @attachment_lines_offset - DECORATION_LINES - 1
317
+ i = curpos - @attachment_lines_offset - DECORATION_LINES - 2
209
318
  if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachment_names[i]}?")
210
319
  @attachments.delete_at i
211
320
  @attachment_names.delete_at i
@@ -215,6 +324,12 @@ EOS
215
324
 
216
325
  protected
217
326
 
327
+ def rerun_crypto_selector_hook
328
+ if @crypto_selector && !@crypto_selector.changed_by_user
329
+ HookManager.run "crypto-mode", :header => @header, :body => @body, :crypto_selector => @crypto_selector
330
+ end
331
+ end
332
+
218
333
  def mime_encode string
219
334
  string = [string].pack('M') # basic quoted-printable
220
335
  string.gsub!(/=\n/,'') # .. remove trailing newline
@@ -242,6 +357,7 @@ protected
242
357
  if curpos < @selectors.length
243
358
  @selectors[curpos].roll_left
244
359
  buffer.mark_dirty
360
+ update if @account_selector
245
361
  else
246
362
  col_left
247
363
  end
@@ -251,6 +367,7 @@ protected
251
367
  if curpos < @selectors.length
252
368
  @selectors[curpos].roll_right
253
369
  buffer.mark_dirty
370
+ update if @account_selector
254
371
  else
255
372
  col_right
256
373
  end
@@ -262,6 +379,14 @@ protected
262
379
  end
263
380
 
264
381
  def update
382
+ if @account_selector
383
+ if @account_selector.val.nil?
384
+ @header["From"] = @account_user
385
+ else
386
+ @header["From"] = AccountManager.full_address_for @account_selector.val
387
+ end
388
+ end
389
+
265
390
  regen_text
266
391
  buffer.mark_dirty if buffer
267
392
  end
@@ -269,8 +394,8 @@ protected
269
394
  def regen_text
270
395
  header, @header_lines = format_headers(@header - NON_EDITABLE_HEADERS) + [""]
271
396
  @text = header + [""] + @body
272
- @text += sig_lines unless $config[:edit_signature]
273
-
397
+ @text += sig_lines unless @sig_edited
398
+
274
399
  @attachment_lines_offset = 0
275
400
 
276
401
  unless @attachments.empty?
@@ -339,7 +464,7 @@ protected
339
464
  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
340
465
  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
341
466
 
342
- from_email =
467
+ from_email =
343
468
  if @header["From"] =~ /<?(\S+@(\S+?))>?$/
344
469
  $1
345
470
  else
@@ -384,7 +509,7 @@ protected
384
509
  m = RMail::Message.new
385
510
  m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
386
511
  m.body = @body.join("\n")
387
- m.body += sig_lines.join("\n") unless $config[:edit_signature]
512
+ m.body += "\n" + sig_lines.join("\n") unless @sig_edited
388
513
  ## body must end in a newline or GPG signatures will be WRONG!
389
514
  m.body += "\n" unless m.body =~ /\n\Z/
390
515
 
@@ -393,7 +518,7 @@ protected
393
518
  body_m = m
394
519
  body_m.header["Content-Disposition"] = "inline"
395
520
  m = RMail::Message.new
396
-
521
+
397
522
  m.add_part body_m
398
523
  @attachments.each { |a| m.add_part a }
399
524
  end
@@ -414,7 +539,7 @@ protected
414
539
  ## finally, set the top-level headers
415
540
  @header.each do |k, v|
416
541
  next if v.nil? || v.empty?
417
- m.header[k] =
542
+ m.header[k] =
418
543
  case v
419
544
  when String
420
545
  k.match(/subject/i) ? mime_encode_subject(v) : mime_encode_address(v)
@@ -454,7 +579,7 @@ EOS
454
579
  f.puts
455
580
  f.puts sanitize_body(@body.join("\n"))
456
581
  f.puts sig_lines if full unless $config[:edit_signature]
457
- end
582
+ end
458
583
 
459
584
  protected
460
585
 
@@ -479,6 +604,13 @@ protected
479
604
  if contacts
480
605
  text = contacts.map { |s| s.full_address }.join(", ")
481
606
  @header[field] = parse_header field, text
607
+
608
+ if @account_selector and field == "From"
609
+ @account_user = @header["From"]
610
+ @account_selector.set_to nil
611
+ end
612
+
613
+ rerun_crypto_selector_hook
482
614
  update
483
615
  end
484
616
  end
@@ -514,7 +646,7 @@ private
514
646
 
515
647
  ## no hook, do default signature generation based on config.yaml
516
648
  return [] unless from_email
517
- sigfn = (AccountManager.account_for(from_email) ||
649
+ sigfn = (AccountManager.account_for(from_email) ||
518
650
  AccountManager.default_account).signature
519
651
 
520
652
  if sigfn && File.exists?(sigfn)
@@ -29,7 +29,7 @@ class FileBrowserMode < LineCursorMode
29
29
  def [] i; @text[i]; end
30
30
 
31
31
  protected
32
-
32
+
33
33
  def back
34
34
  return if @dirs.size == 1
35
35
  @dirs.pop
@@ -75,7 +75,7 @@ protected
75
75
  end
76
76
 
77
77
  def regen_text
78
- @files =
78
+ @files =
79
79
  begin
80
80
  cwd.entries.sort_by do |f|
81
81
  [f.directory? ? 0 : 1, f.basename.to_s]
@@ -7,7 +7,7 @@ class ForwardMode < EditMessageMode
7
7
  "From" => AccountManager.default_account.full_address,
8
8
  }
9
9
 
10
- header["Subject"] =
10
+ header["Subject"] =
11
11
  if opts[:message]
12
12
  "Fwd: " + opts[:message].subj
13
13
  elsif opts[:attachments]
@@ -20,7 +20,7 @@ class ForwardMode < EditMessageMode
20
20
 
21
21
  body =
22
22
  if opts[:message]
23
- forward_body_lines(opts[:message])
23
+ forward_body_lines(opts[:message])
24
24
  elsif opts[:attachments]
25
25
  ["Note: #{opts[:attachments].size.pluralize 'attachment'}."]
26
26
  end
@@ -32,7 +32,7 @@ class ForwardMode < EditMessageMode
32
32
  to = opts[:to] || (BufferManager.ask_for_contacts(:people, "To: ") or return if ($config[:ask_for_to] != false))
33
33
  cc = opts[:cc] || (BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc])
34
34
  bcc = opts[:bcc] || (BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc])
35
-
35
+
36
36
  attachment_hash = {}
37
37
  attachments = opts[:attachments] || []
38
38
 
@@ -64,7 +64,7 @@ class ForwardMode < EditMessageMode
64
64
  protected
65
65
 
66
66
  def forward_body_lines m
67
- ["--- Begin forwarded message from #{m.from.mediumname} ---"] +
67
+ ["--- Begin forwarded message from #{m.from.mediumname} ---"] +
68
68
  m.quotable_header_lines + [""] + m.quotable_body_lines +
69
69
  ["--- End forwarded message ---"]
70
70
  end
@@ -78,7 +78,7 @@ protected
78
78
  end
79
79
 
80
80
  def search_start_line; @curpos end
81
-
81
+
82
82
  def line_down # overwrite scrollmode
83
83
  super
84
84
  call_load_more_callbacks([topline + buffer.content_height - lines, 10].max) if topline + buffer.content_height > lines
@@ -177,7 +177,7 @@ private
177
177
  end
178
178
 
179
179
  def call_load_more_callbacks size
180
- @load_more_q.push size
180
+ @load_more_q.push size if $config[:load_more_threads_when_scrolling]
181
181
  end
182
182
  end
183
183
 
@@ -48,6 +48,7 @@ EOS
48
48
  ## the full headers (most importantly the list-post header, if
49
49
  ## any)
50
50
  body = reply_body_lines message
51
+ @body_orig = body
51
52
 
52
53
  ## first, determine the address at which we received this email. this will
53
54
  ## become our From: address in the reply.
@@ -93,18 +94,25 @@ EOS
93
94
  ## to. if it's a list message, then the list address is. otherwise,
94
95
  ## the cc contains a recipient.
95
96
  useful_recipient = !(cc.empty? || @m.is_list_message?)
96
-
97
+
97
98
  @headers = {}
98
99
  @headers[:recipient] = {
99
100
  "To" => cc.map { |p| p.full_address },
101
+ "Cc" => [],
100
102
  } if useful_recipient
101
103
 
102
104
  ## typically we don't want to have a reply-to-sender option if the sender
103
105
  ## is a user account. however, if the cc is empty, it's a message to
104
106
  ## ourselves, so for the lack of any other options, we'll add it.
105
- @headers[:sender] = { "To" => [to.full_address], } if !AccountManager.is_account?(to) || !useful_recipient
107
+ @headers[:sender] = {
108
+ "To" => [to.full_address],
109
+ "Cc" => [],
110
+ } if !AccountManager.is_account?(to) || !useful_recipient
106
111
 
107
- @headers[:user] = {}
112
+ @headers[:user] = {
113
+ "To" => [],
114
+ "Cc" => [],
115
+ }
108
116
 
109
117
  not_me_ccs = cc.select { |p| !AccountManager.is_account?(p) }
110
118
  @headers[:all] = {
@@ -114,22 +122,11 @@ EOS
114
122
 
115
123
  @headers[:list] = {
116
124
  "To" => [@m.list_address.full_address],
125
+ "Cc" => [],
117
126
  } if @m.is_list_message?
118
127
 
119
128
  refs = gen_references
120
129
 
121
- @headers.each do |k, v|
122
- @headers[k] = {
123
- "From" => from.full_address,
124
- "To" => [],
125
- "Cc" => [],
126
- "Bcc" => [],
127
- "In-reply-to" => "<#{@m.id}>",
128
- "Subject" => Message.reify_subj(@m.subj),
129
- "References" => refs,
130
- }.merge v
131
- end
132
-
133
130
  types = REPLY_TYPES.select { |t| @headers.member?(t) }
134
131
  @type_selector = HorizontalSelector.new "Reply to:", types, types.map { |x| TYPE_DESCRIPTIONS[x] }
135
132
 
@@ -148,13 +145,17 @@ EOS
148
145
  :recipient
149
146
  end)
150
147
 
151
- @bodies = {}
152
- @headers.each do |k, v|
153
- @bodies[k] = body
154
- HookManager.run "before-edit", :header => v, :body => @bodies[k]
155
- end
148
+ headers_full = {
149
+ "From" => from.full_address,
150
+ "Bcc" => [],
151
+ "In-reply-to" => "<#{@m.id}>",
152
+ "Subject" => Message.reify_subj(@m.subj),
153
+ "References" => refs,
154
+ }.merge @headers[@type_selector.val]
155
+
156
+ HookManager.run "before-edit", :header => headers_full, :body => body
156
157
 
157
- super :header => @headers[@type_selector.val], :body => @bodies[@type_selector.val], :twiddles => false
158
+ super :header => headers_full, :body => body, :twiddles => false
158
159
  add_selector @type_selector
159
160
  end
160
161
 
@@ -163,8 +164,8 @@ protected
163
164
  def move_cursor_right
164
165
  super
165
166
  if @headers[@type_selector.val] != self.header
166
- self.header = @headers[@type_selector.val]
167
- self.body = @bodies[@type_selector.val] unless @edited
167
+ self.header = self.header.merge @headers[@type_selector.val]
168
+ rerun_crypto_selector_hook
168
169
  update
169
170
  end
170
171
  end
@@ -172,8 +173,8 @@ protected
172
173
  def move_cursor_left
173
174
  super
174
175
  if @headers[@type_selector.val] != self.header
175
- self.header = @headers[@type_selector.val]
176
- self.body = @bodies[@type_selector.val] unless @edited
176
+ self.header = self.header.merge @headers[@type_selector.val]
177
+ rerun_crypto_selector_hook
177
178
  update
178
179
  end
179
180
  end
@@ -190,14 +191,15 @@ protected
190
191
  end
191
192
 
192
193
  def handle_new_text new_header, new_body
193
- if new_body != @bodies[@type_selector.val]
194
- @bodies[@type_selector.val] = new_body
194
+ if new_body != @body_orig
195
+ @body_orig = new_body
195
196
  @edited = true
196
197
  end
197
198
  old_header = @headers[@type_selector.val]
198
- if new_header.size != old_header.size || old_header.any? { |k, v| new_header[k] != v }
199
+ if old_header.any? { |k, v| new_header[k] != v }
199
200
  @type_selector.set_to :user
200
- self.header = @headers[:user] = new_header
201
+ self.header["To"] = @headers[:user]["To"] = new_header["To"]
202
+ self.header["Cc"] = @headers[:user]["Cc"] = new_header["Cc"]
201
203
  update
202
204
  end
203
205
  end
@@ -208,8 +210,10 @@ protected
208
210
 
209
211
  def edit_field field
210
212
  edited_field = super
211
- if edited_field && edited_field != "Subject"
213
+ if edited_field and (field == "To" or field == "Cc")
212
214
  @type_selector.set_to :user
215
+ @headers[:user]["To"] = self.header["To"]
216
+ @headers[:user]["Cc"] = self.header["Cc"]
213
217
  update
214
218
  end
215
219
  end