sup 0.1 → 0.2

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 (48) hide show
  1. data/History.txt +9 -0
  2. data/Manifest.txt +5 -1
  3. data/Rakefile +2 -1
  4. data/bin/sup +27 -10
  5. data/bin/sup-add +2 -1
  6. data/bin/sup-sync-back +51 -23
  7. data/doc/FAQ.txt +29 -37
  8. data/doc/Hooks.txt +38 -0
  9. data/doc/{UserGuide.txt → NewUserGuide.txt} +27 -21
  10. data/doc/TODO +91 -57
  11. data/lib/sup.rb +17 -1
  12. data/lib/sup/buffer.rb +80 -16
  13. data/lib/sup/colormap.rb +0 -2
  14. data/lib/sup/contact.rb +3 -2
  15. data/lib/sup/crypto.rb +110 -0
  16. data/lib/sup/draft.rb +2 -6
  17. data/lib/sup/hook.rb +131 -0
  18. data/lib/sup/imap.rb +27 -16
  19. data/lib/sup/index.rb +38 -14
  20. data/lib/sup/keymap.rb +0 -2
  21. data/lib/sup/label.rb +30 -9
  22. data/lib/sup/logger.rb +12 -1
  23. data/lib/sup/maildir.rb +48 -3
  24. data/lib/sup/mbox.rb +1 -1
  25. data/lib/sup/mbox/loader.rb +22 -12
  26. data/lib/sup/mbox/ssh-loader.rb +1 -1
  27. data/lib/sup/message-chunks.rb +198 -0
  28. data/lib/sup/message.rb +154 -115
  29. data/lib/sup/modes/compose-mode.rb +18 -0
  30. data/lib/sup/modes/contact-list-mode.rb +1 -1
  31. data/lib/sup/modes/edit-message-mode.rb +112 -31
  32. data/lib/sup/modes/file-browser-mode.rb +1 -1
  33. data/lib/sup/modes/inbox-mode.rb +1 -1
  34. data/lib/sup/modes/label-list-mode.rb +8 -6
  35. data/lib/sup/modes/label-search-results-mode.rb +4 -1
  36. data/lib/sup/modes/log-mode.rb +1 -1
  37. data/lib/sup/modes/reply-mode.rb +18 -16
  38. data/lib/sup/modes/search-results-mode.rb +1 -1
  39. data/lib/sup/modes/thread-index-mode.rb +61 -33
  40. data/lib/sup/modes/thread-view-mode.rb +111 -102
  41. data/lib/sup/person.rb +5 -1
  42. data/lib/sup/poll.rb +36 -7
  43. data/lib/sup/sent.rb +1 -0
  44. data/lib/sup/source.rb +7 -3
  45. data/lib/sup/textfield.rb +48 -34
  46. data/lib/sup/thread.rb +9 -5
  47. data/lib/sup/util.rb +16 -22
  48. metadata +7 -3
@@ -12,13 +12,26 @@ class EditMessageMode < LineCursorMode
12
12
  MULTI_HEADERS = %w(To Cc Bcc)
13
13
  NON_EDITABLE_HEADERS = %w(Message-Id Date)
14
14
 
15
+ HookManager.register "signature", <<EOS
16
+ Generates a signature for a message.
17
+ Variables:
18
+ header: an object that supports string-to-string hashtable-style access
19
+ to the raw headers for the message. E.g., header["From"],
20
+ header["To"], etc.
21
+ from_email: the email part of the From: line, or nil if empty
22
+ Return value:
23
+ A string (multi-line ok) containing the text of the signature, or nil to
24
+ use the default signature.
25
+ EOS
26
+
15
27
  attr_reader :status
16
28
  attr_accessor :body, :header
17
29
  bool_reader :edited
18
30
 
19
31
  register_keymap do |k|
20
32
  k.add :send_message, "Send message", 'y'
21
- k.add :edit, "Edit message", 'e', :enter
33
+ k.add :edit_field, "Edit field", 'e'
34
+ k.add :edit_message, "Edit message", :enter
22
35
  k.add :save_as_draft, "Save as draft", 'P'
23
36
  k.add :attach_file, "Attach a file", 'a'
24
37
  k.add :delete_attachment, "Delete an attachment", 'd'
