Class: Astrotrain::Message

Inherits:
Object
  • Object
show all
Defined in:
lib/astrotrain/message.rb

Overview

Wrapper around a TMail object

Constant Summary collapse

EMAIL_REGEX =
/[\w\.\_\%\+\-]+[^\s\.]@[\w\-\_\.]+/

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mail, path = nil) ⇒ Message

Returns a new instance of Message.



48
49
50
51
52
53
# File 'lib/astrotrain/message.rb', line 48

def initialize(mail, path = nil)
  @body = @html = @attachments = nil
  @path        = path
  @mail        = mail
  @recipients  = {}
end

Class Attribute Details

.recipient_header_orderObject

Returns the value of attribute recipient_header_order.



18
19
20
# File 'lib/astrotrain/message.rb', line 18

def recipient_header_order
  @recipient_header_order
end

.skipped_headersObject

Returns the value of attribute skipped_headers.



18
19
20
# File 'lib/astrotrain/message.rb', line 18

def skipped_headers
  @skipped_headers
end

Instance Attribute Details

#mailObject (readonly)

Reference to the internal Mail object that parsed the raw email.



12
13
14
# File 'lib/astrotrain/message.rb', line 12

def mail
  @mail
end

#pathObject (readonly)

Refebrence to the original file that this Mail came from.



15
16
17
# File 'lib/astrotrain/message.rb', line 15

def path
  @path
end

Class Method Details

.parse(raw) ⇒ Object

Public: Parses the raw email headers into a Astrotrain::Message instance.

raw - String of the email content

Returns Astrotrain::Message instance.



44
45
46
# File 'lib/astrotrain/message.rb', line 44

def self.parse(raw)
  new(::Mail.new(raw))
end

.read(path) ⇒ Object

Public: Parses the raw email headers into a Astrotrain::Message instance.

path - String path to the file.

Returns Astrotrain::Message instance.



35
36
37
# File 'lib/astrotrain/message.rb', line 35

def self.read(path)
  new(::Mail.read(path), path)
end

.unescape(s) ⇒ Object

Stolen from Rack/Camping, remove the “+” => “ ” translation



248
249
250
251
252
253
254
255
# File 'lib/astrotrain/message.rb', line 248

def self.unescape(s)
  s.gsub!(/((?:%[0-9a-fA-F]{2})+)/n) do
    original = $1.dup
    replacement = [$1.delete('%')].pack('H*')
    replacement.as_utf8.valid? ? replacement : original
  end
  s
end

Instance Method Details

#address_list_for(emails, retried = false) ⇒ Object

Uses Mail::AddressList to parse the given comma separated emails.

emails - Array of String email headers.

Example: ["[email protected], Bar <[email protected]>", ...]

Returns Array of Mail::Address objects



233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/astrotrain/message.rb', line 233

def address_list_for(emails, retried = false)
  emails = emails * ", "
  list   = Mail::AddressList.new(self.class.unescape(emails))
  addrs  = list.addresses.each { |a| a.decoded }
  addrs.uniq!
  addrs
rescue Mail::Field::ParseError
  if retried
    raise
  else
    address_list_for(emails.scan(EMAIL_REGEX), true)
  end
end

#attachmentsObject

Public: Gets the attachments in the email.

Returns Array of Astrotrain::Attachment objects.



135
136
137
138
# File 'lib/astrotrain/message.rb', line 135

def attachments
  process_message_body if !@attachments
  @attachments
end

#bodyObject

Public: Gets the plain/text body of the email.

Returns String



119
120
121
122
# File 'lib/astrotrain/message.rb', line 119

def body
  process_message_body if !@body
  @body
end

#ccObject

Public: Unquotes and converts the Cc header to UTF-8.

Returns Array of Mail::Address objects



97
98
99
# File 'lib/astrotrain/message.rb', line 97

def cc
  @cc ||= unquoted_address_header(:cc)
end

#convert_to_utf8(s) ⇒ Object

Converts a given String to UTF-8. If the message has no charset assigned, we’ll attempt to detect it then convert it to UTF-8.

s - unconverted String in the wrong character set

Returns converted String.



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/astrotrain/message.rb', line 310

def convert_to_utf8(s)
  # If this string is already valid UTF-8 just hand it back
  if s.as_utf8.valid?
    s.force_encoding("UTF-8") if s.respond_to?(:force_encoding)
    return s
  end

  # First lets try to detect the encoding if the message didn't specify
  if !@mail.charset && detection = CharlockHolmes::EncodingDetector.detect(s)
    @mail.charset = detection[:encoding]
  end

  # if the encoding was already set or we just detected it AND it's not already
  # set to UTF-8 - try to transcode the body into UTF-8
  if @mail.charset && @mail.charset != 'UTF-8'
    s = CharlockHolmes::Converter.convert s, @mail.charset, 'UTF-8'
  end

  # By the time we get here, `s` is either UTF-8 or we need to force it to be
  # But, even if it's UTF-8 we could be in the case where the charset on the
  # message was set to UTF-8 but is in fact invalid.
  # So for either case, we want to make sure the output is valid UTF-8 - even
  # if it means mutating the invalid string.
  # Also we're not reusing the String::UTF8 version of `s` from above here
  # because by this point, it may be a new string.
  s.as_utf8.clean.as_raw
end

#fromObject Also known as: sender

Public: Unquotes and converts the From header to UTF-8.

Returns Array of Mail::Address objects



82
83
84
# File 'lib/astrotrain/message.rb', line 82

def from
  @from ||= unquoted_address_header(:from)
end

#headersObject

