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 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
@@ -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
+