Class: Mailscope::Message
- Inherits:
-
Object
- Object
- Mailscope::Message
- Defined in:
- lib/mailscope/message.rb
Overview
A captured message. Reads from the metadata index for anything the list
view needs, and lazily parses the raw .eml for bodies, headers and
attachments, so rendering the sidebar never touches the full message.
Defined Under Namespace
Classes: Address, Attachment
Constant Summary collapse
- METADATA_VERSION =
1- PREVIEW_LENGTH =
180
Instance Attribute Summary collapse
-
#id ⇒ Object
readonly
Returns the value of attribute id.
-
#metadata ⇒ Object
readonly
Returns the value of attribute metadata.
-
#store ⇒ Object
readonly
Returns the value of attribute store.
Class Method Summary collapse
- .address_list(mail, field) ⇒ Object
- .attachments_for(mail) ⇒ Object
-
.mailer_for(mail) ⇒ Object
ActionMailer sets
delivery_handlerto the mailer class; the header is an escape hatch for anything delivering mail by hand. -
.metadata_for(mail, id:, size:) ⇒ Object
Builds the metadata index for a freshly delivered mail.
- .parts_for(mail) ⇒ Object
-
.preview_for(mail) ⇒ Object
Short plain-text snippet for the message list.
- .preview_source(mail) ⇒ Object
- .sanitize_filename(name) ⇒ Object
- .squeeze_preview(body) ⇒ Object
Instance Method Summary collapse
- #attachment(attachment_id) ⇒ Object
- #attachment_body(attachment_id) ⇒ Object
- #attachments ⇒ Object
- #bcc ⇒ Object
- #cc ⇒ Object
- #default_part ⇒ Object
- #from ⇒ Object
- #header_pairs ⇒ Object
- #html? ⇒ Boolean
- #html_body ⇒ Object
-
#initialize(metadata, store:) ⇒ Message
constructor
A new instance of Message.
- #mail ⇒ Object
-
#mailbox_keys ⇒ Object
Every mailbox this message landed in, used to build the mailbox rail.
- #mailer ⇒ Object
- #message_id ⇒ Object
- #parts ⇒ Object
- #preview ⇒ Object
-
#raw ⇒ Object
--- lazy data (parses the raw .eml) ------------------------------------.
- #recipients ⇒ Object
- #reply_to ⇒ Object
- #sent_at ⇒ Object
- #size ⇒ Object
-
#subject ⇒ Object
--- indexed data (cheap: comes straight from metadata.json) ------------.
- #text? ⇒ Boolean
- #text_body ⇒ Object
- #to ⇒ Object
- #to_h ⇒ Object
- #to_param ⇒ Object
- #visible_attachments ⇒ Object
Constructor Details
#initialize(metadata, store:) ⇒ Message
Returns a new instance of Message.
37 38 39 40 41 |
# File 'lib/mailscope/message.rb', line 37 def initialize(, store:) @metadata = @id = ['id'] @store = store end |
Instance Attribute Details
#id ⇒ Object (readonly)
Returns the value of attribute id.
35 36 37 |
# File 'lib/mailscope/message.rb', line 35 def id @id end |
#metadata ⇒ Object (readonly)
Returns the value of attribute metadata.
35 36 37 |
# File 'lib/mailscope/message.rb', line 35 def @metadata end |
#store ⇒ Object (readonly)
Returns the value of attribute store.
35 36 37 |
# File 'lib/mailscope/message.rb', line 35 def store @store end |
Class Method Details
.address_list(mail, field) ⇒ Object
191 192 193 194 195 196 197 198 199 |
# File 'lib/mailscope/message.rb', line 191 def address_list(mail, field) field_value = mail[field] addrs = field_value.respond_to?(:addrs) ? field_value.addrs : nil return [] unless addrs addrs.map { |a| { 'name' => a.display_name, 'address' => a.address } } rescue StandardError Array(mail.send(field)).map { |a| { 'name' => nil, 'address' => a } } end |
.attachments_for(mail) ⇒ Object
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/mailscope/message.rb', line 208 def (mail) mail..each_with_index.map do |, index| { 'id' => format('%02d', index), 'filename' => sanitize_filename(.filename.to_s), 'content_type' => .mime_type, 'size' => .body.decoded.bytesize, 'cid' => .cid, 'inline' => .inline? } rescue StandardError { 'id' => format('%02d', index), 'filename' => "attachment-#{index}", 'content_type' => 'application/octet-stream', 'size' => 0, 'cid' => nil, 'inline' => false } end end |
.mailer_for(mail) ⇒ Object
ActionMailer sets delivery_handler to the mailer class; the header is
an escape hatch for anything delivering mail by hand.
257 258 259 260 261 262 263 264 265 |
# File 'lib/mailscope/message.rb', line 257 def mailer_for(mail) header = mail['X-Mailscope-Mailer'] return header.to_s if header handler = mail.delivery_handler if mail.respond_to?(:delivery_handler) handler.respond_to?(:name) ? handler.name : nil rescue StandardError nil end |
.metadata_for(mail, id:, size:) ⇒ Object
Builds the metadata index for a freshly delivered mail. This is the only place that knows the on-disk contract, so bumping METADATA_VERSION only needs a change here plus a reader fallback.
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/mailscope/message.rb', line 171 def (mail, id:, size:) { 'version' => METADATA_VERSION, 'id' => id, 'sent_at' => Time.now.utc.iso8601(3), 'subject' => mail.subject.to_s, 'from' => address_list(mail, :from), 'to' => address_list(mail, :to), 'cc' => address_list(mail, :cc), 'bcc' => address_list(mail, :bcc), 'reply_to' => address_list(mail, :reply_to), 'message_id' => mail., 'mailer' => mailer_for(mail), 'parts' => parts_for(mail), 'attachments' => (mail), 'preview' => preview_for(mail), 'size' => size } end |
.parts_for(mail) ⇒ Object
201 202 203 204 205 206 |
# File 'lib/mailscope/message.rb', line 201 def parts_for(mail) parts = [] parts << 'html' if mail.html_part || mail.mime_type == 'text/html' parts << 'text' if mail.text_part || mail.mime_type == 'text/plain' || mail.mime_type.nil? parts end |
.preview_for(mail) ⇒ Object
Short plain-text snippet for the message list.
225 226 227 228 229 |
# File 'lib/mailscope/message.rb', line 225 def preview_for(mail) squeeze_preview(preview_source(mail)) rescue StandardError '' end |
.preview_source(mail) ⇒ Object
231 232 233 234 235 236 237 238 239 |
# File 'lib/mailscope/message.rb', line 231 def preview_source(mail) text = mail.text_part || (mail.mime_type == 'text/plain' ? mail : nil) return text.decoded if text html = mail.html_part || (mail.mime_type == 'text/html' ? mail : nil) return '' unless html html.decoded.gsub(%r{<(script|style)[^>]*>.*?</\1>}mi, ' ').gsub(/<[^>]+>/, ' ') end |
.sanitize_filename(name) ⇒ Object
246 247 248 249 250 251 252 253 |
# File 'lib/mailscope/message.rb', line 246 def sanitize_filename(name) cleaned = name.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: '_').strip # Drop any directory part first: transliterating separators afterwards # would leave "../../etc/passwd" as a single (harmless but ugly) name. cleaned = File.basename(cleaned.tr('\\', '/')) cleaned = cleaned.tr("\u{202E}%$|:;\t\r\n", '-').delete_prefix('.') cleaned.empty? ? 'attachment' : cleaned end |
.squeeze_preview(body) ⇒ Object
241 242 243 244 |
# File 'lib/mailscope/message.rb', line 241 def squeeze_preview(body) body.to_s.dup.force_encoding(Encoding::UTF_8).scrub .gsub(/ ?/, ' ').squeeze(" \t\n").strip[0, PREVIEW_LENGTH].to_s end |
Instance Method Details
#attachment(attachment_id) ⇒ Object
92 |
# File 'lib/mailscope/message.rb', line 92 def () = .find { |a| a.id == } |
#attachment_body(attachment_id) ⇒ Object
113 114 115 116 |
# File 'lib/mailscope/message.rb', line 113 def () found = mail..each_with_index.find { |_, i| (i) == } found && found[0].body.decoded end |
#attachments ⇒ Object
81 82 83 84 85 86 87 88 |
# File 'lib/mailscope/message.rb', line 81 def @attachments ||= Array(['attachments']).map do |raw| Attachment.new( id: raw['id'], filename: raw['filename'], content_type: raw['content_type'], size: raw['size'], cid: raw['cid'], inline: raw['inline'] ) end end |
#bcc ⇒ Object
59 |
# File 'lib/mailscope/message.rb', line 59 def bcc = addresses('bcc') |
#cc ⇒ Object
58 |
# File 'lib/mailscope/message.rb', line 58 def cc = addresses('cc') |
#default_part ⇒ Object
79 |
# File 'lib/mailscope/message.rb', line 79 def default_part = html? ? 'html' : 'text' |
#from ⇒ Object
56 |
# File 'lib/mailscope/message.rb', line 56 def from = addresses('from') |
#header_pairs ⇒ Object
109 110 111 |
# File 'lib/mailscope/message.rb', line 109 def header_pairs @header_pairs ||= mail.header.fields.map { |f| [f.name.to_s, safe_field_value(f)] } end |
#html? ⇒ Boolean
74 |
# File 'lib/mailscope/message.rb', line 74 def html? = parts.include?('html') |
#html_body ⇒ Object
105 |
# File 'lib/mailscope/message.rb', line 105 def html_body = @html_body ||= decode(html_part) |
#mail ⇒ Object
98 99 100 101 102 103 |
# File 'lib/mailscope/message.rb', line 98 def mail @mail ||= Mail.read_from_string(raw) rescue StandardError => e Mailscope.logger.warn("[mailscope] could not parse #{id}: #{e.class}: #{e.}") @mail = Mail.new end |
#mailbox_keys ⇒ Object
Every mailbox this message landed in, used to build the mailbox rail.
65 |
# File 'lib/mailscope/message.rb', line 65 def mailbox_keys = recipients.map { |a| a.address.to_s.downcase }.reject(&:empty?).uniq |
#mailer ⇒ Object
70 |
# File 'lib/mailscope/message.rb', line 70 def mailer = ['mailer'] |
#message_id ⇒ Object
69 |
# File 'lib/mailscope/message.rb', line 69 def = ['message_id'] |
#parts ⇒ Object
77 |
# File 'lib/mailscope/message.rb', line 77 def parts = Array(['parts']) |
#preview ⇒ Object
72 |
# File 'lib/mailscope/message.rb', line 72 def preview = ['preview'].to_s |
#raw ⇒ Object
--- lazy data (parses the raw .eml) ------------------------------------
96 |
# File 'lib/mailscope/message.rb', line 96 def raw = @raw ||= store.read_raw(id) |
#recipients ⇒ Object
62 |
# File 'lib/mailscope/message.rb', line 62 def recipients = to + cc + bcc |
#reply_to ⇒ Object
60 |
# File 'lib/mailscope/message.rb', line 60 def reply_to = addresses('reply_to') |
#sent_at ⇒ Object
50 51 52 53 54 |
# File 'lib/mailscope/message.rb', line 50 def sent_at @sent_at ||= Time.parse(['sent_at']) rescue ArgumentError, TypeError Time.at(0) end |
#size ⇒ Object
71 |
# File 'lib/mailscope/message.rb', line 71 def size = ['size'].to_i |
#subject ⇒ Object
--- indexed data (cheap: comes straight from metadata.json) ------------
45 46 47 48 |
# File 'lib/mailscope/message.rb', line 45 def subject value = ['subject'].to_s value.empty? ? Mailscope.translate('message.no_subject', '(no subject)') : value end |
#text? ⇒ Boolean
75 |
# File 'lib/mailscope/message.rb', line 75 def text? = parts.include?('text') |
#text_body ⇒ Object
107 |
# File 'lib/mailscope/message.rb', line 107 def text_body = @text_body ||= decode(text_part) |
#to ⇒ Object
57 |
# File 'lib/mailscope/message.rb', line 57 def to = addresses('to') |
#to_h ⇒ Object
118 119 120 121 122 123 124 |
# File 'lib/mailscope/message.rb', line 118 def to_h .merge( 'subject' => subject, 'sent_at' => sent_at.iso8601, 'mailboxes' => mailbox_keys ) end |
#to_param ⇒ Object
67 |
# File 'lib/mailscope/message.rb', line 67 def to_param = id |
#visible_attachments ⇒ Object
90 |
# File 'lib/mailscope/message.rb', line 90 def = .reject(&:inline?) |