Module: InboxBeam::Message

Defined in:
lib/inbox_beam/message.rb

Overview

Builds an RFC 5322 message ready to hand to IMAP APPEND. Zero dependencies — UTF-8 safe via base64 bodies and encoded-word headers.

Constant Summary collapse

CRLF =
"\r\n"

Class Method Summary collapse

Class Method Details

.ascii?(value) ⇒ Boolean

Returns:

  • (Boolean)


57
58
59
# File 'lib/inbox_beam/message.rb', line 57

def ascii?(value)
  value.ascii_only?
end

.base64_body(content) ⇒ Object

Base64-encode a body and wrap at 76 columns per RFC 2045.



80
81
82
# File 'lib/inbox_beam/message.rb', line 80

def base64_body(content)
  Base64.strict_encode64(content.encode("UTF-8")).scan(/.{1,76}/).join(CRLF)
end

.build(from:, to:, subject:, text: nil, html: nil, date: nil) ⇒ String

Returns the raw RFC 5322 message with CRLF line endings.

Parameters:

  • from (String)

    e.g. “you@example.com” or “Name <you@example.com>”

  • to (String)
  • subject (String)
  • text (String, nil) (defaults to: nil)
  • html (String, nil) (defaults to: nil)
  • date (Time, nil) (defaults to: nil)

Returns:

  • (String)

    the raw RFC 5322 message with CRLF line endings



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/inbox_beam/message.rb', line 21

def build(from:, to:, subject:, text: nil, html: nil, date: nil)
  date ||= Time.now
  message_id = "<#{SecureRandom.hex(16)}@#{domain_of(from)}>"

  headers = [
    "From: #{encode_header(from)}",
    "To: #{encode_header(to)}",
    "Subject: #{encode_word(subject)}",
    "Date: #{rfc2822_date(date)}",
    "Message-ID: #{message_id}",
    "MIME-Version: 1.0"
  ]

  if html && text
    boundary = "inbox_beam_#{SecureRandom.hex(12)}"
    headers << %(Content-Type: multipart/alternative; boundary="#{boundary}")
    body = [
      "--#{boundary}",
      mime_part("text/plain", text),
      "--#{boundary}",
      mime_part("text/html", html),
      "--#{boundary}--",
      ""
    ].join(CRLF)
  elsif html
    headers << "Content-Type: text/html; charset=UTF-8" << "Content-Transfer-Encoding: base64"
    body = base64_body(html)
  else
    headers << "Content-Type: text/plain; charset=UTF-8" << "Content-Transfer-Encoding: base64"
    body = base64_body(text.to_s)
  end

  raw = headers.join(CRLF) + CRLF + CRLF + body
  raw.gsub(/\r\n|\r|\n/, CRLF)
end

.domain_of(address) ⇒ Object



88
89
90
91
# File 'lib/inbox_beam/message.rb', line 88

def domain_of(address)
  m = address.match(/@([^>\s]+)/)
  m ? m[1] : "localhost"
end

.encode_header(value) ⇒ Object

Encode a header that may contain a display name: ‘名前 <addr>`.



69
70
71
72
73
74
75
76
77
# File 'lib/inbox_beam/message.rb', line 69

def encode_header(value)
  return value if ascii?(value)

  if (m = value.match(/\A(.*?)\s*<([^>]+)>\s*\z/))
    "#{encode_word(m[1])} <#{m[2]}>"
  else
    encode_word(value)
  end
end

.encode_word(value) ⇒ Object

RFC 2047 encoded-word for non-ASCII header values.



62
63
64
65
66
# File 'lib/inbox_beam/message.rb', line 62

def encode_word(value)
  return value if ascii?(value)

  "=?UTF-8?B?#{Base64.strict_encode64(value.encode("UTF-8"))}?="
end

.mime_part(content_type, content) ⇒ Object



93
94
95
96
97
98
99
100
# File 'lib/inbox_beam/message.rb', line 93

def mime_part(content_type, content)
  [
    "Content-Type: #{content_type}; charset=UTF-8",
    "Content-Transfer-Encoding: base64",
    "",
    base64_body(content)
  ].join(CRLF)
end

.rfc2822_date(time) ⇒ Object



84
85
86
# File 'lib/inbox_beam/message.rb', line 84

def rfc2822_date(time)
  time.getutc.strftime("%a, %d %b %Y %H:%M:%S +0000")
end