lib/sup/message.rb in sup-0.8.1 vs lib/sup/message.rb in sup-0.9
- old
+ new
@@ -44,11 +44,11 @@
@source = opts[:source] or raise ArgumentError, "source can't be nil"
@source_info = opts[:source_info] or raise ArgumentError, "source_info can't be nil"
@snippet = opts[:snippet]
@snippet_contains_encrypted_content = false
@have_snippet = !(opts[:snippet].nil? || opts[:snippet].empty?)
- @labels = (opts[:labels] || []).to_set_of_symbols
+ @labels = Set.new(opts[:labels] || [])
@dirty = false
@encrypted = false
@chunks = nil
@attachments = []
@@ -71,34 +71,34 @@
mid = header["message-id"] =~ /<(.+?)>/ ? $1 : header["message-id"]
sanitize_message_id mid
else
id = "sup-faked-" + Digest::MD5.hexdigest(raw_header)
from = header["from"]
- #Redwood::log "faking non-existent message-id for message from #{from}: #{id}"
+ #debug "faking non-existent message-id for message from #{from}: #{id}"
id
end
@from = Person.from_address(if header["from"]
header["from"]
else
name = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
- #Redwood::log "faking non-existent sender for message #@id: #{name}"
+ #debug "faking non-existent sender for message #@id: #{name}"
name
end)
@date = case(date = header["date"])
when Time
date
when String
begin
Time.parse date
rescue ArgumentError => e
- #Redwood::log "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
+ #debug "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
Time.now
end
else
- #Redwood::log "faking non-existent date header for #{@id}"
+ #debug "faking non-existent date header for #{@id}"
Time.now
end
@subj = header.member?("subject") ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
@to = Person.from_address_list header["to"]
@@ -125,20 +125,45 @@
@source_marked_read = header["status"] == "RO"
@list_subscribe = header["list-subscribe"]
@list_unsubscribe = header["list-unsubscribe"]
end
+ ## Expected index entry format:
+ ## :message_id, :subject => String
+ ## :date => Time
+ ## :refs, :replytos => Array of String
+ ## :from => Person
+ ## :to, :cc, :bcc => Array of Person
+ def load_from_index! entry
+ @id = entry[:message_id]
+ @from = entry[:from]
+ @date = entry[:date]
+ @subj = entry[:subject]
+ @to = entry[:to]
+ @cc = entry[:cc]
+ @bcc = entry[:bcc]
+ @refs = (@refs + entry[:refs]).uniq
+ @replytos = entry[:replytos]
+
+ @replyto = nil
+ @list_address = nil
+ @recipient_email = nil
+ @source_marked_read = false
+ @list_subscribe = nil
+ @list_unsubscribe = nil
+ end
+
def add_ref ref
@refs << ref
@dirty = true
end
def remove_ref ref
@dirty = true if @refs.delete ref
end
- def snippet; @snippet || (chunks && @snippet); end
+ attr_reader :snippet
def is_list_message?; !@list_address.nil?; end
def is_draft?; @source.is_a? DraftLoader; end
def draft_filename
raise "not a draft" unless is_draft?
@source.fn_for_offset @source_info
@@ -155,35 +180,40 @@
##
## an alternative would be to SHA1 or MD5 all message ids on a regular basis.
## don't tempt me.
def sanitize_message_id mid; mid.gsub(/(\s|[^\000-\177])+/, "")[0..254] end
- def save index
+ def save_state index
return unless @dirty
- index.sync_message self
+ index.update_message_state self
@dirty = false
true
end
def has_label? t; @labels.member? t; end
- def add_label t
- return if @labels.member? t
- @labels = (@labels + [t]).to_set_of_symbols
+ def add_label l
+ l = l.to_sym
+ return if @labels.member? l
+ @labels << l
@dirty = true
end
- def remove_label t
- return unless @labels.member? t
- @labels.delete t
+ def remove_label l
+ l = l.to_sym
+ return unless @labels.member? l
+ @labels.delete l
@dirty = true
end
def recipients
@to + @cc + @bcc
end
def labels= l
- @labels = l.to_set_of_symbols
+ raise ArgumentError, "not a set" unless l.is_a?(Set)
+ raise ArgumentError, "not a set of labels" unless l.all? { |ll| ll.is_a?(Symbol) }
+ return if @labels == l
+ @labels = l
@dirty = true
end
def chunks
load_from_source!
@@ -206,11 +236,11 @@
## actually, it's also the differentiation between to/cc/bcc,
## so i will keep this.
parse_header @source.load_header(@source_info)
message_to_chunks @source.load_message(@source_info)
rescue SourceError, SocketError => e
- Redwood::log "problem getting messages from #{@source}: #{e.message}"
+ warn "problem getting messages from #{@source}: #{e.message}"
## we need force_to_top here otherwise this window will cover
## up the error message one
@source.error ||= e
Redwood::report_broken_sources :force_to_top => true
[Chunk::Text.new(error_message(e.message).split("\n"))]
@@ -240,11 +270,11 @@
## wrap any source methods that might throw sourceerrors
def with_source_errors_handled
begin
yield
rescue SourceError => e
- Redwood::log "problem getting messages from #{@source}: #{e.message}"
+ warn "problem getting messages from #{@source}: #{e.message}"
@source.error ||= e
Redwood::report_broken_sources :force_to_top => true
error_message e.message
end
end
@@ -268,15 +298,27 @@
[
from && from.indexable_content,
to.map { |p| p.indexable_content },
cc.map { |p| p.indexable_content },
bcc.map { |p| p.indexable_content },
- chunks.select { |c| c.is_a? Chunk::Text }.map { |c| c.lines },
- Message.normalize_subj(subj),
+ indexable_chunks.map { |c| c.lines },
+ indexable_subject,
].flatten.compact.join " "
end
+ def indexable_body
+ indexable_chunks.map { |c| c.lines }.flatten.compact.join " "
+ end
+
+ def indexable_chunks
+ chunks.select { |c| c.is_a? Chunk::Text }
+ end
+
+ def indexable_subject
+ Message.normalize_subj(subj)
+ end
+
def quotable_body_lines
chunks.find_all { |c| c.quotable? }.map { |c| c.lines }.flatten
end
def quotable_header_lines
@@ -286,10 +328,16 @@
(@bcc.empty? ? [] : ["Bcc: " + @bcc.map { |p| p.full_address }.join(", ")]) +
["Date: #{@date.rfc822}",
"Subject: #{@subj}"]
end
+ def self.build_from_source source, source_info
+ m = Message.new :source => source, :source_info => source_info
+ m.load_from_source!
+ m
+ end
+
private
## 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
@@ -313,67 +361,71 @@
## mime-encoded message, but need only see the delicious end
## product.
def multipart_signed_to_chunks m
if m.body.size != 2
- Redwood::log "warning: multipart/signed with #{m.body.size} parts (expecting 2)"
+ warn "multipart/signed with #{m.body.size} parts (expecting 2)"
return
end
payload, signature = m.body
if signature.multipart?
- Redwood::log "warning: multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
+ warn "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
return
end
## this probably will never happen
- if payload.header.content_type == "application/pgp-signature"
- Redwood::log "warning: multipart/signed with payload content type #{payload.header.content_type}"
+ if payload.header.content_type && payload.header.content_type.downcase == "application/pgp-signature"
+ warn "multipart/signed with payload content type #{payload.header.content_type}"
return
end
- if signature.header.content_type != "application/pgp-signature"
+ if signature.header.content_type && signature.header.content_type.downcase != "application/pgp-signature"
## unknown signature type; just ignore.
- #Redwood::log "warning: multipart/signed with signature content type #{signature.header.content_type}"
+ #warn "multipart/signed with signature content type #{signature.header.content_type}"
return
end
[CryptoManager.verify(payload, signature), message_to_chunks(payload)].flatten.compact
end
def multipart_encrypted_to_chunks m
if m.body.size != 2
- Redwood::log "warning: multipart/encrypted with #{m.body.size} parts (expecting 2)"
+ warn "multipart/encrypted with #{m.body.size} parts (expecting 2)"
return
end
control, payload = m.body
if control.multipart?
- Redwood::log "warning: multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
+ warn "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
return
end
- if payload.header.content_type != "application/octet-stream"
- Redwood::log "warning: multipart/encrypted with payload content type #{payload.header.content_type}"
+ if payload.header.content_type && payload.header.content_type.downcase != "application/octet-stream"
+ warn "multipart/encrypted with payload content type #{payload.header.content_type}"
return
end
- if control.header.content_type != "application/pgp-encrypted"
- Redwood::log "warning: multipart/encrypted with control content type #{signature.header.content_type}"
+ if control.header.content_type && control.header.content_type.downcase != "application/pgp-encrypted"
+ warn "multipart/encrypted with control content type #{signature.header.content_type}"
return
end
- decryptedm, sig, notice = CryptoManager.decrypt payload
- children = message_to_chunks(decryptedm, true) if decryptedm
- [notice, sig, children].flatten.compact
+ notice, sig, decryptedm = CryptoManager.decrypt payload
+ if decryptedm # managed to decrypt
+ children = message_to_chunks(decryptedm, true)
+ [notice, sig].compact + children
+ else
+ [notice]
+ end
end
## takes a RMail::Message, breaks it into Chunk:: classes.
def message_to_chunks m, encrypted=false, sibling_types=[]
if m.multipart?
chunks =
- case m.header.content_type
+ case m.header.content_type.downcase
when "multipart/signed"
multipart_signed_to_chunks m
when "multipart/encrypted"
multipart_encrypted_to_chunks m
end
@@ -382,33 +434,61 @@
sibling_types = m.body.map { |p| p.header.content_type }
chunks = m.body.map { |p| message_to_chunks p, encrypted, sibling_types }.flatten.compact
end
chunks
- elsif m.header.content_type == "message/rfc822"
- payload = RMail::Parser.read(m.body)
- from = payload.header.from.first
- from_person = from ? Person.from_address(from.format) : nil
- [Chunk::EnclosedMessage.new(from_person, payload.to_s)] +
- message_to_chunks(payload, encrypted)
+ elsif m.header.content_type && m.header.content_type.downcase == "message/rfc822"
+ if m.body
+ payload = RMail::Parser.read(m.body)
+ from = payload.header.from.first ? payload.header.from.first.format : ""
+ to = payload.header.to.map { |p| p.format }.join(", ")
+ cc = payload.header.cc.map { |p| p.format }.join(", ")
+ subj = payload.header.subject
+ subj = subj ? Message.normalize_subj(payload.header.subject.gsub(/\s+/, " ").gsub(/\s+$/, "")) : subj
+ if Rfc2047.is_encoded? subj
+ subj = Rfc2047.decode_to $encoding, subj
+ end
+ msgdate = payload.header.date
+ from_person = from ? Person.from_address(from) : nil
+ to_people = to ? Person.from_address_list(to) : nil
+ cc_people = cc ? Person.from_address_list(cc) : nil
+ [Chunk::EnclosedMessage.new(from_person, to_people, cc_people, msgdate, subj)] + message_to_chunks(payload, encrypted)
+ else
+ debug "no body for message/rfc822 enclosure; skipping"
+ []
+ end
+ elsif m.header.content_type && m.header.content_type.downcase == "application/pgp" && m.body
+ ## apparently some versions of Thunderbird generate encryped email that
+ ## does not follow RFC3156, e.g. messages with X-Enigmail-Version: 0.95.0
+ ## they have no MIME multipart and just set the body content type to
+ ## application/pgp. this handles that.
+ ##
+ ## TODO: unduplicate code between here and multipart_encrypted_to_chunks
+ notice, sig, decryptedm = CryptoManager.decrypt m.body
+ if decryptedm # managed to decrypt
+ children = message_to_chunks decryptedm, true
+ [notice, sig].compact + children
+ else
+ [notice]
+ end
else
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="?(.*?[^\\])("|;|$)/
+ elsif m.header["Content-Type"] && m.header["Content-Type"] =~ /name="?(.*?[^\\])("|;|$)/i
$1
## haven't found one, but it's a non-text message. fake
## it.
##
## TODO: make this less lame.
- elsif m.header["Content-Type"] && m.header["Content-Type"] !~ /^text\/plain/
+ elsif m.header["Content-Type"] && m.header["Content-Type"] !~ /^text\/plain/i
extension =
case m.header["Content-Type"]
- when /text\/html/: "html"
- when /image\/(.*)/: $1
+ when /text\/html/ then "html"
+ when /image\/(.*)/ then $1
end
["sup-attachment-#{Time.now.to_i}-#{rand 10000}", extension].join(".")
end
@@ -417,10 +497,10 @@
# add this to the attachments list if its not a generated html
# attachment (should we allow images with generated names?).
# Lowercase the filename because searches are easier that way
@attachments.push filename.downcase unless filename =~ /^sup-attachment-/
add_label :attachment unless filename =~ /^sup-attachment-/
- content_type = m.header.content_type || "application/unknown" # sometimes RubyMail gives us nil
+ content_type = m.header.content_type.downcase || "application/unknown" # sometimes RubyMail gives us nil
[Chunk::Attachment.new(content_type, filename, m, sibling_types)]
## otherwise, it's body text
else
## if there's no charset, use the current encoding as the charset.