@@ -26,12 +39,15 @@ class EditMessageMode < LineCursorMode
26
39
 
27
40
  def initialize opts={}
28
41
  @header = opts.delete(:header) || {}
42
+ @header_lines = []
43
+
29
44
  @body = opts.delete(:body) || []
30
45
  @body += sig_lines if $config[:edit_signature]
46
+
31
47
  @attachments = []
32
- @attachment_lines = {}
33
48
  @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>"
34
49
  @edited = false
50
+ @skip_top_rows = opts[:skip_top_rows] || 0
35
51
 
36
52
  super opts
37
53
  regen_text
@@ -43,9 +59,36 @@ class EditMessageMode < LineCursorMode
43
59
  ## a hook
44
60
  def handle_new_text header, body; end
45
61
 
46
- def edit
62
+ def edit_field
63
+ if (curpos - @skip_top_rows) >= @header_lines.length
64
+ edit_message
65
+ else
66
+ case(field = @header_lines[curpos - @skip_top_rows])
67
+ when "Subject"
68
+ text = BufferManager.ask :subject, "Subject: ", @header[field]
69
+ @header[field] = parse_header field, text if text
70
+ else
71
+ default =
72
+ case field
73
+ when *MULTI_HEADERS
74
+ @header[field].join(", ")
75
+ else
76
+ @header[field]
77
+ end
78
+
79
+ contacts = BufferManager.ask_for_contacts :people, "#{field}: ", default
80
+ if contacts
81
+ text = contacts.map { |s| s.longname }.join(", ")
82
+ @header[field] = parse_header field, text
83
+ end
84
+ end
85
+ update
86
+ end
87
+ end
88
+
89
+ def edit_message
47
90
  @file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}"
48
- @file.puts header_lines(@header - NON_EDITABLE_HEADERS)
91
+ @file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
49
92
  @file.puts
50
93
  @file.puts @body
51
94
  @file.close
@@ -56,12 +99,14 @@ class EditMessageMode < LineCursorMode
56
99
  BufferManager.shell_out "#{editor} #{@file.path}"
57
100
  @edited = true if File.mtime(@file.path) > mtime
58
101
 
59
- BufferManager.kill_buffer self.buffer unless @edited
102
+ return @edited unless @edited
60
103
 
61
104
  header, @body = parse_file @file.path
62
105
  @header = header - NON_EDITABLE_HEADERS
63
106
  handle_new_text @header, @body
64
107
  update
108
+
109
+ @edited
65
110
  end
66
111
 
67
112
  def killable?
@@ -69,13 +114,14 @@ class EditMessageMode < LineCursorMode
69
114
  end
70
115
 
71
116
  def attach_file
72
- fn = BufferManager.ask_for_filenames :attachment, "File name (enter for browser): "
73
- fn.each { |f| @attachments << Pathname.new(f) }
117
+ fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
118
+ return unless fn
119
+ @attachments << Pathname.new(fn)
74
120
  update
75
121
  end
76
122
 
77
123
  def delete_attachment
78
- i = curpos - @attachment_lines_offset
124
+ i = (curpos - @skip_top_rows) - @attachment_lines_offset
79
125
  if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachments[i]}?")
80
126
  @attachments.delete_at i
81
127
  update
@@ -90,8 +136,8 @@ protected
90
136
  end
91
137
 
92
138
  def regen_text
93
- top = header_lines(@header - NON_EDITABLE_HEADERS) + [""]
94
- @text = top + @body
139
+ header, @header_lines = format_headers(@header - NON_EDITABLE_HEADERS) + [""]
140
+ @text = header + [""] + @body
95
141
  @text += sig_lines unless $config[:edit_signature]
96
142
 
97
143
  unless @attachments.empty?
@@ -107,24 +153,30 @@ protected
107
153
  body = f.readlines
108
154
 
109
155
  header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
110
- header.each do |k, v|
111
- next unless MULTI_HEADERS.include?(k) && !v.empty?
112
- header[k] = v.split_on_commas.map do |name|
113
- (p = ContactManager.person_with(name)) && p.full_address || name
114
- end
115
- end
156
+ header.each { |k, v| header[k] = parse_header k, v }
116
157
 
