mail_builder 0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+