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
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
earliest, latest = nil, nil
latest_date = nil
altcolor = false
+
@thread.each do |m, d, p|
next unless m
earliest ||= m
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
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: "
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
end
l = @layout[m]
+ ## is this still necessary?
next unless @layout[m].state # skip discarded drafts
## build the patina
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
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