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.