117
158
  [header, body]
118
159
  end
119
160
  end
120
161
 
121
- def header_lines header
122
- force_headers = FORCE_HEADERS.map { |h| make_lines "#{h}:", header[h] }
123
- other_headers = (header.keys - FORCE_HEADERS).map do |h|
124
- make_lines "#{h}:", header[h]
162
+ def parse_header k, v
163
+ if MULTI_HEADERS.include?(k)
164
+ v.split_on_commas.map do |name|
165
+ (p = ContactManager.contact_for(name)) && p.full_address || name
166
+ end
167
+ else
168
+ v
125
169
  end
170
+ end
126
171
 
127
- (force_headers + other_headers).flatten.compact
172
+ def format_headers header
173
+ header_lines = []
174
+ headers = (FORCE_HEADERS + (header.keys - FORCE_HEADERS)).map do |h|
175
+ lines = make_lines "#{h}:", header[h]
176
+ lines.length.times { header_lines << h }
177
+ lines
178
+ end.flatten.compact
179
+ [headers, header_lines]
128
180
  end
129
181
 
130
182
  def make_lines header, things
@@ -150,7 +202,9 @@ protected
150
202
  end
151
203
 
152
204
  def send_message
153
- return unless edited? || BufferManager.ask_yes_or_no("Message unedited. Really send?")
205
+ return false if !edited? && !BufferManager.ask_yes_or_no("Message unedited. Really send?")
206
+ 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
207
+ 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
154
208
 
155
209
  date = Time.now
156
210
  from_email =
@@ -164,14 +218,16 @@ protected
164
218
  BufferManager.flash "Sending..."
165
219
 
166
220
  begin
167
- IO.popen(acct.sendmail, "w") { |p| write_full_message_to p, date }
221
+ IO.popen(acct.sendmail, "w") { |p| write_full_message_to p, date, false }
168
222
  raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
169
- SentManager.write_sent_message(date, from_email) { |f| write_full_message_to f, date }
223
+ SentManager.write_sent_message(date, from_email) { |f| write_full_message_to f, date, true }
170
224
  BufferManager.kill_buffer buffer
171
225
  BufferManager.flash "Message sent!"
226
+ true
172
227
  rescue SystemCallError, SendmailCommandFailed => e
173
228
  Redwood::log "Problem sending mail: #{e.message}"
174
229
  BufferManager.flash "Problem sending mail: #{e.message}"
230
+ false
175
231
  end
176
232
  end
177
233
 
@@ -181,7 +237,7 @@ protected
181
237
  BufferManager.flash "Saved for later editing."
182
238
  end
183
239
 
184
- def write_full_message_to f, date=Time.now
240
+ def write_full_message_to f, date=Time.now, escape=false
185
241
  m = RMail::Message.new
186
242
  @header.each do |k, v|
187
243
  next if v.nil? || v.empty?
@@ -199,26 +255,31 @@ protected
199
255
  m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
200
256
 
201
257
  if @attachments.empty?
202
- m.header["Content-Disposition"] = "inline"
203
258
  m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
204
259
  m.body = @body.join
260
+ m.body = sanitize_body m.body if escape
205
261
  m.body += sig_lines.join("\n") unless $config[:edit_signature]
206
262
  else
207
263
  body_m = RMail::Message.new
208
264
  body_m.body = @body.join
265
+ body_m.body = sanitize_body body_m.body if escape
209
266
  body_m.body += sig_lines.join("\n") unless $config[:edit_signature]
267
+ body_m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
268
+ body_m.header["Content-Disposition"] = "inline"
210
269
 
211
270
  m.add_part body_m
212
- @attachments.each { |fn| m.add_attachment fn.to_s }
271
+ @attachments.each { |fn| m.add_file_attachment fn.to_s }
213
272
  end
214
273
  f.puts m.to_s
215
274
  end
216
275
 
276
+ ## TODO: remove this. redundant with write_full_message_to.
277
+ ##
217
278
  ## this is going to change soon: draft messages (currently written
218
279
  ## with full=false) will be output as yaml.
219
280
  def write_message f, full=true, date=Time.now
220
281
  raise ArgumentError, "no pre-defined date: header allowed" if @header["Date"]
