mail_builder 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +47 -0
- data/lib/mail_builder/attachment.rb +35 -0
- data/lib/mail_builder.rb +323 -0
- metadata +65 -0
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
task :default => [:test]
|
5
|
+
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.libs << "tests"
|
8
|
+
t.test_files = FileList["tests/**/*_test.rb"]
|
9
|
+
t.verbose = true
|
10
|
+
end
|
11
|
+
|
12
|
+
task :rdoc do
|
13
|
+
sh 'rm -r doc' if File.directory?('doc')
|
14
|
+
begin
|
15
|
+
sh 'sdoc --line-numbers --inline-source --main "README.rdoc" --title "MailBuilder Documentation" README.rdoc lib'
|
16
|
+
rescue
|
17
|
+
puts "sdoc not installed:"
|
18
|
+
puts " gem install voloko-sdoc --source http://gems.github.com"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
require "rake/gempackagetask"
|
23
|
+
|
24
|
+
NAME = "mail_builder"
|
25
|
+
SUMMARY = "MailBuilder is a simple library for building RFC compliant MIME emails."
|
26
|
+
GEM_VERSION = "0.1"
|
27
|
+
|
28
|
+
spec = Gem::Specification.new do |s|
|
29
|
+
s.name = NAME
|
30
|
+
s.summary = s.description = SUMMARY
|
31
|
+
s.author = "Bernerd Schaefer"
|
32
|
+
s.email = "bernerd@wieck.com"
|
33
|
+
s.version = GEM_VERSION
|
34
|
+
s.platform = Gem::Platform::RUBY
|
35
|
+
s.require_path = 'lib'
|
36
|
+
s.files = %w(Rakefile) + Dir.glob("lib/**/*")
|
37
|
+
s.add_dependency 'mime-types'
|
38
|
+
end
|
39
|
+
|
40
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
41
|
+
pkg.gem_spec = spec
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "Install MailBuilder as a gem"
|
45
|
+
task :install => [:repackage] do
|
46
|
+
sh %{gem install pkg/#{NAME}-#{GEM_VERSION}}
|
47
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class MailBuilder
|
2
|
+
class Attachment
|
3
|
+
|
4
|
+
attr_accessor :file, :name, :type, :headers
|
5
|
+
|
6
|
+
def initialize(file, name, type, headers)
|
7
|
+
@file = file
|
8
|
+
@name = name
|
9
|
+
@type = type
|
10
|
+
@headers = headers
|
11
|
+
|
12
|
+
# If we have a File/IO object, read it. Otherwise, we'll read it lazily.
|
13
|
+
@body = file.read() if !file.kind_of?(Pathname) && file.respond_to?(:read)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
@body ||= File.open(file.to_s(), "rb") { |f| f.read() }
|
18
|
+
|
19
|
+
[@body].pack("m")
|
20
|
+
end
|
21
|
+
|
22
|
+
def name
|
23
|
+
@name
|
24
|
+
end
|
25
|
+
|
26
|
+
def type
|
27
|
+
@type ||= MIME::Types.type_for(@name.to_s).to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def inspect
|
31
|
+
"#<Wheels::Mail::Attachment @file=#{@file.inspect}> @name=#{@name.inspect} @type=#{@type.inspect} @headers=#{@headers.inspect}"
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/mail_builder.rb
ADDED
@@ -0,0 +1,323 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
# Enumerators are not built into Ruby 1.8.6
|
5
|
+
require 'enumerator' unless ''.respond_to?(:enum_for)
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
require 'mime/types'
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'mail_builder'
|
12
|
+
|
13
|
+
##
|
14
|
+
# MailBuilder is a library for building RFC compliant MIME messages,
|
15
|
+
# with support for text and HTML emails, as well as attachments.
|
16
|
+
#
|
17
|
+
# Basic Usage:
|
18
|
+
#
|
19
|
+
# mail = MailBuilder.new
|
20
|
+
# mail.to = "joe@example.com"
|
21
|
+
# mail.text = "Body"
|
22
|
+
#
|
23
|
+
# sendmail = IO.popen("#{`which sendmail`.chomp} -i -t", "w+")
|
24
|
+
# sendmail.puts mail
|
25
|
+
# sendmail.close
|
26
|
+
#
|
27
|
+
# # or
|
28
|
+
#
|
29
|
+
# require 'net/smtp'
|
30
|
+
#
|
31
|
+
# Net::SMTP.start("smtp.address.com", 25) do |smtp|
|
32
|
+
# smtp.send_message(mail.to_s, mail.from, mail.to)
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
##
|
36
|
+
class MailBuilder
|
37
|
+
require Pathname(__FILE__).dirname + 'mail_builder/attachment'
|
38
|
+
|
39
|
+
##
|
40
|
+
# Boundary characters, slightly adapted from those allowed by rfc1341,
|
41
|
+
# representing:
|
42
|
+
#
|
43
|
+
# ALPHA / DIGIT / "'" / "(" / ")" / "*" / "," / "-" / "." / "/" / ":"
|
44
|
+
#
|
45
|
+
# See 7.2.1, http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
|
46
|
+
##
|
47
|
+
BOUNDARY_CHARS = ((39..58).to_a + (65..90).to_a + (97..122).to_a).map { |_| _.chr }.freeze
|
48
|
+
|
49
|
+
##
|
50
|
+
# Valid characters for an envelope_id
|
51
|
+
##
|
52
|
+
ENVELOPE_CHARS = BOUNDARY_CHARS - ["+"]
|
53
|
+
|
54
|
+
CHARSET = 'utf-8'.freeze
|
55
|
+
|
56
|
+
##
|
57
|
+
# Printable characters which RFC 2047 says must be escaped.
|
58
|
+
##
|
59
|
+
RFC2047_REPLACEMENTS = [
|
60
|
+
['?', '=%X' % ??],
|
61
|
+
['_', '=%X' % ?_],
|
62
|
+
[' ', '_'],
|
63
|
+
[/=$/, '']
|
64
|
+
].freeze
|
65
|
+
|
66
|
+
attr_accessor :html, :text
|
67
|
+
attr_reader :headers
|
68
|
+
|
69
|
+
##
|
70
|
+
# Accepts an options hash, setting text and html if provided, and
|
71
|
+
# setting any provided headers.
|
72
|
+
#
|
73
|
+
# mailer = MailBuilder.new(:text => "Text", :to => "admin@site.com", "X-Priority" => 1)
|
74
|
+
# mailer.text # => "Text"
|
75
|
+
# mailer.to # => "admin@site.com"
|
76
|
+
# mailer.get_header("X-Priority") # => 1
|
77
|
+
##
|
78
|
+
def initialize(options = {})
|
79
|
+
@headers = []
|
80
|
+
@attachments = []
|
81
|
+
@text = nil
|
82
|
+
@html = nil
|
83
|
+
|
84
|
+
parse_options(options)
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Adds a header value to the mailer's headers, without removing
|
89
|
+
# previous values.
|
90
|
+
#
|
91
|
+
# mailer.add_header("From", "admin@example.com")
|
92
|
+
# mailer.add_header("From", "john@example.com")
|
93
|
+
#
|
94
|
+
# mailer.headers # => [["From", "admin@example.com"], ["From", "admin@example.com"]]
|
95
|
+
##
|
96
|
+
def add_header(key, value)
|
97
|
+
@headers << [key.to_s, value]
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Retrieves a value from the mailer's headers.
|
102
|
+
#
|
103
|
+
# mailer.get_header("to") # => "admin@example.com"
|
104
|
+
##
|
105
|
+
def get_header(key)
|
106
|
+
@headers.detect { |k, v| return v if k == key }
|
107
|
+
end
|
108
|
+
|
109
|
+
def remove_header(key)
|
110
|
+
@headers.reject! { |k,| k == key }
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Adds a header value to the mailer's headers, replacing previous values.
|
115
|
+
#
|
116
|
+
# mailer.add_header("From", "admin@example.com")
|
117
|
+
# mailer.set_header("From", "john@example.com")
|
118
|
+
#
|
119
|
+
# mailer.headers # => [["From", "admin@example.com"]]
|
120
|
+
##
|
121
|
+
def set_header(key, value)
|
122
|
+
remove_header(key)
|
123
|
+
add_header(key, value)
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Returns the envelope id for this mailer. The mail spec's ENV_ID is
|
128
|
+
# used to provide a unique identifier that follows an email through it's
|
129
|
+
# various states -- included bounces -- allowing it to be tracked.
|
130
|
+
##
|
131
|
+
def envelope_id
|
132
|
+
@envelope_id ||= (1..25).to_a.map { ENVELOPE_CHARS[rand(ENVELOPE_CHARS.size)] }.join
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# We define getters and setters for commonly used headers.
|
137
|
+
##
|
138
|
+
%w(from to cc bcc reply_to subject).each do |header|
|
139
|
+
define_method(header) do
|
140
|
+
get_header(header)
|
141
|
+
end
|
142
|
+
|
143
|
+
define_method(header + "=") do |value|
|
144
|
+
set_header(header, value)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
##
|
149
|
+
# Wrapper for attach_as, setting the attachment filename to the file's
|
150
|
+
# basename.
|
151
|
+
#
|
152
|
+
# mailer.attach "some/file.pdf"
|
153
|
+
##
|
154
|
+
def attach(file, type = nil, headers = nil)
|
155
|
+
file = Pathname(file)
|
156
|
+
|
157
|
+
attach_as(file, file.basename, type, headers)
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Adds an Attachment to the email.
|
162
|
+
#
|
163
|
+
# If file is an IO or File object, it's contents will be read immediately, in case
|
164
|
+
# the mail will be delivered to an external service without access to the object.
|
165
|
+
# Otherwise, the attached file's content will be read when the message is built.
|
166
|
+
#
|
167
|
+
# If no type is provided, the MIME::Types library will be used to find a suitable
|
168
|
+
# content type.
|
169
|
+
#
|
170
|
+
# mailer.attach "account.html"
|
171
|
+
# mailer.attach_as StringIO.new("test"), "account.txt"
|
172
|
+
#
|
173
|
+
##
|
174
|
+
def attach_as(file, name, type = nil, headers = nil)
|
175
|
+
@attachments << Attachment.new(file, name, type, headers)
|
176
|
+
end
|
177
|
+
|
178
|
+
def multipart?
|
179
|
+
attachments? || @html
|
180
|
+
end
|
181
|
+
|
182
|
+
def attachments?
|
183
|
+
@attachments.any?
|
184
|
+
end
|
185
|
+
|
186
|
+
##
|
187
|
+
# Builds the full email message suitable to be passed to Sendmail, Net::SMTP, etc.
|
188
|
+
#
|
189
|
+
# It sets the Mail-From header (used for tracking emails), date, and message id,
|
190
|
+
# and then assembles the headers, body, and attachments.
|
191
|
+
#
|
192
|
+
# All expensive operations -- generating boundaries, rendering views, reading
|
193
|
+
# attached files -- are delayed until this method is called.
|
194
|
+
##
|
195
|
+
def build
|
196
|
+
set_header("Mail-From", "#{ENV["USER"]}@localhost ENVID=#{envelope_id}")
|
197
|
+
set_header("Date", Time.now.rfc2822)
|
198
|
+
set_header("Message-ID", "<#{Time.now.to_f}.#{Process.pid}@#{get_header("from").to_s.split("@", 2)[1]}>")
|
199
|
+
|
200
|
+
if multipart?
|
201
|
+
set_header("Mime-Version", "1.0")
|
202
|
+
if attachments?
|
203
|
+
set_header("Content-Type", "multipart/mixed; boundary=\"#{attachment_boundary}\"")
|
204
|
+
else
|
205
|
+
set_header("Content-Type", "multipart/alternative; boundary=\"#{body_boundary}\"")
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
build_headers + build_body
|
210
|
+
end
|
211
|
+
alias to_s build
|
212
|
+
|
213
|
+
private
|
214
|
+
|
215
|
+
def build_headers
|
216
|
+
@headers.map do |header, value|
|
217
|
+
header = header.gsub("_", "-")
|
218
|
+
key = header.downcase
|
219
|
+
|
220
|
+
value = quote(value, 'rfc2047') if key == "subject"
|
221
|
+
value = quote_address(value) if %w(from to cc bcc reply-to).include?(key)
|
222
|
+
|
223
|
+
"#{header}: #{value}"
|
224
|
+
end.join("\r\n") + "\r\n\r\n"
|
225
|
+
end
|
226
|
+
|
227
|
+
def build_body
|
228
|
+
return @text unless multipart?
|
229
|
+
|
230
|
+
body = []
|
231
|
+
body << "This is a multi-part message in MIME format."
|
232
|
+
body << "--#{attachment_boundary}\r\nContent-Type: multipart/alternative; boundary=\"#{body_boundary}\""
|
233
|
+
|
234
|
+
body << build_body_boundary('text/plain')
|
235
|
+
body << quote(@text)
|
236
|
+
|
237
|
+
body << build_body_boundary('text/html')
|
238
|
+
body << quote(@html)
|
239
|
+
|
240
|
+
body << "--#{body_boundary}--"
|
241
|
+
|
242
|
+
if attachments?
|
243
|
+
@attachments.each do |attachment|
|
244
|
+
body << build_attachment_boundary(attachment)
|
245
|
+
body << attachment
|
246
|
+
body << "\r\n--#{attachment_boundary}--"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
body.join("\r\n\r\n")
|
251
|
+
end
|
252
|
+
|
253
|
+
def build_body_boundary(type)
|
254
|
+
boundary = []
|
255
|
+
boundary << "--#{body_boundary}"
|
256
|
+
boundary << "Content-Type: #{type}; charset=#{CHARSET}#{'; format=flowed' if type == 'text/plain'}"
|
257
|
+
boundary << "Content-Transfer-Encoding: quoted-printable"
|
258
|
+
boundary.join("\r\n")
|
259
|
+
end
|
260
|
+
|
261
|
+
def build_attachment_boundary(attachment)
|
262
|
+
boundary = []
|
263
|
+
boundary << "--#{attachment_boundary}"
|
264
|
+
boundary << "Content-Type: #{attachment.type}; name=\"#{attachment.name}\""
|
265
|
+
boundary << "Content-Transfer-Encoding: base64"
|
266
|
+
boundary << "Content-Disposition: inline; filename=\"#{attachment.name}\""
|
267
|
+
|
268
|
+
boundary.push(*attachment.headers) if attachment.headers
|
269
|
+
|
270
|
+
boundary.join("\r\n")
|
271
|
+
end
|
272
|
+
|
273
|
+
def generate_boundary
|
274
|
+
"----=_NextPart_" + (1..25).map { BOUNDARY_CHARS[rand(BOUNDARY_CHARS.size)] }.join
|
275
|
+
end
|
276
|
+
|
277
|
+
def attachment_boundary
|
278
|
+
@attachment_boundary ||= generate_boundary
|
279
|
+
end
|
280
|
+
|
281
|
+
def body_boundary
|
282
|
+
@body_boundary ||= generate_boundary
|
283
|
+
end
|
284
|
+
|
285
|
+
def parse_options(options)
|
286
|
+
options.each do |key, value|
|
287
|
+
case key
|
288
|
+
when :html
|
289
|
+
self.html = value
|
290
|
+
when :text
|
291
|
+
self.text = text
|
292
|
+
else
|
293
|
+
set_header(key, value)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def quote(text, method = 'rfc2045')
|
299
|
+
return unless text
|
300
|
+
|
301
|
+
self.send("#{method}_encode", text)
|
302
|
+
end
|
303
|
+
|
304
|
+
def quote_address(address)
|
305
|
+
return address.map { |a| quote_address(a) }.join(", ") if address.is_a?(Array)
|
306
|
+
|
307
|
+
address.gsub(/['"](.+?)['"]\s+(<.+?>)/) do
|
308
|
+
"\"#{quote($1, 'rfc2047')}\" #{$2}"
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def rfc2047_encode(text)
|
313
|
+
text = text.enum_for(:each_byte).map { |ord| ord < 128 && ord != ?= ? ord.chr : "=%X" % ord }.join.chomp
|
314
|
+
|
315
|
+
RFC2047_REPLACEMENTS.each { |replacement| text.gsub!(*replacement) }
|
316
|
+
|
317
|
+
"=?#{CHARSET}?Q?#{text}?="
|
318
|
+
end
|
319
|
+
|
320
|
+
def rfc2045_encode(text)
|
321
|
+
[text].pack('M').gsub("\n", "\r\n").chomp.gsub(/=$/, '')
|
322
|
+
end
|
323
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mail_builder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bernerd Schaefer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-30 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: mime-types
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: MailBuilder is a simple library for building RFC compliant MIME emails.
|
26
|
+
email: bernerd@wieck.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- Rakefile
|
35
|
+
- lib/mail_builder
|
36
|
+
- lib/mail_builder/attachment.rb
|
37
|
+
- lib/mail_builder.rb
|
38
|
+
has_rdoc: false
|
39
|
+
homepage:
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 1.3.1
|
61
|
+
signing_key:
|
62
|
+
specification_version: 2
|
63
|
+
summary: MailBuilder is a simple library for building RFC compliant MIME emails.
|
64
|
+
test_files: []
|
65
|
+
|