Public: Builds a hash of headers, skipping the keys specified in #skipped_headers. If header values cannot be parsed, the original raw value is provided.

Returns Hash of the headers with String keys and values.



145
146
147
148
149
150
151
152
153
154
155
# File 'lib/astrotrain/message.rb', line 145

def headers
  @headers ||= begin
    @mail.header.fields.inject({}) do |memo, field|
      name = field.name.downcase.to_s
      next memo if self.class.skipped_headers.include?(name)

      header = unquoted_header(name)
      memo.update(name => self.class.unescape(header))
    end
  end
end

#htmlObject

Public: Gets the html body of the email.

Returns String



127
128
129
130
# File 'lib/astrotrain/message.rb', line 127

def html
  process_message_body if !@html
  @html
end

#message_idObject

Public: Gets the unique message-id for the email, with the surrounding ‘<` and `>` parsed out.

Returns String



112
113
114
# File 'lib/astrotrain/message.rb', line 112

def message_id
  @mail.message_id
end

#process_message_bodyObject

Parses the mail’s parts, assembling the plain/HTML Strings, as well as any attachments.

Returns nothing.



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/astrotrain/message.rb', line 261

def process_message_body
  @attachments = []
  if @mail.multipart?
    @body, @html = [], []
    scan_parts(@mail)
    @body = @body.join("\n")
    @html = @html.join("\n")
  else
    if @mail.content_type =~ /text\/html/
      @html = @mail.body.to_s
      @body = ''
    else
      @body = @mail.body.to_s
      @html = ''
    end
  end

  @body = convert_to_utf8(@body)
  @html = convert_to_utf8(@html)
end

#recipients(order = nil) ⇒ Object

Public: Gets the recipients of an email using the To/Delivered-To/X-Original-To headers. It’s not always straightforward which email we want when dealing with filters and forward rules.

order - Array of email header names that specifies the order that the

list of recipient emails is assembled.  Valid strings are:
'original_to', 'delivered_to', and 'to'.

Returns Array of possible recipients.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/astrotrain/message.rb', line 64

def recipients(order = nil)
  if !@recipients.key?(order)
    order = self.class.recipient_header_order if order.blank?
    recipients = []

    emails = order.inject([]) do |memo, key|
      memo.push *send("recipients_from_#{key}")
    end

    @recipients[order] = emails.map! { |em| em.address }
    @recipients[order].uniq!
  end
  @recipients[order]
end

#recipients_from_bodyObject

Parses out all email addresses from the body of the email.

Returns Array of Mail::Address objects



183
184
185
186
187
188
189
190
# File 'lib/astrotrain/message.rb', line 183

def recipients_from_body
  @recipients_from_body ||= begin
    emails_from_body = body.scan(EMAIL_REGEX)
    address_list_for(emails_from_body, true)
  rescue Mail::Field::ParseError
    []
  end
end

#recipients_from_delivered_toObject

Parses the ‘Delivered-To’ header for email address.

Returns Array of Mail::Address objects



169
170
171
# File 'lib/astrotrain/message.rb', line 169

def recipients_from_delivered_to
  @recipients_from_delivered_to ||= unquoted_address_header('delivered-to')
end

#recipients_from_original_toObject

Parses the ‘X-Original-To’ header for email address.

Returns Array of Mail::Address objects



176
177
178
# File 'lib/astrotrain/message.rb', line 176

def recipients_from_original_to
  @recipients_from_original_to ||= unquoted_address_header('x-original-to')
end

#recipients_from_toObject

Parses the ‘To’ header for email address.

Returns Array of Mail::Address objects



162
163
164
# File 'lib/astrotrain/message.rb', line 162

def recipients_from_to
  to
end

#scan_parts(message) ⇒ Object

Recursive method to scan all the parts of the given part.

Returns nothing.



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/astrotrain/message.rb', line 285

def scan_parts(message)
  message.parts.each do |part|
    if part.multipart?
      scan_parts(part)
    else
      case part.content_type
        when /text\/plain/
          @body << part.body.to_s
        when /text\/html/
          @html << part.body.to_s
        else
          att = Attachment.new(part)
          @attachments << att if att.attached?
      end
    end
  end
end

#subjectObject

Public: Unquotes and converts the Subject header to UTF-8.

Returns String



104
105
106
# File 'lib/astrotrain/message.rb', line 104

def subject
  @mail.subject
end

#toObject

Public: Unquotes and converts the To header to UTF-8.

Returns Array of Mail::Address objects



90
91
92
# File 'lib/astrotrain/message.rb', line 90

def to
  @to ||= unquoted_address_header(:to)
end

#unquoted_address_header(key) ⇒ Object

Parses the given header for email addresses. Handles the case where some keys return arrays if there are multiple values.

key - String or Symbol header name

Returns Array of Mail::Address objects



214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/astrotrain/message.rb', line 214

def unquoted_address_header(key)
  if header = @mail[key]
    emails = if header.respond_to?(:value)
      [header.value]
    else
      header.map { |h| h.value }
    end
    address_list_for(emails)
  else
    []
  end
end

#unquoted_header(key) ⇒ Object

Parses the quoted header values: ‘=?…?=`.

key - String or Symbol header name

Returns unquoted String.



197
198
199
200
201
202
203
204
205
206
# File 'lib/astrotrain/message.rb', line 197

def unquoted_header(key)
  if header = @mail[key]
    value = header.respond_to?(:map) ?
      header.map { |h| h.value }.join("\n") :
      header.value
    Mail::Encodings.value_decode(value)
  else
    ''
  end
end