Class: Redwood::Message
Overview
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?)
Constant Summary collapse
- SNIPPET_LEN =
80
- RE_PATTERN =
/^((re|re[\[\(]\d[\]\)]):\s*)+/i
- QUOTE_PATTERN =
/^\s{0,4}[>|\}]/
- BLOCK_QUOTE_PATTERN =
/^-----\s*Original Message\s*----+$/
- SIG_PATTERN =
/(^(- )*-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)|(^\s*--~--~-)|(^\s*--\+\+\*\*==)/
- GPG_SIGNED_START =
"-----BEGIN PGP SIGNED MESSAGE-----"
- GPG_SIGNED_END =
"-----END PGP SIGNED MESSAGE-----"
- GPG_START =
"-----BEGIN PGP MESSAGE-----"
- GPG_END =
"-----END PGP MESSAGE-----"
- GPG_SIG_START =
"-----BEGIN PGP SIGNATURE-----"
- GPG_SIG_END =
"-----END PGP SIGNATURE-----"
- MAX_SIG_DISTANCE =
lines from the end
15
- DEFAULT_SUBJECT =
""
- DEFAULT_SENDER =
"(missing sender)"
- MAX_HEADER_VALUE_SIZE =
4096
Instance Attribute Summary collapse
-
#attachments ⇒ Object
readonly
Returns the value of attribute attachments.
-
#bcc ⇒ Object
readonly
Returns the value of attribute bcc.
-
#cc ⇒ Object
readonly
Returns the value of attribute cc.
-
#date ⇒ Object
readonly
Returns the value of attribute date.
-
#from ⇒ Object
readonly
Returns the value of attribute from.
-
#id ⇒ Object
readonly
Returns the value of attribute id.
-
#labels ⇒ Object
Returns the value of attribute labels.
-
#list_address ⇒ Object
readonly
Returns the value of attribute list_address.
-
#list_subscribe ⇒ Object
readonly
Returns the value of attribute list_subscribe.
-
#list_unsubscribe ⇒ Object
readonly
Returns the value of attribute list_unsubscribe.
-
#locations ⇒ Object
Returns the value of attribute locations.
-
#recipient_email ⇒ Object
readonly
Returns the value of attribute recipient_email.
-
#refs ⇒ Object
readonly
Returns the value of attribute refs.
-
#replyto ⇒ Object
readonly
Returns the value of attribute replyto.
-
#replytos ⇒ Object
readonly
Returns the value of attribute replytos.
-
#snippet ⇒ Object
readonly
Returns the value of attribute snippet.
-
#subj ⇒ Object
readonly
Returns the value of attribute subj.
-
#to ⇒ Object
readonly
Returns the value of attribute to.
Class Method Summary collapse
- .build_from_source(source, source_info) ⇒ Object
- .normalize_subj(s) ⇒ Object
- .reify_subj(s) ⇒ Object
- .subj_is_reply?(s) ⇒ Boolean
Instance Method Summary collapse
- #add_label(l) ⇒ Object
- #add_ref(ref) ⇒ Object
- #chunks ⇒ Object
- #clear_dirty ⇒ Object
- #decode_header_field(v) ⇒ Object
- #draft_filename ⇒ Object
- #each_raw_message_line(&b) ⇒ Object
- #error_message ⇒ Object
- #has_label?(t) ⇒ Boolean
- #indexable_body ⇒ Object
- #indexable_chunks ⇒ Object
-
#indexable_content ⇒ Object
returns all the content from a message that will be indexed.
- #indexable_subject ⇒ Object
-
#initialize(opts) ⇒ Message
constructor
if you specify a :header, will use values from that.
- #is_draft? ⇒ Boolean
- #is_list_message? ⇒ Boolean
-
#load_from_index!(entry) ⇒ Object
Expected index entry format: :message_id, :subject => String :date => Time :refs, :replytos => Array of String :from => Person :to, :cc, :bcc => Array of Person.
-
#load_from_source! ⇒ Object
this is called when the message body needs to actually be loaded.
- #location ⇒ Object
- #merge_labels_from_locations(merge_labels) ⇒ Object
- #parse_header(encoded_header) ⇒ Object
- #quotable_body_lines ⇒ Object
- #quotable_header_lines ⇒ Object
- #raw_header ⇒ Object
- #raw_message ⇒ Object
- #recipients ⇒ Object
- #reload_from_source! ⇒ Object
- #remove_label(l) ⇒ Object
- #remove_ref(ref) ⇒ Object
-
#sanitize_message_id(mid) ⇒ Object
sanitize message ids by removing spaces and non-ascii characters.
- #source ⇒ Object
- #source_info ⇒ Object
- #sync_back ⇒ Object
Constructor Details
#initialize(opts) ⇒ Message
if you specify a :header, will use values from that. otherwise, will try and load the header from the source.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/sup/message.rb', line 53 def initialize opts @locations = opts[:locations] or raise ArgumentError, "locations 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 |
Instance Attribute Details
#attachments ⇒ Object (readonly)
Returns the value of attribute attachments.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def @attachments end |
#bcc ⇒ Object (readonly)
Returns the value of attribute bcc.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def bcc @bcc end |
#cc ⇒ Object (readonly)
Returns the value of attribute cc.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def cc @cc end |
#date ⇒ Object (readonly)
Returns the value of attribute date.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def date @date end |
#from ⇒ Object (readonly)
Returns the value of attribute from.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def from @from end |
#id ⇒ Object (readonly)
Returns the value of attribute id.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def id @id end |
#labels ⇒ Object
Returns the value of attribute labels.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def labels @labels end |
#list_address ⇒ Object (readonly)
Returns the value of attribute list_address.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def list_address @list_address end |
#list_subscribe ⇒ Object (readonly)
Returns the value of attribute list_subscribe.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def list_subscribe @list_subscribe end |
#list_unsubscribe ⇒ Object (readonly)
Returns the value of attribute list_unsubscribe.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def list_unsubscribe @list_unsubscribe end |
#locations ⇒ Object
Returns the value of attribute locations.
49 50 51 |
# File 'lib/sup/message.rb', line 49 def locations @locations end |
#recipient_email ⇒ Object (readonly)
Returns the value of attribute recipient_email.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def recipient_email @recipient_email end |
#refs ⇒ Object (readonly)
Returns the value of attribute refs.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def refs @refs end |
#replyto ⇒ Object (readonly)
Returns the value of attribute replyto.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def replyto @replyto end |
#replytos ⇒ Object (readonly)
Returns the value of attribute replytos.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def replytos @replytos end |
#snippet ⇒ Object (readonly)
Returns the value of attribute snippet.
189 190 191 |
# File 'lib/sup/message.rb', line 189 def snippet @snippet end |
#subj ⇒ Object (readonly)
Returns the value of attribute subj.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def subj @subj end |
#to ⇒ Object (readonly)
Returns the value of attribute to.
43 44 45 |
# File 'lib/sup/message.rb', line 43 def to @to end |
Class Method Details
.build_from_source(source, source_info) ⇒ Object
380 381 382 383 384 |
# File 'lib/sup/message.rb', line 380 def self.build_from_source source, source_info m = Message.new :locations => [Location.new(source, source_info)] m.load_from_source! m end |
.normalize_subj(s) ⇒ Object
22 |
# File 'lib/sup/message.rb', line 22 def normalize_subj s; s.gsub(RE_PATTERN, ""); end |
.reify_subj(s) ⇒ Object
24 |
# File 'lib/sup/message.rb', line 24 def reify_subj s; subj_is_reply?(s) ? s : "Re: " + s; end |
.subj_is_reply?(s) ⇒ Boolean
23 |
# File 'lib/sup/message.rb', line 23 def subj_is_reply? s; s =~ RE_PATTERN; end |
Instance Method Details
#add_label(l) ⇒ Object
215 216 217 218 219 220 |
# File 'lib/sup/message.rb', line 215 def add_label l l = l.to_sym return if @labels.member? l @labels << l @dirty = true end |
#add_ref(ref) ⇒ Object
180 181 182 183 |
# File 'lib/sup/message.rb', line 180 def add_ref ref @refs << ref @dirty = true end |
#chunks ⇒ Object
240 241 242 243 |
# File 'lib/sup/message.rb', line 240 def chunks load_from_source! @chunks end |
#clear_dirty ⇒ Object
210 211 212 |
# File 'lib/sup/message.rb', line 210 def clear_dirty @dirty = false end |
#decode_header_field(v) ⇒ Object
71 72 73 74 75 76 77 78 |
# File 'lib/sup/message.rb', line 71 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 ## Header values should be either 7-bit with RFC2047-encoded words ## or UTF-8 as per RFC6532. Replace any invalid high bytes with U+FFFD. Rfc2047.decode_to $encoding, v.dup.force_encoding(Encoding::UTF_8).scrub end |
#draft_filename ⇒ Object
192 193 194 195 |
# File 'lib/sup/message.rb', line 192 def draft_filename raise "not a draft" unless is_draft? source.fn_for_offset source_info end |
#each_raw_message_line(&b) ⇒ Object
312 313 314 |
# File 'lib/sup/message.rb', line 312 def &b location.(&b) end |
#error_message ⇒ Object
294 295 296 297 298 299 300 301 302 |
# File 'lib/sup/message.rb', line 294 def <<EOS #@snippet... *********************************************************************** An error occurred while loading this message. *********************************************************************** EOS end |
#has_label?(t) ⇒ Boolean
214 |
# File 'lib/sup/message.rb', line 214 def has_label? t; @labels.member? t; end |
#indexable_body ⇒ Object
355 356 357 |
# File 'lib/sup/message.rb', line 355 def indexable_body indexable_chunks.map { |c| c.lines }.flatten.compact.map { |l| l.fix_encoding! }.join " " end |
#indexable_chunks ⇒ Object
359 360 361 |
# File 'lib/sup/message.rb', line 359 def indexable_chunks chunks.select { |c| c.indexable? } || [] end |
#indexable_content ⇒ Object
returns all the content from a message that will be indexed
343 344 345 346 347 348 349 350 351 352 353 |
# File 'lib/sup/message.rb', line 343 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.map { |l| l.fix_encoding! } }, indexable_subject, ].flatten.compact.join " " end |
#indexable_subject ⇒ Object
363 364 365 |
# File 'lib/sup/message.rb', line 363 def indexable_subject Message.normalize_subj(subj) end |
#is_draft? ⇒ Boolean
191 |
# File 'lib/sup/message.rb', line 191 def is_draft?; @labels.member? :draft; end |
#is_list_message? ⇒ Boolean
190 |
# File 'lib/sup/message.rb', line 190 def ; !@list_address.nil?; end |
#load_from_index!(entry) ⇒ Object
Expected index entry format: :message_id, :subject => String :date => Time :refs, :replytos => Array of String :from => Person :to, :cc, :bcc => Array of Person
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/sup/message.rb', line 161 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 |
#load_from_source! ⇒ Object
this is called when the message body needs to actually be loaded.
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/sup/message.rb', line 258 def load_from_source! @chunks ||= 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 = location. parse_header rmsg.header rmsg rescue SourceError, SocketError, RMail::EncodingUnsupportedError => e warn_with_location "problem reading message #{id}" debug "could not load message, exception: #{e.inspect}" [Chunk::Text.new(.split("\n"))] rescue Exception => e warn_with_location "problem reading message #{id}" debug "could not load message: #{location.inspect}, exception: #{e.inspect}" raise e end end |
#location ⇒ Object
245 246 247 |
# File 'lib/sup/message.rb', line 245 def location @locations.find { |x| x.valid? } || raise(OutOfSyncSourceError.new) end |
#merge_labels_from_locations(merge_labels) ⇒ Object
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 |
# File 'lib/sup/message.rb', line 322 def merge_labels_from_locations merge_labels ## Get all labels from all locations location_labels = Set.new([]) @locations.each do |l| if l.valid? location_labels = location_labels.union(l.labels?) end end ## Add to the message labels the intersection between all location ## labels and those we want to merge location_labels = location_labels.intersection(merge_labels.to_set) if not location_labels.empty? @labels = @labels.union(location_labels) @dirty = true end end |
#parse_header(encoded_header) ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/sup/message.rb', line 80 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"] @id = mid end if (not @id.include? '@') || @id.length < 6 @id = "sup-faked-" + Digest::MD5.hexdigest(raw_header) #from = header["from"] #debug "faking non-existent message-id for message from #{from}: #{id}" end @from = Person.from_address(if header["from"] header["from"] else name = "Sup Auto-generated Fake Sender <[email protected]>" #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 #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"] subj = subj ? subj.fix_encoding! : nil @subj = subj ? subj.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| x.first } @refs = (@refs + refs).uniq @replytos = (header["in-reply-to"] || "").scan(/<(.+?)>/).map { |x| 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["mailing-list"] address = if header["mailing-list"] =~ /list (.*?);/ $1 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 |
#quotable_body_lines ⇒ Object
367 368 369 |
# File 'lib/sup/message.rb', line 367 def quotable_body_lines chunks.find_all { |c| c.quotable? }.map { |c| c.lines }.flatten end |
#quotable_header_lines ⇒ Object
371 372 373 374 375 376 377 378 |
# File 'lib/sup/message.rb', line 371 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 |
#raw_header ⇒ Object
304 305 306 |
# File 'lib/sup/message.rb', line 304 def raw_header location.raw_header end |
#raw_message ⇒ Object
308 309 310 |
# File 'lib/sup/message.rb', line 308 def location. end |
#recipients ⇒ Object
228 229 230 |
# File 'lib/sup/message.rb', line 228 def recipients @to + @cc + @bcc end |
#reload_from_source! ⇒ Object
288 289 290 291 |
# File 'lib/sup/message.rb', line 288 def reload_from_source! @chunks = nil load_from_source! end |
#remove_label(l) ⇒ Object
221 222 223 224 225 226 |
# File 'lib/sup/message.rb', line 221 def remove_label l l = l.to_sym return unless @labels.member? l @labels.delete l @dirty = true end |
#remove_ref(ref) ⇒ Object
185 186 187 |
# File 'lib/sup/message.rb', line 185 def remove_ref ref @dirty = true if @refs.delete ref end |
#sanitize_message_id(mid) ⇒ Object
sanitize message ids by removing spaces and non-ascii characters. also, truncate to 255 characters. all these steps are necessary to make the index 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.
208 |
# File 'lib/sup/message.rb', line 208 def mid; mid.gsub(/(\s|[^\000-\177])+/, "")[0..254] end |
#source ⇒ Object
249 250 251 |
# File 'lib/sup/message.rb', line 249 def source location.source end |
#source_info ⇒ Object
253 254 255 |
# File 'lib/sup/message.rb', line 253 def source_info location.info end |
#sync_back ⇒ Object
316 317 318 319 320 |
# File 'lib/sup/message.rb', line 316 def sync_back @locations.map { |l| l.sync_back @labels, self }.any? do UpdateManager.relay self, :updated, self end end |