lib/sup/modes/thread_view_mode.rb (33963B) - raw
1 module Redwood
2
3 class ThreadViewMode < LineCursorMode
4 ## this holds all info we need to lay out a message
5 class MessageLayout
6 attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new, :toggled_state
7 end
8
9 class ChunkLayout
10 attr_accessor :state
11 end
12
13 HookManager.register "detailed-headers", <<EOS
14 Add or remove headers from the detailed header display of a message.
15 Variables:
16 message: The message whose headers are to be formatted.
17 headers: A hash of header (name, value) pairs, initialized to the default
18 headers.
19 Return value:
20 None. The variable 'headers' should be modified in place.
21 EOS
22
23 HookManager.register "bounce-command", <<EOS
24 Determines the command used to bounce a message.
25 Variables:
26 from: The From header of the message being bounced
27 (eg: likely _not_ your address).
28 to: The addresses you asked the message to be bounced to as an array.
29 Return value:
30 A string representing the command to pipe the mail into. This
31 should include the entire command except for the destination addresses,
32 which will be appended by sup.
33 EOS
34
35 HookManager.register "publish", <<EOS
36 Executed when a message or a chunk is requested to be published.
37 Variables:
38 chunk: Redwood::Message or Redwood::Chunk::* to be published.
39 Return value:
40 None.
41 EOS
42
43 HookManager.register "goto", <<EOS
44 Open the uri given as a parameter.
45 Variables:
46 uri: The uri
47 Return value:
48 None.
49 EOS
50
51 register_keymap do |k|
52 k.add :toggle_detailed_header, "Toggle detailed header", 'h'
53 k.add :show_header, "Show full message header", 'H'
54 k.add :show_message, "Show full message (raw form)", 'V'
55 k.add :reload, "Update message in thread", '@'
56 k.add :activate_chunk, "Expand/collapse or activate item", :enter
57 k.add :expand_all_messages, "Expand/collapse all messages", 'E'
58 k.add :edit_draft, "Edit draft", 'e'
59 k.add :send_draft, "Send draft", 'y'
60 k.add :edit_labels, "Edit or add labels for a thread", 'l'
61 k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o'
62 k.add :jump_to_next_open, "Jump to next open message", 'n'
63 k.add :jump_to_next_and_open, "Jump to next message and open", "\C-n"
64 k.add :jump_to_prev_open, "Jump to previous open message", 'p'
65 k.add :jump_to_prev_and_open, "Jump to previous message and open", "\C-p"
66 k.add :align_current_message, "Align current message in buffer", 'z'
67 k.add :toggle_starred, "Star or unstar message", '*'
68 k.add :toggle_new, "Toggle unread/read status of message", 'N'
69 # k.add :collapse_non_new_messages, "Collapse all but unread messages", 'N'
70 k.add :reply, "Reply to a message", 'r'
71 k.add :reply_all, "Reply to all participants of this message", 'G'
72 k.add :forward, "Forward a message or attachment", 'f'
73 k.add :bounce, "Bounce message to other recipient(s)", '!'
74 k.add :alias, "Edit alias/nickname for a person", 'i'
75 k.add :edit_as_new, "Edit message as new", 'D'
76 k.add :save_to_disk, "Save message/attachment to disk", 's'
77 k.add :save_all_to_disk, "Save all attachments to disk", 'A'
78 k.add :publish, "Publish message/attachment using publish-hook", 'P'
79 k.add :search, "Search for messages from particular people", 'S'
80 k.add :compose, "Compose message to person", 'm'
81 k.add :subscribe_to_list, "Subscribe to/unsubscribe from mailing list", "("
82 k.add :unsubscribe_from_list, "Subscribe to/unsubscribe from mailing list", ")"
83 k.add :pipe_message, "Pipe message or attachment to a shell command", '|'
84
85 k.add :archive_and_next, "Archive this thread, kill buffer, and view next", 'a'
86 k.add :delete_and_next, "Delete this thread, kill buffer, and view next", 'd'
87 k.add :kill_and_next, "Kill this thread, kill buffer, and view next", '&'
88 k.add :toggle_wrap, "Toggle wrapping of text", 'w'
89
90 k.add :goto_uri, "Goto uri under cursor", 'g'
91 k.add :fetch_and_verify, "Fetch the PGP key on poolserver and re-verify message", "v"
92
93 k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read:", '.' do |kk|
94 kk.add :archive_and_kill, "Archive this thread and kill buffer", 'a'
95 kk.add :delete_and_kill, "Delete this thread and kill buffer", 'd'
96 kk.add :kill_and_kill, "Kill this thread and kill buffer", '&'
97 kk.add :spam_and_kill, "Mark this thread as spam and kill buffer", 's'
98 kk.add :unread_and_kill, "Mark this thread as unread and kill buffer", 'N'
99 kk.add :do_nothing_and_kill, "Just kill this buffer", '.'
100 end
101
102 k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read/do (n)othing:", ',' do |kk|
103 kk.add :archive_and_next, "Archive this thread, kill buffer, and view next", 'a'
104 kk.add :delete_and_next, "Delete this thread, kill buffer, and view next", 'd'
105 kk.add :kill_and_next, "Kill this thread, kill buffer, and view next", '&'
106 kk.add :spam_and_next, "Mark this thread as spam, kill buffer, and view next", 's'
107 kk.add :unread_and_next, "Mark this thread as unread, kill buffer, and view next", 'N'
108 kk.add :do_nothing_and_next, "Kill buffer, and view next", 'n', ','
109 end
110
111 k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read/do (n)othing:", ']' do |kk|
112 kk.add :archive_and_prev, "Archive this thread, kill buffer, and view previous", 'a'
113 kk.add :delete_and_prev, "Delete this thread, kill buffer, and view previous", 'd'
114 kk.add :kill_and_prev, "Kill this thread, kill buffer, and view previous", '&'
115 kk.add :spam_and_prev, "Mark this thread as spam, kill buffer, and view previous", 's'
116 kk.add :unread_and_prev, "Mark this thread as unread, kill buffer, and view previous", 'N'
117 kk.add :do_nothing_and_prev, "Kill buffer, and view previous", 'n', ']'
118 end
119 end
120
121 ## there are a couple important instance variables we hold to format
122 ## the thread and to provide line-based functionality. @layout is a
123 ## map from Messages to MessageLayouts, and @chunk_layout from
124 ## Chunks to ChunkLayouts. @message_lines is a map from row #s to
125 ## Message objects. @chunk_lines is a map from row #s to Chunk
126 ## objects. @person_lines is a map from row #s to Person objects.
127
128 def initialize thread, hidden_labels=[], index_mode=nil
129 @indent_spaces = $config[:indent_spaces]
130 super :slip_rows => $config[:slip_rows]
131 @thread = thread
132 @hidden_labels = hidden_labels
133
134 ## used for dispatch-and-next
135 @index_mode = index_mode
136 @dying = false
137
138 @layout = SavingHash.new { MessageLayout.new }
139 @chunk_layout = SavingHash.new { ChunkLayout.new }
140 earliest, latest = nil, nil
141 latest_date = nil
142 altcolor = false
143
144 @thread.each do |m, d, p|
145 next unless m
146 earliest ||= m
147 @layout[m].state = initial_state_for m
148 @layout[m].toggled_state = false
149 @layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color
150 @layout[m].star_color = altcolor ? :alternate_starred_patina_color : :starred_patina_color
151 @layout[m].orig_new = m.has_label? :read
152 altcolor = !altcolor
153 if latest_date.nil? || m.date > latest_date
154 latest_date = m.date
155 latest = m
156 end
157 end
158
159 @wrap = true
160
161 @layout[latest].state = :open if @layout[latest].state == :closed
162 @layout[earliest].state = :detailed if earliest.has_label?(:unread) || @thread.size == 1
163 end
164
165 def toggle_wrap
166 @wrap = !@wrap
167 regen_text
168 buffer.mark_dirty if buffer
169 end
170
171 def draw_line ln, opts={}
172 if ln == curpos
173 super ln, :highlight => true
174 else
175 super
176 end
177 end
178 def lines; @text.length; end
179 def [] i; @text[i]; end
180
181 ## a little hacky---since regen_text can depend on buffer features like the
182 ## content_width, we don't call it in the constructor, and instead call it
183 ## here, which is set before we're responsible for drawing ourself.
184 def buffer= b
185 super
186 regen_text
187 end
188
189 def show_header
190 m = @message_lines[curpos] or return
191 BufferManager.spawn_unless_exists("Full header for #{m.id}") do
192 TextMode.new m.raw_header.ascii
193 end
194 end
195
196 def show_message
197 m = @message_lines[curpos] or return
198 BufferManager.spawn_unless_exists("Raw message for #{m.id}") do
199 TextMode.new m.raw_message.ascii
200 end
201 end
202
203 def toggle_detailed_header
204 m = @message_lines[curpos] or return
205 @layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
206 update
207 end
208
209 def reload
210 update
211 end
212
213 def reply type_arg=nil
214 m = @message_lines[curpos] or return
215 mode = ReplyMode.new m, type_arg
216 BufferManager.spawn "Reply to #{m.subj}", mode
217 end
218
219 def reply_all; reply :all; end
220
221 def subscribe_to_list
222 m = @message_lines[curpos] or return
223 if m.list_subscribe && m.list_subscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
224 ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "subscribe")
225 else
226 BufferManager.flash "Can't find List-Subscribe header for this message."
227 end
228 end
229
230 def unsubscribe_from_list
231 m = @message_lines[curpos] or return
232 BufferManager.flash "Can't find List-Unsubscribe header for this message." unless m.list_unsubscribe
233
234 if m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
235 ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "unsubscribe")
236 elsif m.list_unsubscribe =~ /<(http.*)?>/
237 unless HookManager.enabled? "goto"
238 BufferManager.flash "You must add a goto.rb hook before you can goto an unsubscribe URI."
239 return
240 end
241
242 begin
243 u = URI.parse($1)
244 rescue URI::InvalidURIError
245 BufferManager.flash("Invalid unsubscribe link")
246 return
247 end
248
249 HookManager.run "goto", :uri => Shellwords.escape(u.to_s)
250 end
251 end
252
253 def forward
254 if(chunk = @chunk_lines[curpos]) && chunk.is_a?(Chunk::Attachment)
255 ForwardMode.spawn_nicely :attachments => [chunk]
256 elsif(m = @message_lines[curpos])
257 ForwardMode.spawn_nicely :message => m
258 end
259 end
260
261 def bounce
262 m = @message_lines[curpos] or return
263 to = BufferManager.ask_for_contacts(:people, "Bounce To: ") or return
264
265 defcmd = AccountManager.default_account.bounce_sendmail
266
267 cmd = case (hookcmd = HookManager.run "bounce-command", :from => m.from, :to => to)
268 when nil, /^$/ then defcmd
269 else hookcmd
270 end + ' ' + to.map { |t| t.email }.join(' ')
271
272 bt = to.size > 1 ? "#{to.size} recipients" : to[0].to_s
273
274 if BufferManager.ask_yes_or_no "Really bounce to #{bt}?"
275 debug "bounce command: #{cmd}"
276 begin
277 IO.popen(cmd, 'w') do |sm|
278 sm.puts m.raw_message
279 end
280 raise SendmailCommandFailed, "Couldn't execute #{cmd}" unless $? == 0
281 m.add_label :forwarded
282 Index.save_message m
283 rescue SystemCallError, SendmailCommandFailed => e
284 warn "problem sending mail: #{e.message}"
285 BufferManager.flash "Problem sending mail: #{e.message}"
286 end
287 end
288 end
289
290 include CanAliasContacts
291 def alias
292 p = @person_lines[curpos] or return
293 alias_contact p
294 update
295 end
296
297 def search
298 p = @person_lines[curpos] or return
299 mode = PersonSearchResultsMode.new [p]
300 BufferManager.spawn "Search for #{p.name}", mode
301 end
302
303 def compose
304 p = @person_lines[curpos]
305 if p
306 ComposeMode.spawn_nicely :to_default => p
307 else
308 ComposeMode.spawn_nicely
309 end
310 end
311
312 def edit_labels
313 old_labels = @thread.labels
314 reserved_labels = old_labels.select { |l| LabelManager::RESERVED_LABELS.include? l }
315 new_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", @thread.labels.sort_by {|x| x.to_s}
316
317 return unless new_labels
318 @thread.labels = Set.new(reserved_labels) + new_labels
319 new_labels.each { |l| LabelManager << l }
320 update
321 UpdateManager.relay self, :labeled, @thread.first
322 Index.save_thread @thread
323 UndoManager.register "labeling thread" do
324 @thread.labels = old_labels
325 Index.save_thread @thread
326 UpdateManager.relay self, :labeled, @thread.first
327 end
328 end
329
330 def toggle_starred
331 m = @message_lines[curpos] or return
332 toggle_label m, :starred
333 end
334
335 def toggle_new
336 m = @message_lines[curpos] or return
337 toggle_label m, :unread
338 end
339
340 def toggle_label m, label
341 if m.has_label? label
342 m.remove_label label
343 else
344 m.add_label label
345 end
346 ## TODO: don't recalculate EVERYTHING just to add a stupid little
347 ## star to the display
348 update
349 UpdateManager.relay self, :single_message_labeled, m
350 Index.save_thread @thread
351 end
352
353 ## called when someone presses enter when the cursor is highlighting
354 ## a chunk. for expandable chunks (including messages) we toggle
355 ## open/closed state; for viewable chunks (like attachments) we
356 ## view.
357 def activate_chunk
358 chunk = @chunk_lines[curpos] or return
359 if chunk.is_a? Chunk::Text
360 ## if the cursor is over a text region, expand/collapse the
361 ## entire message
362 chunk = @message_lines[curpos]
363 end
364 layout = if chunk.is_a?(Message)
365 @layout[chunk]
366 elsif chunk.expandable?
367 @chunk_layout[chunk]
368 end
369 if layout
370 layout.state = (layout.state != :closed ? :closed : :open)
371 #cursor_down if layout.state == :closed # too annoying
372 update
373 elsif chunk.viewable?
374 view chunk
375 end
376 if chunk.is_a?(Message) && $config[:jump_to_open_message]
377 jump_to_message chunk
378 jump_to_next_open if layout.state == :closed
379 end
380 end
381
382 def edit_as_new
383 m = @message_lines[curpos] or return
384 mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc, :refs => m.refs, :replytos => m.replytos)
385 BufferManager.spawn "edit as new", mode
386 mode.default_edit_message
387 end
388
389 def save_to_disk
390 chunk = @chunk_lines[curpos] or return
391 case chunk
392 when Chunk::Attachment
393 default_dir = $config[:default_attachment_save_dir]
394 default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
395 default_fn = File.expand_path File.join(default_dir, chunk.filesafe_filename)
396 fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
397
398 # if user selects directory use file name from message
399 if fn and File.directory? fn
400 fn = File.join(fn, chunk.filename)
401 end
402
403 save_to_file(fn) { |f| f.print chunk.raw_content } if fn
404 else
405 m = @message_lines[curpos]
406 fn = BufferManager.ask_for_filename :filename, "Save message to file: "
407 return unless fn
408 save_to_file(fn) do |f|
409 m.each_raw_message_line { |l| f.print l }
410 end
411 end
412 end
413
414 def save_all_to_disk
415 m = @message_lines[curpos] or return
416 default_dir = ($config[:default_attachment_save_dir] || ".")
417 folder = BufferManager.ask_for_filename :filename, "Save all attachments to folder: ", default_dir, true
418 return unless folder
419
420 num = 0
421 num_errors = 0
422 m.chunks.each do |chunk|
423 next unless chunk.is_a?(Chunk::Attachment)
424 fn = File.join(folder, chunk.filesafe_filename)
425 num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
426 num += 1
427 end
428
429 if num == 0
430 BufferManager.flash "Didn't find any attachments!"
431 else
432 if num_errors == 0
433 BufferManager.flash "Wrote #{num.pluralize 'attachment'} to #{folder}."
434 else
435 BufferManager.flash "Wrote #{(num - num_errors).pluralize 'attachment'} to #{folder}; couldn't write #{num_errors} of them (see log)."
436 end
437 end
438 end
439
440 def publish
441 chunk = @chunk_lines[curpos] or return
442 if HookManager.enabled? "publish"
443 HookManager.run "publish", :chunk => chunk
444 else
445 BufferManager.flash "Publishing hook not defined."
446 end
447 end
448
449 def edit_draft
450 m = @message_lines[curpos] or return
451 if m.is_draft?
452 mode = ResumeMode.new m
453 BufferManager.spawn "Edit message", mode
454 BufferManager.kill_buffer self.buffer
455 mode.default_edit_message
456 else
457 BufferManager.flash "Not a draft message!"
458 end
459 end
460
461 def send_draft
462 m = @message_lines[curpos] or return
463 if m.is_draft?
464 mode = ResumeMode.new m
465 BufferManager.spawn "Send message", mode
466 BufferManager.kill_buffer self.buffer
467 mode.send_message
468 else
469 BufferManager.flash "Not a draft message!"
470 end
471 end
472
473 def jump_to_first_open
474 m = @message_lines[0] or return
475 if @layout[m].state != :closed
476 jump_to_message m#, true
477 else
478 jump_to_next_open #true
479 end
480 end
481
482 def jump_to_next_and_open
483 return continue_search_in_buffer if in_search? # err.. don't know why im doing this
484
485 m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
486 return unless m
487
488 nextm = @layout[m].next
489 return unless nextm
490
491 if @layout[m].toggled_state == true
492 @layout[m].state = :closed
493 @layout[m].toggled_state = false
494 update
495 end
496
497 if @layout[nextm].state == :closed
498 @layout[nextm].state = :open
499 @layout[nextm].toggled_state = true
500 end
501
502 jump_to_message nextm if nextm
503
504 update if @layout[nextm].toggled_state
505 end
506
507 def jump_to_next_open force_alignment=nil
508 return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations
509 m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
510 return unless m
511 while nextm = @layout[m].next
512 break if @layout[nextm].state != :closed
513 m = nextm
514 end
515 jump_to_message nextm, force_alignment if nextm
516 end
517
518 def align_current_message
519 m = @message_lines[curpos] or return
520 jump_to_message m, true
521 end
522
523 def jump_to_prev_and_open force_alignment=nil
524 m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] }
525 return unless m
526
527 nextm = @layout[m].prev
528 return unless nextm
529
530 if @layout[m].toggled_state == true
531 @layout[m].state = :closed
532 @layout[m].toggled_state = false
533 update
534 end
535
536 if @layout[nextm].state == :closed
537 @layout[nextm].state = :open
538 @layout[nextm].toggled_state = true
539 end
540
541 jump_to_message nextm if nextm
542 update if @layout[nextm].toggled_state
543 end
544
545 def jump_to_prev_open
546 m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] } # bah, .to_a
547 return unless m
548 ## jump to the top of the current message if we're in the body;
549 ## otherwise, to the previous message
550
551 top = @layout[m].top
552 if curpos == top
553 while(prevm = @layout[m].prev)
554 break if @layout[prevm].state != :closed
555 m = prevm
556 end
557 jump_to_message prevm if prevm
558 else
559 jump_to_message m
560 end
561 end
562
563 def jump_to_message m, force_alignment=false
564 l = @layout[m]
565
566 ## boundaries of the message
567 message_left = l.depth * @indent_spaces
568 message_right = message_left + l.width
569
570 ## calculate leftmost colum
571 left = if force_alignment # force mode: align exactly
572 message_left
573 else # regular: minimize cursor movement
574 ## leftmost and rightmost are boundaries of all valid left-column
575 ## alignments.
576 leftmost = [message_left, message_right - buffer.content_width + 1].min
577 rightmost = message_left
578 leftcol.clamp(leftmost, rightmost)
579 end
580
581 jump_to_line l.top # move vertically
582 jump_to_col left # move horizontally
583 set_cursor_pos l.top # set cursor pos
584 end
585
586 def expand_all_messages
587 @global_message_state ||= :closed
588 @global_message_state = (@global_message_state == :closed ? :open : :closed)
589 @layout.each { |m, l| l.state = @global_message_state }
590 update
591 end
592
593 def collapse_non_new_messages
594 @layout.each { |m, l| l.state = l.orig_new ? :open : :closed }
595 update
596 end
597
598 def expand_all_quotes
599 if(m = @message_lines[curpos])
600 quotes = m.chunks.select { |c| (c.is_a?(Chunk::Quote) || c.is_a?(Chunk::Signature)) && c.lines.length > 1 }
601 numopen = quotes.inject(0) { |s, c| s + (@chunk_layout[c].state == :open ? 1 : 0) }
602 newstate = numopen > quotes.length / 2 ? :closed : :open
603 quotes.each { |c| @chunk_layout[c].state = newstate }
604 update
605 end
606 end
607
608 def cleanup
609 @layout = @chunk_layout = @text = nil # for good luck
610 end
611
612 def archive_and_kill; archive_and_then :kill end
613 def spam_and_kill; spam_and_then :kill end
614 def delete_and_kill; delete_and_then :kill end
615 def kill_and_kill; kill_and_then :kill end
616 def unread_and_kill; unread_and_then :kill end
617 def do_nothing_and_kill; do_nothing_and_then :kill end
618
619 def archive_and_next; archive_and_then :next end
620 def spam_and_next; spam_and_then :next end
621 def delete_and_next; delete_and_then :next end
622 def kill_and_next; kill_and_then :next end
623 def unread_and_next; unread_and_then :next end
624 def do_nothing_and_next; do_nothing_and_then :next end
625
626 def archive_and_prev; archive_and_then :prev end
627 def spam_and_prev; spam_and_then :prev end
628 def delete_and_prev; delete_and_then :prev end
629 def kill_and_prev; kill_and_then :prev end
630 def unread_and_prev; unread_and_then :prev end
631 def do_nothing_and_prev; do_nothing_and_then :prev end
632
633 def archive_and_then op
634 dispatch op do
635 @thread.remove_label :inbox
636 UpdateManager.relay self, :archived, @thread.first
637 Index.save_thread @thread
638 UndoManager.register "archiving 1 thread" do
639 @thread.apply_label :inbox
640 Index.save_thread @thread
641 UpdateManager.relay self, :unarchived, @thread.first
642 end
643 end
644 end
645
646 def spam_and_then op
647 dispatch op do
648 @thread.apply_label :spam
649 UpdateManager.relay self, :spammed, @thread.first
650 Index.save_thread @thread
651 UndoManager.register "marking 1 thread as spam" do
652 @thread.remove_label :spam
653 Index.save_thread @thread
654 UpdateManager.relay self, :unspammed, @thread.first
655 end
656 end
657 end
658
659 def delete_and_then op
660 dispatch op do
661 @thread.apply_label :deleted
662 UpdateManager.relay self, :deleted, @thread.first
663 Index.save_thread @thread
664 UndoManager.register "deleting 1 thread" do
665 @thread.remove_label :deleted
666 Index.save_thread @thread
667 UpdateManager.relay self, :undeleted, @thread.first
668 end
669 end
670 end
671
672 def kill_and_then op
673 dispatch op do
674 @thread.apply_label :killed
675 UpdateManager.relay self, :killed, @thread.first
676 Index.save_thread @thread
677 UndoManager.register "killed 1 thread" do
678 @thread.remove_label :killed
679 Index.save_thread @thread
680 UpdateManager.relay self, :unkilled, @thread.first
681 end
682 end
683 end
684
685 def unread_and_then op
686 dispatch op do
687 @thread.apply_label :unread
688 UpdateManager.relay self, :unread, @thread.first
689 Index.save_thread @thread
690 end
691 end
692
693 def do_nothing_and_then op
694 dispatch op
695 end
696
697 def dispatch op
698 return if @dying
699 @dying = true
700
701 l = lambda do
702 yield if block_given?
703 BufferManager.kill_buffer_safely buffer
704 end
705
706 case op
707 when :next
708 @index_mode.launch_next_thread_after @thread, &l
709 when :prev
710 @index_mode.launch_prev_thread_before @thread, &l
711 when :kill
712 l.call
713 else
714 raise ArgumentError, "unknown thread dispatch operation #{op.inspect}"
715 end
716 end
717 private :dispatch
718
719 def pipe_message
720 chunk = @chunk_lines[curpos]
721 chunk = nil unless chunk.is_a?(Chunk::Attachment)
722 message = @message_lines[curpos] unless chunk
723
724 return unless chunk || message
725
726 command = BufferManager.ask(:shell, "pipe command: ")
727 return if command.nil? || command.empty?
728
729 output, success = pipe_to_process(command) do |stream|
730 if chunk
731 stream.print chunk.raw_content
732 else
733 message.each_raw_message_line { |l| stream.print l }
734 end
735 end
736
737 unless success
738 BufferManager.flash "Invalid command: '#{command}' is not an executable"
739 return
740 end
741
742 if output
743 BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
744 else
745 BufferManager.flash "'#{command}' done!"
746 end
747 end
748
749
750 def status
751 user_labels = @thread.labels.to_a.map do |l|
752 l.to_s if LabelManager.user_defined_labels.member?(l)
753 end.compact.join(",")
754 user_labels = (user_labels.empty? and "" or "<#{user_labels}>")
755 [user_labels, super].join(" -- ")
756 end
757
758 def goto_uri
759 unless (chunk = @chunk_lines[curpos])
760 BufferManager.flash "No URI found."
761 return
762 end
763 unless HookManager.enabled? "goto"
764 BufferManager.flash "You must add a goto.rb hook before you can goto a URI."
765 return
766 end
767
768 # @text is a list of lines with this format:
769 # [
770 # [[:text_color, "Some text"]]
771 # [[:text_color, " continued here"]]
772 # ]
773
774 linetext = @text.slice(curpos, @text.length).flatten(1)
775 .take_while{|d| [:text_color, :sig_color].include?(d[0]) and d[1].strip != ""} # Only take up to the first "" alone on its line
776 .map{|d| d[1].strip}.join("").strip
777
778 found = false
779 URI.extract(linetext || "").each do |match|
780 begin
781 u = URI.parse(match)
782 next unless u.absolute?
783 next unless ["http", "https"].include?(u.scheme)
784
785 reallink = Shellwords.escape(u.to_s)
786 BufferManager.flash "Going to #{reallink} ..."
787 HookManager.run "goto", :uri => reallink
788 BufferManager.completely_redraw_screen
789 found = true
790
791 rescue URI::InvalidURIError => e
792 debug "not a uri: #{e}"
793 # Do nothing, this is an ok flow
794 end
795 end
796 BufferManager.flash "No URI found." unless found
797 end
798
799 def fetch_and_verify
800 message = @message_lines[curpos]
801 crypto_chunk = message.chunks.select {|chunk| chunk.is_a?(Chunk::CryptoNotice)}.first
802 return unless crypto_chunk
803 return unless crypto_chunk.unknown_fingerprint
804
805 BufferManager.flash "Retrieving key #{crypto_chunk.unknown_fingerprint} ..."
806
807 error = CryptoManager.retrieve crypto_chunk.unknown_fingerprint
808
809 if error
810 BufferManager.flash "Couldn't retrieve key: #{error.to_s}"
811 else
812 BufferManager.flash "Key #{crypto_chunk.unknown_fingerprint} successfully retrieved !"
813 end
814
815 # Re-trigger gpg verification
816 message.reload_from_source!
817 update
818 end
819
820 private
821
822 def initial_state_for m
823 if m.has_label?(:starred) || m.has_label?(:unread)
824 :open
825 else
826 :closed
827 end
828 end
829
830 def update
831 regen_text
832 buffer.mark_dirty if buffer
833 end
834
835 ## here we generate the actual content lines. we accumulate
836 ## everything into @text, and we set @chunk_lines and
837 ## @message_lines, and we update @layout.
838 def regen_text
839 @text = []
840 @chunk_lines = []
841 @message_lines = []
842 @person_lines = []
843
844 prevm = nil
845 @thread.each do |m, depth, parent|
846 unless m.is_a? Message # handle nil and :fake_root
847 @text += chunk_to_lines m, nil, @text.length, depth, parent
848 next
849 end
850 l = @layout[m]
851
852 ## is this still necessary?
853 next unless @layout[m].state # skip discarded drafts
854
855 ## build the patina
856 text = chunk_to_lines m, l.state, @text.length, depth, parent, l.color, l.star_color
857
858 l.top = @text.length
859 l.bot = @text.length + text.length # updated below
860 l.prev = prevm
861 l.next = nil
862 l.depth = depth
863 # l.state we preserve
864 l.width = 0 # updated below
865 @layout[l.prev].next = m if l.prev
866
867 (0 ... text.length).each do |i|
868 @chunk_lines[@text.length + i] = m
869 @message_lines[@text.length + i] = m
870 end
871
872 @text += text
873 prevm = m
874 if l.state != :closed
875 m.chunks.each do |c|
876 cl = @chunk_layout[c]
877
878 ## set the default state for chunks
879 cl.state ||=
880 if c.expandable? && c.respond_to?(:initial_state)
881 c.initial_state
882 else
883 :closed
884 end
885
886 text = chunk_to_lines c, cl.state, @text.length, depth
887 (0 ... text.length).each do |i|
888 @chunk_lines[@text.length + i] = c
889 @message_lines[@text.length + i] = m
890 lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * @indent_spaces)
891 l.width = lw if lw > l.width
892 end
893 @text += text
894 end
895 @layout[m].bot = @text.length
896 end
897 end
898 end
899
900 def message_patina_lines m, state, start, parent, prefix, color, star_color
901 prefix_widget = [color, prefix]
902
903 open_widget = [color, (state == :closed ? "+ " : "- ")]
904 new_widget = [color, (m.has_label?(:unread) ? "N" : " ")]
905 starred_widget = if m.has_label?(:starred)
906 [star_color, "*"]
907 else
908 [color, " "]
909 end
910 attach_widget = [color, (m.has_label?(:attachment) ? "@" : " ")]
911
912 case state
913 when :open
914 @person_lines[start] = m.from
915 [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
916 [color,
917 "#{m.from ? m.from.mediumname.fix_encoding! : '?'} to #{m.recipients.map { |l| l.shortname.fix_encoding! }.join(', ')} #{m.date.to_nice_s.fix_encoding!} (#{m.date.to_nice_distance_s.fix_encoding!})"]]]
918
919 when :closed
920 @person_lines[start] = m.from
921 [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
922 [color,
923 "#{m.from ? m.from.mediumname.fix_encoding! : '?'}, #{m.date.to_nice_s.fix_encoding!} (#{m.date.to_nice_distance_s.fix_encoding!}) #{m.snippet ? m.snippet.fix_encoding! : ''}"]]]
924
925 when :detailed
926 @person_lines[start] = m.from
927 from_line = [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
928 [color, "From: #{m.from ? format_person(m.from) : '?'}"]]]
929
930 addressee_lines = []
931 unless m.to.empty?
932 m.to.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
933 addressee_lines += format_person_list " To: ", m.to
934 end
935 unless m.cc.empty?
936 m.cc.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
937 addressee_lines += format_person_list " Cc: ", m.cc
938 end
939 unless m.bcc.empty?
940 m.bcc.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
941 addressee_lines += format_person_list " Bcc: ", m.bcc
942 end
943
944 headers = {
945 "Date" => "#{m.date.to_message_nice_s} (#{m.date.to_nice_distance_s})",
946 "Subject" => m.subj
947 }
948
949 show_labels = @thread.labels - LabelManager::HIDDEN_RESERVED_LABELS
950 unless show_labels.empty?
951 headers["Labels"] = show_labels.map { |x| x.to_s }.sort.join(', ')
952 end
953 if parent
954 headers["In reply to"] = "#{parent.from.mediumname}'s message of #{parent.date.to_message_nice_s}"
955 end
956
957 HookManager.run "detailed-headers", :message => m, :headers => headers
958
959 from_line + (addressee_lines + headers.map { |k, v| " #{k}: #{v}" }).map { |l| [[color, prefix + " " + l]] }
960 end
961 end
962
963 def format_person_list prefix, people
964 ptext = people.map { |p| format_person p }
965 pad = " " * prefix.display_length
966 [prefix + ptext.first + (ptext.length > 1 ? "," : "")] +
967 ptext[1 .. -1].map_with_index do |e, i|
968 pad + e + (i == ptext.length - 1 ? "" : ",")
969 end
970 end
971
972 def format_person p
973 p.longname + (ContactManager.is_aliased_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
974 end
975
976 def maybe_wrap_text lines
977 if @wrap
978 config_width = $config[:wrap_width]
979 if config_width and config_width != 0
980 width = [config_width, buffer.content_width].min
981 else
982 width = buffer.content_width
983 end
984 # lines can apparently be both String and Array, convert to Array for map.
985 if lines.kind_of? String
986 lines = lines.lines.to_a
987 end
988 lines = lines.map { |l| l.chomp.wrap width if l }.flatten
989 end
990 return lines
991 end
992
993 ## todo: check arguments on this overly complex function
994 def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
995 prefix = " " * @indent_spaces * depth
996 case chunk
997 when :fake_root
998 [[[:missing_message_color, "#{prefix}<one or more unreceived messages>"]]]
999 when nil
1000 [[[:missing_message_color, "#{prefix}<an unreceived message>"]]]
1001 when Message
1002 message_patina_lines(chunk, state, start, parent, prefix, color, star_color) +
1003 (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. Hit 'e' to edit, 'y' to send. <<<"]]] : [])
1004
1005 else
1006 raise "Bad chunk: #{chunk.inspect}" unless chunk.respond_to?(:inlineable?) ## debugging
1007 if chunk.inlineable?
1008 lines = maybe_wrap_text(chunk.lines)
1009 lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
1010 elsif chunk.expandable?
1011 case state
1012 when :closed
1013 [[[chunk.patina_color, "#{prefix}+ #{chunk.patina_text}"]]]
1014 when :open
1015 lines = maybe_wrap_text(chunk.lines)
1016 [[[chunk.patina_color, "#{prefix}- #{chunk.patina_text}"]]] + lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
1017 end
1018 else
1019 [[[chunk.patina_color, "#{prefix}x #{chunk.patina_text}"]]]
1020 end
1021 end
1022 end
1023
1024 def view chunk
1025 BufferManager.flash "viewing #{chunk.content_type} attachment..."
1026 success = chunk.view!
1027 BufferManager.erase_flash
1028 BufferManager.completely_redraw_screen
1029 unless success
1030 BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s.ascii, chunk.filename)
1031 BufferManager.flash "Couldn't execute view command, viewing as text."
1032 end
1033 end
1034 end
1035
1036 end