]> git.cworth.org Git - sup/commitdiff
better attachment viewing
authorwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Fri, 6 Jul 2007 18:46:49 +0000 (18:46 +0000)
committerwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Fri, 6 Jul 2007 18:46:49 +0000 (18:46 +0000)
git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@475 5c8cc53c-5e98-4d25-b20a-d8db53a31250

lib/sup/message.rb
lib/sup/modes/edit-message-mode.rb
lib/sup/modes/thread-view-mode.rb

index 7a0d550e618f2979883bcea969da48dcece3e2f3..9be71c1626f85b83efdafbd421775cfa0ce959c0 100644 (file)
@@ -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
index f0e4afbdc6a98cb7739e188ee31017e0a21e7bd4..e31b2c9c2ccc7e5d3a31dfef9fc3c5d93a1bdaa2 100644 (file)
@@ -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
index b77f71b43d155b61856c71f3194cc96ec856f456..d65330c0ca5c50087be92ca46476a6b412495115 100644 (file)
@@ -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