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
- #parse_header(encoded_header) ⇒ Object
- #quotable_body_lines ⇒ Object
- #quotable_header_lines ⇒ Object
- #raw_header ⇒ Object
- #raw_message ⇒ Object
- #recipients ⇒ 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
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.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/sup/message.rb', line 50 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.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def @attachments end |
#bcc ⇒ Object (readonly)
Returns the value of attribute bcc.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def bcc @bcc end |
#cc ⇒ Object (readonly)
Returns the value of attribute cc.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def cc @cc end |
#date ⇒ Object (readonly)
Returns the value of attribute date.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def date @date end |
#from ⇒ Object (readonly)
Returns the value of attribute from.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def from @from end |
#id ⇒ Object (readonly)
Returns the value of attribute id.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def id @id end |
#labels ⇒ Object
Returns the value of attribute labels.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def labels @labels end |
#list_address ⇒ Object (readonly)
Returns the value of attribute list_address.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def list_address @list_address end |
#list_subscribe ⇒ Object (readonly)
Returns the value of attribute list_subscribe.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def list_subscribe @list_subscribe end |
#list_unsubscribe ⇒ Object (readonly)
Returns the value of attribute list_unsubscribe.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def list_unsubscribe @list_unsubscribe end |
#locations ⇒ Object
Returns the value of attribute locations.
46 47 48 |
# File 'lib/sup/message.rb', line 46 def locations @locations end |
#recipient_email ⇒ Object (readonly)
Returns the value of attribute recipient_email.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def recipient_email @recipient_email end |
#refs ⇒ Object (readonly)
Returns the value of attribute refs.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def refs @refs end |
#replyto ⇒ Object (readonly)
Returns the value of attribute replyto.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def replyto @replyto end |
#replytos ⇒ Object (readonly)
Returns the value of attribute replytos.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def replytos @replytos end |
#snippet ⇒ Object (readonly)
Returns the value of attribute snippet.
177 178 179 |
# File 'lib/sup/message.rb', line 177 def snippet @snippet end |
#subj ⇒ Object (readonly)
Returns the value of attribute subj.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def subj @subj end |
#to ⇒ Object (readonly)
Returns the value of attribute to.
40 41 42 |
# File 'lib/sup/message.rb', line 40 def to @to end |
Class Method Details
.build_from_source(source, source_info) ⇒ Object
326 327 328 329 330 |
# File 'lib/sup/message.rb', line 326 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
19 |
# File 'lib/sup/message.rb', line 19 def normalize_subj s; s.gsub(RE_PATTERN, ""); end |
.reify_subj(s) ⇒ Object
21 |
# File 'lib/sup/message.rb', line 21 def reify_subj s; subj_is_reply?(s) ? s : "Re: " + s; end |
.subj_is_reply?(s) ⇒ Boolean
20 |
# File 'lib/sup/message.rb', line 20 def subj_is_reply? s; s =~ RE_PATTERN; end |
Instance Method Details
#add_label(l) ⇒ Object
203 204 205 206 207 208 |
# File 'lib/sup/message.rb', line 203 def add_label l l = l.to_sym return if @labels.member? l @labels << l @dirty = true end |
#add_ref(ref) ⇒ Object
168 169 170 171 |
# File 'lib/sup/message.rb', line 168 def add_ref ref @refs << ref @dirty = true end |
#chunks ⇒ Object
228 229 230 231 |
# File 'lib/sup/message.rb', line 228 def chunks load_from_source! @chunks end |
#clear_dirty ⇒ Object
198 199 200 |
# File 'lib/sup/message.rb', line 198 def clear_dirty @dirty = false end |
#decode_header_field(v) ⇒ Object
68 69 70 71 72 73 |
# File 'lib/sup/message.rb', line 68 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 |
#draft_filename ⇒ Object
180 181 182 183 |
# File 'lib/sup/message.rb', line 180 def draft_filename raise "not a draft" unless is_draft? source.fn_for_offset source_info end |
#each_raw_message_line(&b) ⇒ Object
284 285 286 |
# File 'lib/sup/message.rb', line 284 def &b location. &b end |
#error_message ⇒ Object
266 267 268 269 270 271 272 273 274 |
# File 'lib/sup/message.rb', line 266 def <<EOS #@snippet... *********************************************************************** An error occurred while loading this message. *********************************************************************** EOS end |
#has_label?(t) ⇒ Boolean
202 |
# File 'lib/sup/message.rb', line 202 def has_label? t; @labels.member? t; end |
#indexable_body ⇒ Object
301 302 303 |
# File 'lib/sup/message.rb', line 301 def indexable_body indexable_chunks.map { |c| c.lines }.flatten.compact.join " " end |
#indexable_chunks ⇒ Object
305 306 307 |
# File 'lib/sup/message.rb', line 305 def indexable_chunks chunks.select { |c| c.is_a? Chunk::Text } end |
#indexable_content ⇒ Object
returns all the content from a message that will be indexed
289 290 291 292 293 294 295 296 297 298 299 |
# File 'lib/sup/message.rb', line 289 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 |
#indexable_subject ⇒ Object
309 310 311 |
# File 'lib/sup/message.rb', line 309 def indexable_subject Message.normalize_subj(subj) end |
#is_draft? ⇒ Boolean
179 |
# File 'lib/sup/message.rb', line 179 def is_draft?; @labels.member? :draft; end |
#is_list_message? ⇒ Boolean
178 |
# File 'lib/sup/message.rb', line 178 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
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/sup/message.rb', line 149 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.
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/sup/message.rb', line 246 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 "problem reading message #{id}" [Chunk::Text.new(.split("\n"))] end end |
#location ⇒ Object
233 234 235 |
# File 'lib/sup/message.rb', line 233 def location @locations.find { |x| x.valid? } || raise(OutOfSyncSourceError.new) end |
#parse_header(encoded_header) ⇒ Object
75 76 77 78 79 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 |
# File 'lib/sup/message.rb', line 75 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 => 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| 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["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
313 314 315 |
# File 'lib/sup/message.rb', line 313 def quotable_body_lines chunks.find_all { |c| c.quotable? }.map { |c| c.lines }.flatten end |
#quotable_header_lines ⇒ Object
317 318 319 320 321 322 323 324 |
# File 'lib/sup/message.rb', line 317 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
276 277 278 |
# File 'lib/sup/message.rb', line 276 def raw_header location.raw_header end |
#raw_message ⇒ Object
280 281 282 |
# File 'lib/sup/message.rb', line 280 def location. end |
#recipients ⇒ Object
216 217 218 |
# File 'lib/sup/message.rb', line 216 def recipients @to + @cc + @bcc end |
#remove_label(l) ⇒ Object
209 210 211 212 213 214 |
# File 'lib/sup/message.rb', line 209 def remove_label l l = l.to_sym return unless @labels.member? l @labels.delete l @dirty = true end |
#remove_ref(ref) ⇒ Object
173 174 175 |
# File 'lib/sup/message.rb', line 173 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.
196 |
# File 'lib/sup/message.rb', line 196 def mid; mid.gsub(/(\s|[^\000-\177])+/, "")[0..254] end |
#source ⇒ Object
237 238 239 |
# File 'lib/sup/message.rb', line 237 def source location.source end |
#source_info ⇒ Object
241 242 243 |
# File 'lib/sup/message.rb', line 241 def source_info location.info end |