From f6c8a40d7bb3339c0008f52f00271b48fbe18f3e Mon Sep 17 00:00:00 2001 From: wmorgan Date: Fri, 6 Jul 2007 18:46:49 +0000 Subject: [PATCH] better attachment viewing git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@475 5c8cc53c-5e98-4d25-b20a-d8db53a31250 --- lib/sup/message.rb | 109 ++++++++++++++++++++--------- lib/sup/modes/edit-message-mode.rb | 3 +- lib/sup/modes/thread-view-mode.rb | 42 ++++++++--- 3 files changed, 107 insertions(+), 47 deletions(-) diff --git a/lib/sup/message.rb b/lib/sup/message.rb index 7a0d550..9be71c1 100644 --- a/lib/sup/message.rb +++ b/lib/sup/message.rb @@ -26,27 +26,30 @@ class Message end class Attachment - attr_reader :content_type, :desc, :filename - def initialize content_type, desc, part + attr_reader :content_type, :filename, :content, :lines + def initialize content_type, filename, content @content_type = content_type - @desc = desc - @part = part - @file = nil - desc =~ /filename="?(.*?)("|$)/ && @filename = $1 + @filename = filename + @content = content + + if inlineable? + @lines = to_s.split("\n") + end end def view! - unless @file - @file = Tempfile.new "redwood.attachment" - @file.print self - @file.close - end + file = Tempfile.new "redwood.attachment" + file.print raw_content + file.close - system "/usr/bin/run-mailcap --action=view #{@content_type}:#{@file.path} >& /dev/null" + system "/usr/bin/run-mailcap --action=view #{@content_type}:#{file.path} >& /dev/null" $? == 0 end - def to_s; @part.decode; end + def to_s; Message.decode_and_convert @content; end + def raw_content; @content.decode end + + def inlineable?; @content_type =~ /^text\/plain/ end end class Text @@ -262,36 +265,72 @@ EOS private - ## (almost) everything rmail-specific goes here + ## here's where we handle decoding mime attachments. unfortunately + ## but unsurprisingly, the world of mime attachments is a bit of a + ## mess. as an empiricist, i'm basing the following behavior on + ## observed mail rather than on interpretations of rfcs, so probably + ## this will have to be tweaked. + ## + ## the general behavior i want is: ignore content-disposition, at + ## least in so far as it suggests something being inline vs being an + ## attachment. (because really, that should be the recipient's + ## decision to make.) if a mime part is text/plain, then decode it + ## and display it inline. if it has associated filename, then make + ## it collapsable and individually saveable; otherwise, treat it as + ## regular body text. + ## + ## so, in contrast to mutt, the user is not exposed to the workings + ## of the gruesome slaughterhouse and sausage factory that is a + ## mime-encoded message, but need only see the delicious end + ## product. def message_to_chunks m if m.multipart? - m.body.map { |p| message_to_chunks p }.flatten.compact + m.body.map { |p| message_to_chunks p }.flatten.compact # recurse else - case m.header.content_type - when "text/plain", nil - charset = - if m.header.field?("content-type") && m.header.fetch("content-type") =~ /charset=(.*?)(;|$)/ - $1 - end - - m.body && body = m.decode or raise MessageFormatError, "For some bizarre reason, RubyMail was unable to parse this message." - - if charset - begin - body = Iconv.iconv($encoding, charset, body).join - rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e - Redwood::log "warning: error decoding message body from #{charset}: #{e.message}" - end + filename = + ## first, paw through the headers looking for a filename + if m.header["Content-Disposition"] && + m.header["Content-Disposition"] =~ /filename="(.*?[^\\])"/ + $1 + elsif m.header["Content-Type"] && + m.header["Content-Type"] =~ /name=(.*?)(;|$)/ + $1 + + ## haven't found one, but it's a non-text message. fake + ## it. + elsif m.header["Content-Type"] && m.header["Content-Type"] !~ /^text\/plain/ + "sup-attachment-#{Time.now.to_i}-#{rand 10000}" end - text_to_chunks(body.normalize_whitespace.split("\n")) - when /^multipart\// - [] + ## if there's a filename, we'll treat it as an attachment. + if filename + [Attachment.new(m.header.content_type, filename, m)] + + ## otherwise, it's body text else - disp = m.header["Content-Disposition"] || "" - [Attachment.new(m.header.content_type, disp.gsub(/[\s\n]+/, " "), m)] + body = Message.decode_and_convert m + + text_to_chunks body.normalize_whitespace.split("\n") + end + end + end + + def self.decode_and_convert m + charset = + if m.header.field?("content-type") && m.header.fetch("content-type") =~ /charset=(.*?)(;|$)/ + $1 + end + + m.body && body = m.decode or raise MessageFormatError, "For some bizarre reason, RubyMail was unable to parse this message." + + if charset + begin + body = Iconv.iconv($encoding, charset, body).join + rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e + Redwood::log "warning: error decoding message body from #{charset}: #{e.message}" end end + body end ## parse the lines of text into chunk objects. the heuristics here diff --git a/lib/sup/modes/edit-message-mode.rb b/lib/sup/modes/edit-message-mode.rb index f0e4afb..e31b2c9 100644 --- a/lib/sup/modes/edit-message-mode.rb +++ b/lib/sup/modes/edit-message-mode.rb @@ -29,9 +29,8 @@ class EditMessageMode < LineCursorMode @attachments = [] @attachment_lines = {} @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>" - @edited = false - @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>" + super opts regen_text end diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb index b77f71b..d65330c 100644 --- a/lib/sup/modes/thread-view-mode.rb +++ b/lib/sup/modes/thread-view-mode.rb @@ -51,6 +51,7 @@ class ThreadViewMode < LineCursorMode earliest, latest = nil, nil latest_date = nil altcolor = false + @thread.each do |m, d, p| next unless m earliest ||= m @@ -150,16 +151,16 @@ class ThreadViewMode < LineCursorMode 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 + toggle_chunk_expansion chunk when Message::Quote, Message::Signature return if chunk.lines.length == 1 - l = @chunk_layout[chunk] - l.state = (l.state != :closed ? :closed : :open) - cursor_down if l.state == :closed + toggle_chunk_expansion chunk when Message::Attachment - view_attachment chunk + if chunk.inlineable? + toggle_chunk_expansion chunk + else + view_attachment chunk + end end update end @@ -176,7 +177,7 @@ class ThreadViewMode < LineCursorMode case chunk when Message::Attachment fn = BufferManager.ask :filename, "Save attachment to file: ", chunk.filename - save_to_file(fn) { |f| f.print chunk } if fn + save_to_file(fn) { |f| f.print chunk.raw_content } if fn else m = @message_lines[curpos] fn = BufferManager.ask :filename, "Save message to file: " @@ -280,6 +281,12 @@ class ThreadViewMode < LineCursorMode 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 @@ -310,6 +317,7 @@ private end l = @layout[m] + ## is this still necessary? next unless @layout[m].state # skip discarded drafts ## build the patina @@ -335,7 +343,15 @@ private if l.state != :closed m.chunks.each do |c| cl = @chunk_layout[c] - cl.state ||= :closed + + ## set the default state for chunks + cl.state ||= + if c.is_a?(Message::Attachment) && c.inlineable? + :open + else + :closed + end + text = chunk_to_lines c, cl.state, @text.length, depth (0 ... text.length).each do |i| @chunk_lines[@text.length + i] = c @@ -432,7 +448,13 @@ private 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 - [[[:attachment_color, "#{prefix}+ Attachment: #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]] + 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 -- 2.45.2