lib/sup/mbox/ssh-file.rb
lib/sup/mbox/ssh-loader.rb
lib/sup/message.rb
+lib/sup/message-chunks.rb
lib/sup/mode.rb
lib/sup/modes/buffer-list-mode.rb
lib/sup/modes/completion-mode.rb
require "sup/update"
require "sup/suicide"
+require "sup/message-chunks"
require "sup/message"
require "sup/source"
require "sup/mbox"
module Redwood
-class CryptoSignature
- attr_reader :lines, :status, :description
-
- def initialize status, description, lines
- @status = status
- @description = description
- @lines = lines
- end
-end
-
-class CryptoDecryptedNotice
- attr_reader :lines, :status, :description
-
- def initialize status, description, lines=[]
- @status = status
- @description = description
- @lines = lines
- end
-end
-
class CryptoManager
include Singleton
if gpg_output =~ /^gpg: (.* signature from .*$)/
if $? == 0
- CryptoSignature.new :valid, $1, output_lines
+ Chunk::CryptoNotice.new :valid, $1, output_lines
else
- CryptoSignature.new :invalid, $1, output_lines
+ Chunk::CryptoNotice.new :invalid, $1, output_lines
end
else
unknown_status output_lines
sig =
if sig_lines # encrypted & signed
if sig_lines =~ /^gpg: (Good signature from .*$)/
- CryptoSignature.new :valid, $1, sig_lines.split("\n")
+ Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
else
- CryptoSignature.new :invalid, $1, sig_lines.split("\n")
+ Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
end
end
- notice = CryptoDecryptedNotice.new :valid, "This message has been decrypted for display"
+ notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
[RMail::Parser.read(decrypted_payload), sig, notice]
else
- notice = CryptoDecryptedNotice.new :invalid, "This message could not be decrypted", gpg_output.split("\n")
+ notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", gpg_output.split("\n")
[nil, nil, notice]
end
end
private
def unknown_status lines=[]
- CryptoSignature.new :unknown, "Unable to determine validity of cryptographic signature", lines
+ Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
end
def cant_find_binary
end
end
end
-
-
-## to check:
-## failed decryption
-## decryption but failed signature
-## no gpg found
-## multiple private keys
--- /dev/null
+## Here we define all the "chunks" that a message is parsed
+## into. Chunks are used by ThreadViewMode to render a message. Chunks
+## are used for both MIME stuff like attachments, for Sup's parsing of
+## the message body into text, quote, and signature regions, and for
+## notices like "this message was decrypted" or "this message contains
+## a valid signature"---basically, anything we want to differentiate
+## at display time.
+
+## A chunk can be inlineable, expandable, or viewable. If it's
+## inlineable, #color and #lines are called and the output is treated
+## as part of the message text. This is how Text and one-line Quotes
+## and Signatures work.
+
+## If it's expandable, #patina_color and #patina_text are called to
+## generate a "patina" (a one-line widget, basically), and the user
+## can press enter to toggle the display of the chunk content, which
+## is generated from #color and #lines). This is how Quote, Signature,
+## and most widgets work.
+
+## If it's viewable, a patina is displayed using #patina_color and
+## #patina_text, but no toggling is allowed. Instead, if #view! is
+## defined, pressing enter on the widget calls view! and (if that
+## returns false) #to_s. Otherwise enter does nothing. This is how
+## non-inlineable attachments work.
+
+module Redwood
+module Chunk
+ class Attachment
+ HookManager.register "mime-decode", <<EOS
+Executes when decoding a MIME attachment.
+Variables:
+ content_type: the content-type of the message
+ filename: the filename of the attachment as saved to disk (generated
+ on the fly, so don't call more than once)
+ sibling_types: if this attachment is part of a multipart MIME attachment,
+ an array of content-types for all attachments. Otherwise,
+ the empty array.
+Return value:
+ The decoded text of the attachment, or nil if not decoded.
+EOS
+#' stupid ruby-mode
+
+ ## raw_content is the post-MIME-decode content. this is used for
+ ## saving the attachment to disk.
+ attr_reader :content_type, :filename, :lines, :raw_content
+
+ def initialize content_type, filename, encoded_content, sibling_types
+ @content_type = content_type
+ @filename = filename
+ @raw_content = encoded_content.decode
+
+ @lines =
+ case @content_type
+ when /^text\/plain\b/
+ Message.convert_from(@raw_content, encoded_content.charset).split("\n")
+ else
+ text = HookManager.run "mime-decode", :content_type => content_type,
+ :filename => lambda { write_to_disk },
+ :sibling_types => sibling_types
+ text.split("\n") if text
+ end
+ end
+
+ def color; :none end
+ def patina_color; :attachment_color end
+ def patina_text
+ if expandable?
+ "Attachment: #{filename} (#{lines.length} lines)"
+ else
+ "Attachment: #{filename} (#{content_type})"
+ end
+ end
+
+ ## an attachment is exapndable if we've managed to decode it into
+ ## something we can display inline. otherwise, it's viewable.
+ def inlineable?; false end
+ def expandable?; !viewable? end
+ def viewable?; @lines.nil? end
+ def view!
+ file = Tempfile.new "redwood.attachment"
+ file.print @raw_content
+ file.close
+ system "/usr/bin/run-mailcap --action=view #{@content_type}:#{file.path} >& /dev/null"
+ $? == 0
+ end
+
+ ## used when viewing the attachment as text
+ def to_s
+ @lines || @raw_content
+ end
+ end
+
+ class Text
+ WRAP_LEN = 80 # wrap at this width
+
+ attr_reader :lines
+ def initialize lines
+ @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten # wrap
+
+ ## trim off all empty lines except one
+ lines.pop while lines.last =~ /^\s*$/
+ end
+
+ def color; :none end
+ def inlineable?; true end
+ end
+
+ class Quote
+ attr_reader :lines
+ def initialize lines
+ @lines = lines
+ end
+
+ def inlineable?; @lines.length == 1 end
+ def expandable?; !inlineable? end
+ def patina_color; :quote_patina_color end
+ def patina_text; "(#{lines.length} quoted lines)" end
+ def color; :quote_color end
+ end
+
+ class Signature
+ attr_reader :lines
+ def initialize lines
+ @lines = lines
+ end
+
+ def inlineable?; @lines.length == 1 end
+ def expandable?; !inlineable? end
+ def patina_color; :sig_patina_color end
+ def patina_text; "(#{lines.length}-line signature)" end
+ def color; :sig_color end
+ end
+
+ class EnclosedMessageNotice
+ attr_reader :from
+ def initialize from
+ @from = from
+ end
+
+ def to_s
+ "Begin enclosed message from #{@from.longname}"
+ end
+ end
+
+ class CryptoNotice
+ attr_reader :lines, :status, :patina_text
+
+ def initialize status, description, lines=[]
+ @status = status
+ @patina_text = description
+ @lines = lines
+ end
+
+ def patina_color
+ case status
+ when :valid: :cryptosig_valid_color
+ when :invalid: :cryptosig_invalid_color
+ else :cryptosig_unknown_color
+ end
+ end
+ def color; patina_color end
+
+ def inlineable?; false end
+ def expandable?; !@lines.empty? end
+ def viewable?; false end
+ end
+end
+end
## sequences in the text of an email. (how sweet would that be?)
class Message
SNIPPET_LEN = 80
- WRAP_LEN = 80 # wrap at this width
RE_PATTERN = /^((re|re[\[\(]\d[\]\)]):\s*)+/i
- HookManager.register "mime-decode", <<EOS
-Executes when decoding a MIME attachment.
-Variables:
- content_type: the content-type of the message
- filename: the filename of the attachment as saved to disk (generated
- on the fly, so don't call more than once)
- sibling_types: if this attachment is part of a multipart MIME attachment,
- an array of content-types for all attachments. Otherwise,
- the empty array.
-Return value:
- The decoded text of the attachment, or nil if not decoded.
-EOS
-#' stupid ruby-mode
-
## some utility methods
class << self
def normalize_subj s; s.gsub(RE_PATTERN, ""); end
def reify_subj s; subj_is_reply?(s) ? s : "Re: " + s; end
end
- class Attachment
- ## encoded_content is still possible MIME-encoded
- ##
- ## raw_content is after decoding but before being turned into
- ## inlineable text.
- ##
- ## lines is array of inlineable text.
-
- attr_reader :content_type, :filename, :lines, :raw_content
-
- def initialize content_type, filename, encoded_content, sibling_types
- @content_type = content_type
- @filename = filename
- @raw_content = encoded_content.decode
-
- @lines =
- case @content_type
- when /^text\/plain\b/
- Message.convert_from(@raw_content, encoded_content.charset).split("\n")
- else
- text = HookManager.run "mime-decode", :content_type => content_type,
- :filename => lambda { write_to_disk },
- :sibling_types => sibling_types
- text.split("\n") if text
-
- end
- end
-
- def inlineable?; !@lines.nil? end
-
- def view!
- path = write_to_disk
- system "/usr/bin/run-mailcap --action=view #{@content_type}:#{path} >& /dev/null"
- $? == 0
- end
-
- ## used when viewing the attachment as text
- def to_s
- @lines || @raw_content
- end
-
- private
-
- def write_to_disk
- file = Tempfile.new "redwood.attachment"
- file.print @raw_content
- file.close
- file.path
- end
- end
-
- class Text
- attr_reader :lines
- def initialize lines
- ## do some wrapping
- @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten
- end
- end
-
- class Quote
- attr_reader :lines
- def initialize lines
- @lines = lines
- end
- end
-
- class Signature
- attr_reader :lines
- def initialize lines
- @lines = lines
- end
- end
-
QUOTE_PATTERN = /^\s{0,4}[>|\}]/
BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
QUOTE_START_PATTERN = /(^\s*Excerpts from)|(^\s*In message )|(^\s*In article )|(^\s*Quoting )|((wrote|writes|said|says)\s*:\s*$)/
def load_from_source!
@chunks ||=
if @source.has_errors?
- [Text.new(error_message(@source.error.message.split("\n")))]
+ [Chunk::Text.new(error_message(@source.error.message.split("\n")))]
else
begin
## we need to re-read the header because it contains information
## we need force_to_top here otherwise this window will cover
## up the error message one
Redwood::report_broken_sources :force_to_top => true
- [Text.new(error_message(e.message))]
+ [Chunk::Text.new(error_message(e.message))]
end
end
end
to.map { |p| "#{p.name} #{p.email}" },
cc.map { |p| "#{p.name} #{p.email}" },
bcc.map { |p| "#{p.name} #{p.email}" },
- chunks.select { |c| c.is_a? Text }.map { |c| c.lines },
+ chunks.select { |c| c.is_a? Chunk::Text }.map { |c| c.lines },
Message.normalize_subj(subj),
].flatten.compact.join " "
end
def basic_body_lines
- chunks.find_all { |c| c.is_a?(Text) || c.is_a?(Quote) }.map { |c| c.lines }.flatten
+ chunks.find_all { |c| c.is_a?(Chunk::Text) || c.is_a?(Chunk::Quote) }.map { |c| c.lines }.flatten
end
def basic_header_lines
## if there's a filename, we'll treat it as an attachment.
if filename
- [Attachment.new(m.header.content_type, filename, m, sibling_types)]
+ [Chunk::Attachment.new(m.header.content_type, filename, m, sibling_types)]
## otherwise, it's body text
else
end
if newstate
- chunks << Text.new(chunk_lines) unless chunk_lines.empty?
+ chunks << Chunk::Text.new(chunk_lines) unless chunk_lines.empty?
chunk_lines = [line]
state = newstate
else
if chunk_lines.empty?
# nothing
else
- chunks << Quote.new(chunk_lines)
+ chunks << Chunk::Quote.new(chunk_lines)
end
chunk_lines = [line]
state = newstate
## final object
case state
when :quote, :block_quote
- chunks << Quote.new(chunk_lines) unless chunk_lines.empty?
+ chunks << Chunk::Quote.new(chunk_lines) unless chunk_lines.empty?
when :text
- chunks << Text.new(chunk_lines) unless chunk_lines.empty?
+ chunks << Chunk::Text.new(chunk_lines) unless chunk_lines.empty?
when :sig
- chunks << Signature.new(chunk_lines) unless chunk_lines.empty?
+ chunks << Chunk::Signature.new(chunk_lines) unless chunk_lines.empty?
end
chunks
end
register_keymap do |k|
k.add :toggle_detailed_header, "Toggle detailed header", 'h'
k.add :show_header, "Show full message header", 'H'
- k.add :toggle_expanded, "Expand/collapse item", :enter
+ k.add :activate_chunk, "Expand/collapse or activate item", :enter
k.add :expand_all_messages, "Expand/collapse all messages", 'E'
k.add :edit_draft, "Edit draft", 'e'
k.add :edit_labels, "Edit or add labels for a thread", 'l'
UpdateManager.relay self, :label, m
end
- ## a little overly complicated. for quotes and signatures, if it's
- ## one line, we just display it and don't allow for
- ## collapsing/expanding. for crypto notices, we allow
- ## expanding/collapsing iff the # of notice lines is > 0.
- def toggle_expanded
+ ## called when someone presses enter when the cursor is highlighting
+ ## a chunk. for expandable chunks (including messages) we toggle
+ ## open/closed state; for viewable chunks (like attachments) we
+ ## view.
+ def activate_chunk
chunk = @chunk_lines[curpos] or return
- case chunk
- when Message
- l = @layout[chunk]
- l.state = (l.state != :closed ? :closed : :open)
- cursor_down if l.state == :closed
- when CryptoSignature, CryptoDecryptedNotice
- return if chunk.lines.empty?
- toggle_chunk_expansion chunk
- when Message::Quote, Message::Signature
- return if chunk.lines.length <= 1
- toggle_chunk_expansion chunk
- when Message::Attachment
- if chunk.inlineable?
- toggle_chunk_expansion chunk
- else
- view_attachment chunk
+ layout =
+ if chunk.is_a?(Message)
+ @layout[chunk]
+ elsif chunk.expandable?
+ @chunk_layout[chunk]
end
+ if layout
+ layout.state = (layout.state != :closed ? :closed : :open)
+ #cursor_down if layout.state == :closed # too annoying
+ update
+ elsif chunk.viewable?
+ view chunk
end
- update
end
def edit_as_new
def save_to_disk
chunk = @chunk_lines[curpos] or return
case chunk
- when Message::Attachment
+ when Chunk::Attachment
fn = BufferManager.ask_for_filename :filename, "Save attachment to file: ", chunk.filename
save_to_file(fn) { |f| f.print chunk.raw_content } if fn
else
def expand_all_quotes
if(m = @message_lines[curpos])
- quotes = m.chunks.select { |c| (c.is_a?(Message::Quote) || c.is_a?(Message::Signature)) && c.lines.length > 1 }
+ quotes = m.chunks.select { |c| (c.is_a?(Chunk::Quote) || c.is_a?(Chunk::Signature)) && c.lines.length > 1 }
numopen = quotes.inject(0) { |s, c| s + (@chunk_layout[c].state == :open ? 1 : 0) }
newstate = numopen > quotes.length / 2 ? :closed : :open
quotes.each { |c| @chunk_layout[c].state = newstate }
private
- def toggle_chunk_expansion chunk
- l = @chunk_layout[chunk]
- l.state = (l.state != :closed ? :closed : :open)
- cursor_down if l.state == :closed
- end
-
def initial_state_for m
if m.has_label?(:starred) || m.has_label?(:unread)
:open
## set the default state for chunks
cl.state ||=
- if c.is_a?(Message::Attachment) && c.inlineable?
+ if c.is_a?(Chunk::Attachment) && c.expandable?
:open
else
:closed
p.longname + (ContactManager.is_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
end
+ ## todo: check arguments on this overly complex function
def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
prefix = " " * INDENT_SPACES * depth
case chunk
when Message
message_patina_lines(chunk, state, start, parent, prefix, color, star_color) +
(chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
- when Message::Attachment
- return [[[:attachment_color, "#{prefix}x Attachment: #{chunk.filename} (#{chunk.content_type})"]]] unless chunk.inlineable?
- case state
- when :closed
- [[[:attachment_color, "#{prefix}+ Attachment: #{chunk.filename} (#{chunk.lines.length} lines)"]]]
- when :open
- [[[:attachment_color, "#{prefix}- Attachment: #{chunk.filename} (#{chunk.lines.length} lines)"]]] + chunk.lines.map { |line| [[:none, "#{prefix}#{line}"]] }
- end
- when Message::Text
- t = chunk.lines
- if t.last =~ /^\s*$/ && t.length > 1
- t.pop while t[-2] =~ /^\s*$/ # pop until only one file empty line
- end
- t.map { |line| [[:none, "#{prefix}#{line}"]] }
- when Message::Quote
- return [[[:quote_color, "#{prefix}#{chunk.lines.first}"]]] if chunk.lines.length == 1
- case state
- when :closed
- [[[:quote_patina_color, "#{prefix}+ (#{chunk.lines.length} quoted lines)"]]]
- when :open
- [[[:quote_patina_color, "#{prefix}- (#{chunk.lines.length} quoted lines)"]]] + chunk.lines.map { |line| [[:quote_color, "#{prefix}#{line}"]] }
- end
- when Message::Signature
- return [[[:sig_patina_color, "#{prefix}#{chunk.lines.first}"]]] if chunk.lines.length == 1
- case state
- when :closed
- [[[:sig_patina_color, "#{prefix}+ (#{chunk.lines.length}-line signature)"]]]
- when :open
- [[[:sig_patina_color, "#{prefix}- (#{chunk.lines.length}-line signature)"]]] + chunk.lines.map { |line| [[:sig_color, "#{prefix}#{line}"]] }
- end
- when CryptoSignature, CryptoDecryptedNotice
- color =
- case chunk.status
- when :valid: :cryptosig_valid_color
- when :invalid: :cryptosig_invalid_color
- else :cryptosig_unknown_color
- end
- widget = chunk.lines.empty? ? "x" : (state == :closed ? "+" : "-")
- case state
- when :closed
- [[[color, "#{prefix}#{widget} #{chunk.description}"]]]
- when :open
- [[[color, "#{prefix}#{widget} #{chunk.description}"]]] +
- chunk.lines.map { |line| [[color, "#{prefix}#{line}"]] }
- end
+
else
- raise "unknown chunk type #{chunk.class.name}"
+ if chunk.inlineable?
+ chunk.lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
+ elsif chunk.expandable?
+ case state
+ when :closed
+ [[[chunk.patina_color, "#{prefix}+ #{chunk.patina_text}"]]]
+ when :open
+ [[[chunk.patina_color, "#{prefix}- #{chunk.patina_text}"]]] + chunk.lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
+ end
+ else
+ [[[chunk.patina_color, "#{prefix}x #{chunk.patina_text}"]]]
+ end
end
end
- def view_attachment a
- BufferManager.flash "viewing #{a.content_type} attachment..."
- success = a.view!
+ def view chunk
+ BufferManager.flash "viewing #{chunk.content_type} attachment..."
+ success = chunk.view!
BufferManager.erase_flash
BufferManager.completely_redraw_screen
unless success
- BufferManager.spawn "Attachment: #{a.filename}", TextMode.new(a.to_s)
+ BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s)
BufferManager.flash "Couldn't execute view command, viewing as text."
end
end