a Message is what's threaded.
it is also where the parsing for quotes and signatures is done, but that should be moved out to a separate class at some point (because i would like, for example, to be able to add in a ruby-talk specific module that would detect and link to /ruby-talk:d+/ sequences in the text of an email. (how sweet would that be?)
this class catches all source exceptions. if the underlying source throws an error, it is caught and handled.
# File lib/sup/message.rb, line 338 def self.build_from_source source, source_info m = Message.new :source => source, :source_info => source_info m.load_from_source! m end
if you specify a :header, will use values from that. otherwise, will try and load the header from the source.
# File lib/sup/message.rb, line 44 def initialize opts @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 = Set.new(opts[:labels] || []) @dirty = false @encrypted = false @chunks = nil @attachments = [] ## we need to initialize this. see comments in parse_header as to ## why. @refs = [] #parse_header(opts[:header] || @source.load_header(@source_info)) end
# File lib/sup/message.rb, line 22 def normalize_subj s; s.gsub(RE_PATTERN, ""); end
# File lib/sup/message.rb, line 197 def add_label l l = l.to_sym return if @labels.member? l @labels << l @dirty = true end
# File lib/sup/message.rb, line 162 def add_ref ref @refs << ref @dirty = true end
# File lib/sup/message.rb, line 222 def chunks load_from_source! @chunks end
# File lib/sup/message.rb, line 192 def clear_dirty @dirty = false end
# File lib/sup/message.rb, line 63 def decode_header_field v return unless v return v unless v.is_a? String return unless v.size < MAX_HEADER_VALUE_SIZE # avoid regex blowup on spam Rfc2047.decode_to $encoding, Iconv.easy_decode($encoding, 'ASCII', v) end
# File lib/sup/message.rb, line 174 def draft_filename raise "not a draft" unless is_draft? @source.fn_for_offset @source_info end
much faster than raw_message
# File lib/sup/message.rb, line 296 def each_raw_message_line &b with_source_errors_handled { @source.each_raw_message_line(@source_info, &b) } end
# File lib/sup/message.rb, line 256 def error_message msg #@snippet...*********************************************************************** An error occurred while loading this message. It is possible that the source has changed, or (in the case of remote sources) is down. You can check the log for errors, though hopefully an error window should have popped up at some point. The message location was: #@source##@source_info***********************************************************************The error message was: #{msg} end
# File lib/sup/message.rb, line 196 def has_label? t; @labels.member? t; end
# File lib/sup/message.rb, line 313 def indexable_body indexable_chunks.map { |c| c.lines }.flatten.compact.join " " end
# File lib/sup/message.rb, line 317 def indexable_chunks chunks.select { |c| c.is_a? Chunk::Text } end
returns all the content from a message that will be indexed
# File lib/sup/message.rb, line 301 def indexable_content load_from_source! [ from && from.indexable_content, to.map { |p| p.indexable_content }, cc.map { |p| p.indexable_content }, bcc.map { |p| p.indexable_content }, indexable_chunks.map { |c| c.lines }, indexable_subject, ].flatten.compact.join " " end
# File lib/sup/message.rb, line 321 def indexable_subject Message.normalize_subj(subj) end
# File lib/sup/message.rb, line 173 def is_draft?; @source.is_a? DraftLoader; end
# File lib/sup/message.rb, line 172 def is_list_message?; !@list_address.nil?; end
# File lib/sup/message.rb, line 214 def labels= l 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
Expected index entry format: :message_id, :subject => String :date => Time :refs, :replytos => Array of String :from => Person :to, :cc, :bcc => Array of Person
# File lib/sup/message.rb, line 143 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
this is called when the message body needs to actually be loaded.
# File lib/sup/message.rb, line 228 def load_from_source! @chunks ||= if @source.respond_to?(:has_errors?) && @source.has_errors? [Chunk::Text.new(error_message(@source.error.message).split("\n"))] else begin ## we need to re-read the header because it contains information ## that we don't store in the index. actually i think it's just ## the mailing list address (if any), so this is kinda overkill. ## i could just store that in the index, but i think there might ## be other things like that in the future, and i'd rather not ## bloat the index. ## actually, it's also the differentiation between to/cc/bcc, ## so i will keep this. rmsg = @source.load_message(@source_info) parse_header rmsg.header message_to_chunks rmsg rescue SourceError, SocketError => e 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"))] end end end
# File lib/sup/message.rb, line 70 def parse_header encoded_header header = SavingHash.new { |k| decode_header_field encoded_header[k] } @id = if header["message-id"] mid = header["message-id"] =~ /<(.+?)>/ ? $1 : header["message-id"] sanitize_message_id mid else id = "sup-faked-" + Digest::MD5.hexdigest(raw_header) from = header["from"] #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>" #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 #debug "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})" Time.now end else #debug "faking non-existent date header for #{@id}" Time.now end @subj = header["subject"] ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT @to = Person.from_address_list header["to"] @cc = Person.from_address_list header["cc"] @bcc = Person.from_address_list header["bcc"] ## before loading our full header from the source, we can actually ## have some extra refs set by the UI. (this happens when the user ## joins threads manually). so we will merge the current refs values ## in here. refs = (header["references"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first } @refs = (@refs + refs).uniq @replytos = (header["in-reply-to"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first } @replyto = Person.from_address header["reply-to"] @list_address = if header["list-post"] address = if header["list-post"] =~ /mailto:(.*?)[>\s$]/ $1 elsif header["list-post"] =~ /@/ header["list-post"] # just try the whole fucking thing end address && Person.from_address(address) elsif header["x-mailing-list"] Person.from_address header["x-mailing-list"] end @recipient_email = header["envelope-to"] || header["x-original-to"] || header["delivered-to"] @source_marked_read = header["status"] == "RO" @list_subscribe = header["list-subscribe"] @list_unsubscribe = header["list-unsubscribe"] end
# File lib/sup/message.rb, line 325 def quotable_body_lines chunks.find_all { |c| c.quotable? }.map { |c| c.lines }.flatten end
# File lib/sup/message.rb, line 329 def quotable_header_lines ["From: #{@from.full_address}"] + (@to.empty? ? [] : ["To: " + @to.map { |p| p.full_address }.join(", ")]) + (@cc.empty? ? [] : ["Cc: " + @cc.map { |p| p.full_address }.join(", ")]) + (@bcc.empty? ? [] : ["Bcc: " + @bcc.map { |p| p.full_address }.join(", ")]) + ["Date: #{@date.rfc822}", "Subject: #{@subj}"] end
# File lib/sup/message.rb, line 287 def raw_header with_source_errors_handled { @source.raw_header @source_info } end
# File lib/sup/message.rb, line 291 def raw_message with_source_errors_handled { @source.raw_message @source_info } end
# File lib/sup/message.rb, line 210 def recipients @to + @cc + @bcc end
# File lib/sup/message.rb, line 203 def remove_label l l = l.to_sym return unless @labels.member? l @labels.delete l @dirty = true end
# File lib/sup/message.rb, line 167 def remove_ref ref @dirty = true if @refs.delete ref end
sanitize message ids by removing spaces and non-ascii characters. also, truncate to 255 characters. all these steps are necessary to make ferret happy. of course, we probably fuck up a couple valid message ids as well. as long as we're consistent, this should be fine, though.
also, mostly the message ids that are changed by this belong to spam email.
an alternative would be to SHA1 or MD5 all message ids on a regular basis. don't tempt me.
# File lib/sup/message.rb, line 190 def sanitize_message_id mid; mid.gsub(/(\s|[^\0000-\1177])+/, "")[0..254] end
wrap any source methods that might throw sourceerrors
# File lib/sup/message.rb, line 276 def with_source_errors_handled begin yield rescue SourceError => e 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
Generated with the Darkfish Rdoc Generator 2.