221
- f.puts header_lines(@header)
282
+ f.puts format_headers(@header).first
222
283
  f.puts <<EOS
223
284
  Date: #{date.rfc2822}
224
285
  Message-Id: #{@message_id}
@@ -233,15 +294,35 @@ EOS
233
294
  end
234
295
 
235
296
  f.puts
236
- f.puts @body.map { |l| l =~ /^From / ? ">#{l}" : l }
297
+ f.puts sanitize_body(@body.join)
237
298
  f.puts sig_lines if full unless $config[:edit_signature]
238
299
  end
239
300
 
240
301
  private
241
302
 
303
+ def sanitize_body body
304
+ body.gsub(/^From /, ">From ")
305
+ end
306
+
307
+ def mentions_attachments?
308
+ @body.any? { |l| l =~ /^[^>]/ && l =~ /\battach(ment|ed|ing|)\b/i }
309
+ end
310
+
311
+ def top_posting?
312
+ @body.join =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
313
+ end
314
+
242
315
  def sig_lines
243
- p = PersonManager.person_for @header["From"]
244
- sigfn = (AccountManager.account_for(p.email) ||
316
+ p = PersonManager.person_for(@header["From"])
317
+ from_email = p && p.email
318
+
319
+ ## first run the hook
320
+ hook_sig = HookManager.run "signature", :header => @header, :from_email => from_email
321
+ return ["", "-- "] + hook_sig.split("\n") if hook_sig
322
+
323
+ ## no hook, do default signature generation based on config.yaml
324
+ return [] unless from_email
325
+ sigfn = (AccountManager.account_for(from_email) ||
245
326
  AccountManager.default_account).signature
246
327
 
247
328
  if sigfn && File.exists?(sigfn)
@@ -65,7 +65,7 @@ protected
65
65
  end
66
66
  else
67
67
  begin
68
- @value = [f.realpath.to_s]
68
+ @value = f.realpath.to_s
69
69
  @done = true
70
70
  rescue SystemCallError => e
71
71
  BufferManager.flash e.message
@@ -9,7 +9,7 @@ class InboxMode < ThreadIndexMode
9
9
  end
10
10
 
11
11
  def initialize
12
- super [:inbox, :sent], { :label => :inbox }
12
+ super [:inbox, :sent], { :label => :inbox, :skip_killed => true }
13
13
  raise "can't have more than one!" if defined? @@instance
14
14
  @@instance = self
15
15
  end
@@ -3,7 +3,7 @@ module Redwood
3
3
  class LabelListMode < LineCursorMode
4
4
  register_keymap do |k|
5
5
  k.add :select_label, "Select label", :enter
6
- k.add :reload, "Discard label list and reload", 'D'
6
+ k.add :reload, "Discard label list and reload", '@'
7
7
  end
8
8
 
9
9
  bool_reader :done
@@ -30,17 +30,18 @@ protected
30
30
 
31
31
  def regen_text
32
32
  @text = []
33
- @labels = LabelManager.listable_label_strings
33
+ labels = LabelManager.listable_labels
34
34
 
35
- counts = @labels.map do |string|
36
- label = LabelManager.label_for string
35
+ counts = labels.map do |label|
36
+ string = LabelManager.string_for label
37
37
  total = Index.num_results_for :label => label
38
38
  unread = Index.num_results_for :labels => [label, :unread]
39
39
  [label, string, total, unread]
40
- end
40
+ end.sort_by { |l, s, t, u| s.downcase }
41
41
 
42
- width = @labels.max_of { |string| string.length }
42
+ width = counts.max_of { |l, s, t, u| s.length }
43
43
 
44
+ @labels = []
44
45
  counts.map do |label, string, total, unread|
45
46
  if total == 0 && !LabelManager::RESERVED_LABELS.include?(label)
46
47
  Redwood::log "no hits for label #{label}, deleting"
@@ -50,6 +51,7 @@ protected
50
51
 
51
52
  @text << [[(unread == 0 ? :labellist_old_color : :labellist_new_color),
52
53
  sprintf("%#{width + 1}s %5d %s, %5d unread", string, total, total == 1 ? " message" : "messages", unread)]]
54
+ @labels << label
53
55
  yield i if block_given?
54
56
  end.compact
55
57
  end
@@ -3,7 +3,10 @@ module Redwood
3
3
  class LabelSearchResultsMode < ThreadIndexMode
4
4
  def initialize labels
5
5
  @labels = labels
6
- super [], { :labels => @labels }
6
+ opts = { :labels => @labels }
7
+ opts[:load_deleted] = true if labels.include? :deleted
8
+ opts[:load_spam] = true if labels.include? :spam
9
+ super [], opts
7
10
  end
8
11
 
9
12
  def is_relevant? m; @labels.all? { |l| m.has_label? l }; end
@@ -38,7 +38,7 @@ class LogMode < TextMode
38
38
  end
39
39
 
40
40
  def save_to_disk
41
- fn = BufferManager.ask :filename, "Save log to file: "
41
+ fn = BufferManager.ask_for_filename :filename, "Save log to file: "
42
42
  save_to_file(fn) { |f| f.puts text } if fn
43
43
  end
44
44
 
@@ -24,13 +24,13 @@ class ReplyMode < EditMessageMode
24
24
  body = reply_body_lines message
25
25
 
26
26
  from =
27
- if @m.recipient_email
28
- AccountManager.account_for @m.recipient_email
27
+ if @m.recipient_email && (a = AccountManager.account_for(@m.recipient_email))
28
+ a
29
+ elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
30
+ b
29
31
  else
30
- (@m.to + @m.cc).find { |p| AccountManager.is_account? p }
31
- end || AccountManager.default_account
32
-
33
- from_email = from.email
32
+ AccountManager.default_account
33
+ end
34
34
 
35
35
  ## ignore reply-to for list messages because it's typically set to
36
36
  ## the list address, which we explicitly treat with :list
@@ -39,36 +39,31 @@ class ReplyMode < EditMessageMode
39
39
 
40
40
  @headers = {}
41
41
  @headers[:sender] = {
42
- "From" => "#{from.name} <#{from_email}>",
43
42
  "To" => [to.full_address],
44
43
  } unless AccountManager.is_account? to
45
44
 
46
45
  @headers[:recipient] = {
47
- "From" => "#{from.name} <#{from_email}>",
48
46
  "To" => cc.map { |p| p.full_address },
49
47
  } unless cc.empty? || @m.is_list_message?
50
48
 
51
- @headers[:user] = {
52
- "From" => "#{from.name} <#{from_email}>",
53
- }
49
+ @headers[:user] = {}
54
50
 
55
51
  @headers[:all] = {
56
- "From" => "#{from.name} <#{from_email}>",
57
52
  "To" => [to.full_address],
58
53
  "Cc" => cc.select { |p| !AccountManager.is_account?(p) }.map { |p| p.full_address },
59
54
  } unless cc.empty?
60
55
 
61
56
  @headers[:list] = {
62
- "From" => "#{from.name} <#{from_email}>",
63
57
  "To" => [@m.list_address.full_address],
64
58
  } if @m.is_list_message?
65
59
 
66
60
  refs = gen_references
67
61
  @headers.each do |k, v|
68
62
  @headers[k] = {
69
- "To" => "",
70
- "Cc" => "",
71
- "Bcc" => "",
63
+ "From" => "#{from.name} <#{from.email}>",
64
+ "To" => [],
65
+ "Cc" => [],
66
+ "Bcc" => [],
72
67
  "In-Reply-To" => "<#{@m.id}>",
73
68
  "Subject" => Message.reify_subj(@m.subj),
74
69
  "References" => refs,
@@ -125,6 +120,13 @@ protected
125
120
  def gen_references
126
121
  (@m.refs + [@m.id]).map { |x| "<#{x}>" }.join(" ")
127
122
  end
123
+
124
+ def edit_field
125
+ @selected_type = :user
126
+ self.header = @headers[:user]
127
+ update
128
+ super
129
+ end
128
130
 
129
131
  def move_cursor_left
130
132
  i = @type_labels.index @selected_type
@@ -3,7 +3,7 @@ module Redwood
3
3
  class SearchResultsMode < ThreadIndexMode
4
4
  def initialize qobj
5
5
  @qobj = qobj
6
- super [], { :qobj => @qobj, :load_killed => true, :load_spam => false }
6
+ super [], { :qobj => @qobj }
7
7
  end
8
8
 
9
9
  ## a proper is_relevant? method requires some way of asking ferret
@@ -10,19 +10,19 @@ class ThreadIndexMode < LineCursorMode
10
10
 
11
11
  register_keymap do |k|
12
12
  k.add :load_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
13
- k.add :reload, "Discard threads and reload", 'D'
13
+ k.add :reload, "Refresh view", '@'
14
14
  k.add :toggle_archived, "Toggle archived status", 'a'
15
15
  k.add :toggle_starred, "Star or unstar all messages in thread", '*'
16
16
  k.add :toggle_new, "Toggle new/read status of all messages in thread", 'N'
17
17
  k.add :edit_labels, "Edit or add labels for a thread", 'l'
18
18
  k.add :edit_message, "Edit message (drafts only)", 'e'
19
- k.add :mark_as_spam, "Mark thread as spam", 'S'
20
- k.add :delete, "Mark thread for deletion", 'd'
19
+ k.add :toggle_spam, "Mark/unmark thread as spam", 'S'
20
+ k.add :toggle_deleted, "Delete/undelete thread", 'd'
21
21
  k.add :kill, "Kill thread (never to be seen in inbox again)", '&'
22
22
  k.add :save, "Save changes now", '$'
23
23
  k.add :jump_to_next_new, "Jump to next new thread", :tab
24
- k.add :reply, "Reply to a thread", 'r'
25
- k.add :forward, "Forward a thread", 'f'
24
+ k.add :reply, "Reply to latest message in a thread", 'r'
25
+ k.add :forward, "Forward latest message in a thread", 'f'
26
26
  k.add :toggle_tagged, "Tag/untag current line", 't'
27
27
  k.add :apply_to_tagged, "Apply next command to all tagged threads", ';'
28
28
  end
@@ -80,7 +80,6 @@ class ThreadIndexMode < LineCursorMode
80
80
  ## the first draw_screen is needed before topline and botline
81
81
  ## are set, and the second to show the cursor having moved
82
82
 
83
- t.remove_label :unread
84
83
  update_text_for_line curpos
85
84
  UpdateManager.relay self, :read, t
86
85
  end
@@ -90,7 +89,7 @@ class ThreadIndexMode < LineCursorMode
90
89
  threads.each { |t| select t }
91
90
  end
92
91
 
93
- def handle_starred_update sender, m
92
+ def handle_label_update sender, m
94
93
  t = @ts.thread_for(m) or return
95
94
  l = @lines[t] or return
96
95
  update_text_for_line l
@@ -105,6 +104,12 @@ class ThreadIndexMode < LineCursorMode
105
104
 
106
105
  def handle_archived_update *a; handle_read_update(*a); end
107
106
 
107
+ def handle_deleted_update sender, t
108
+ handle_read_update sender, t
109
+ hide_thread t
110
+ regen_text
111
+ end
112
+
108
113
  ## overwrite me!
109
114
  def is_relevant? m; false; end
110
115
 
@@ -145,8 +150,10 @@ class ThreadIndexMode < LineCursorMode
145
150
  def actually_toggle_starred t
146
151
  if t.has_label? :starred # if ANY message has a star
147
152
  t.remove_label :starred # remove from all
153
+ UpdateManager.relay self, :unstarred, t
148
154
  else
149
155
  t.first.add_label :starred # add only to first
156
+ UpdateManager.relay self, :starred, t
150
157
  end
151
158
  end
152
159
 
@@ -172,6 +179,26 @@ class ThreadIndexMode < LineCursorMode
172
179
  end
173
180
  end
174
181
 
182
+ def actually_toggle_spammed t
183
+ if t.has_label? :spam
184
+ t.remove_label :spam
185
+ UpdateManager.relay self, :unspammed, t
186
+ else
187
+ t.apply_label :spam
188
+ UpdateManager.relay self, :spammed, t
189
+ end
190
+ end
191
+
192
+ def actually_toggle_deleted t
193
+ if t.has_label? :deleted
194
+ t.remove_label :deleted
195
+ UpdateManager.relay self, :undeleted, t
196
+ else
197
+ t.apply_label :deleted
198
+ UpdateManager.relay self, :deleted, t
199
+ end
200
+ end
201
+
175
202
  def toggle_archived
176
203
  t = @threads[curpos] or return
177
204
  actually_toggle_archived t
@@ -211,28 +238,36 @@ class ThreadIndexMode < LineCursorMode
211
238
  end
212
239
  end
213
240
 
214
- def mark_as_spam
241
+ def toggle_spam
215
242
  t = @threads[curpos] or return
216
- multi_mark_as_spam [t]
243
+ multi_toggle_spam [t]
217
244
  end
218
245
 
219
- def multi_mark_as_spam threads
246
+ ## both spam and deleted have the curious characteristic that you
247
+ ## always want to hide the thread after either applying or removing
248
+ ## that label. in all thread-index-views except for
249
+ ## label-search-results-mode, when you mark a message as spam or
250
+ ## deleted, you want it to disappear immediately; in LSRM, you only
251
+ ## see deleted or spam emails, and when you undelete or unspam them
252
+ ## you also want them to disappear immediately.
253
+ def multi_toggle_spam threads
220
254
  threads.each do |t|
221
- t.toggle_label :spam
222
- hide_thread t
255
+ actually_toggle_spammed t
256
+ hide_thread t
223
257
  end
224
258
  regen_text
225
259
  end
226
260
 
227
- def delete
261
+ def toggle_deleted
228
262
  t = @threads[curpos] or return
229
- multi_delete [t]
263
+ multi_toggle_deleted [t]
230
264
  end
231
265
 
232
- def multi_delete threads
266
+ ## see comment for multi_toggle_spam
267
+ def multi_toggle_deleted threads
233
268
  threads.each do |t|
234
- t.toggle_label :deleted
235
- hide_thread t
269
+ actually_toggle_deleted t
270
+ hide_thread t
236
271
  end
237
272
  regen_text
238
273
  end
@@ -248,6 +283,7 @@ class ThreadIndexMode < LineCursorMode
248
283
  hide_thread t
249
284
  end
250
285
  regen_text
286
+ BufferManager.flash "Thread#{threads.size == 1 ? '' : 's'} killed."
251
287
  end
252
288
 
253
289
  def save
@@ -288,20 +324,12 @@ class ThreadIndexMode < LineCursorMode
288
324
  thread = @threads[curpos] or return
289
325
  speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
290
326
  keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
291
- label_string = modifyl.join(" ")
292
- label_string += " " unless label_string.empty?
293
327
 
294
- answer = BufferManager.ask :edit_labels, "edit labels: ", label_string
295
- return unless answer
296
- user_labels = answer.split(/\s+/).map { |l| l.intern }
297
-
298
- hl = user_labels.select { |l| speciall.member? l }
299
- if hl.empty?
300
- thread.labels = keepl + user_labels
301
- user_labels.each { |l| LabelManager << l }
302
- else
303
- BufferManager.flash "'#{hl}' is a reserved label!"
304
- end
328
+ user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl, @hidden_labels
329
+
330
+ return unless user_labels
331
+ thread.labels = keepl + user_labels
332
+ user_labels.each { |l| LabelManager << l }
305
333
  update_text_for_line curpos
306
334
  end
307
335
 
@@ -336,7 +364,7 @@ class ThreadIndexMode < LineCursorMode
336
364
  m.load_from_source!
337
365
  mode = ForwardMode.new m
338
366
  BufferManager.spawn "Forward of #{m.subj}", mode
339
- mode.edit
367
+ mode.edit_message
340
368
  end
341
369
 
342
370
  def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
@@ -384,9 +412,9 @@ class ThreadIndexMode < LineCursorMode
384
412
  myopts = @load_thread_opts.merge({ :when_done => (lambda do |num|
385
413
  opts[:when_done].call(num) if opts[:when_done]
386
414
  if num > 0
387
- BufferManager.flash "Found #{num} threads"
415
+ BufferManager.flash "Found #{num} threads."
388
416
  else
389
- BufferManager.flash "No matches"
417
+ BufferManager.flash "No matches."
390
418
  end
391
419
  end)})
392
420