rhizmail 0.1.0

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.
Files changed (3) hide show
  1. data/lib/rhizmail.rb +290 -0
  2. data/lib/rhizmail.rb~ +260 -0
  3. metadata +60 -0
data/lib/rhizmail.rb ADDED
@@ -0,0 +1,290 @@
1
+ # RhizMail is a test-friendly library for sending out customized emails. This
2
+ # is the library we use day-to-day at http://rhizome.org, where we send out
3
+ # more than 100 customized emails a day.
4
+ #
5
+ # You can view the Rubyforge project page at
6
+ # http://rubyforge.org/projects/rhizmail.
7
+ #
8
+ # To use RhizMail, create an email and deliver it.
9
+ #
10
+ # def send_site_message( to_address, to_name, body )
11
+ # msg = RhizMail::Message.new(
12
+ # 'New features at website.com', to_address, 'webmaster@mysite.com',
13
+ # body
14
+ # )
15
+ # msg.to_name = to_name
16
+ # msg.deliver
17
+ # end
18
+ #
19
+ # If you want to test this message, you can set the MockMailer to take place
20
+ # of the default Mailer:
21
+ #
22
+ # def test_send_site_message
23
+ # mock_mailer = RhizMail::MockMailer.new
24
+ # RhizMail::set_mailer mock_mailer
25
+ # send_site_message( 'bill@yahoo.com', 'Bill Smith', 'Hi, Bill!' )
26
+ # assert_equal( 1, mock_mailer.messages_sent )
27
+ # assert_equal( 'Bill Smith', mock_mailer.messages_sent.first.to_name )
28
+ # end
29
+ #
30
+ # RhizMail comes with a SimpleTemplateMessage class, suited for times when you
31
+ # want email templating that is simpler than Amrita or ERB.
32
+ #
33
+ # == Dependencies
34
+ #
35
+ # RhizMail depends on two external libraries:
36
+ #
37
+ # * Lafcadio: http://lafcadio.rubyforge.org
38
+ # * MockFS: http://mockfs.rubyforge.org
39
+
40
+ require 'lafcadio'
41
+ require 'mockfs'
42
+ require 'net/smtp'
43
+
44
+ module RhizMail
45
+ Version = '0.1.0'
46
+
47
+ # Returns a boolean value describing whether <tt>address</tt> is a plausible
48
+ # email address format.
49
+ def self.valid_address(address); (address =~ /\w@\w*\./) != nil; end
50
+
51
+ # InvalidStateError is raised by SimpleTemplateMessage if you try to send it
52
+ # out without doing all the necessary template substitutions.
53
+ class InvalidStateError < RuntimeError
54
+ end
55
+
56
+ # The Mailer is the class used to handle mail delivery. However, you usually
57
+ # don't need to manipulate it directly; it's usually sufficient to call
58
+ # Message#deliver.
59
+ class Mailer < Lafcadio::ContextualService
60
+ @@smtpServer = 'localhost'
61
+ @@smtpClass = Net::SMTP
62
+ @@messagesSent = []
63
+
64
+ # Resets record of messages sent.
65
+ def self.reset; @@messagesSent = []; end
66
+
67
+ def self.smtp_class=(smtpClass) # :nodoc:
68
+ @@smtpClass = smtpClass;
69
+ end
70
+
71
+ # Sends an email message. Will call +email+ needs to respond to
72
+ # +verify_sendable+; Mailer##send_email will call +verify_sendable+ before
73
+ # sending the email.
74
+ def send_email(email)
75
+ email.verify_sendable
76
+ msg = []
77
+ email.headers.each { |header| msg << "#{header}\n" }
78
+ msg << "\n"
79
+ msg << email.body
80
+ begin
81
+ @@smtpClass.start( @@smtpServer ) { |smtp|
82
+ smtp.sendmail(msg, email.from_address, [ email.to_address ])
83
+ }
84
+ @@messagesSent << email
85
+ rescue Net::ProtoFatalError, TimeoutError, Errno::ECONNREFUSED,
86
+ Errno::ECONNRESET
87
+ # whatever
88
+ end
89
+ end
90
+
91
+ # Returns an array of what messages have been sent.
92
+ def messages_sent; @@messagesSent; end
93
+ end
94
+
95
+ class Message
96
+ HTML_CONTENT_TYPE = 0
97
+ MULTIPART_CONTENT_TYPE = 1
98
+
99
+ attr_accessor :body, :char_set, :content_type, :from_address, :from_name,
100
+ :subject, :to_address, :to_name
101
+
102
+ # Unless they are explicitly set using accessors, the following defaults
103
+ # apply:
104
+ # [to_name] nil
105
+ # [from_name] nil
106
+ # [content_type] nil
107
+ # [char_set] 'iso-8859-1'
108
+ def initialize( subject, to_address, from_address, body = nil )
109
+ @subject = subject
110
+ @to_address = to_address
111
+ @from_address = from_address
112
+ @char_set = 'iso-8859-1'
113
+ @body = body
114
+ end
115
+
116
+ # Sends the email.
117
+ def deliver; Mailer.get_mailer.send_email( self ); end
118
+
119
+ # Returns an array of strings describing the headers for this email
120
+ # message.
121
+ def headers
122
+ headers = []
123
+ headers << "Subject: #{@subject}"
124
+ toHeader = "To: "
125
+ if @to_name
126
+ toHeader += " #{@to_name} <#{@to_address}>"
127
+ else
128
+ toHeader += " #{@to_address}"
129
+ end
130
+ headers << toHeader
131
+ fromHeader = "From: "
132
+ if @from_name
133
+ fromHeader += "#{@from_name} <#{@from_address}>"
134
+ else
135
+ fromHeader += "#{@from_address}"
136
+ end
137
+ headers << fromHeader
138
+ if content_type == HTML_CONTENT_TYPE
139
+ headers << "Content-Type: text/html; charset=\"#{@char_set}\""
140
+ headers << "MIME-Version: 1.0"
141
+ elsif content_type == MULTIPART_CONTENT_TYPE
142
+ headers << "Content-Type: Multipart/Alternative; charset=\"#{@char_set}\""
143
+ headers << "MIME-Version: 1.0"
144
+ end
145
+ headers
146
+ end
147
+
148
+ # Mailer calls this before sending a message; subclasses can override this
149
+ # if they want to ensure that certain parts of the message are valid before
150
+ # sending.
151
+ def verify_sendable
152
+ [ :subject, :from_address, :to_address, :body ].each { |field|
153
+ if self.send( field ).nil?
154
+ raise( InvalidStateError,
155
+ "Can't send email with blank #{ field.id2name }" )
156
+ end
157
+ }
158
+ end
159
+ end
160
+
161
+ # The MockMailer can be swapped out for the Mailer in test code, and then
162
+ # queried to make sure that the right emails were sent out. To set this, call
163
+ #
164
+ # mock_mailer = RhizMail::MockMailer.new
165
+ # RhizMail::set_mailer mock_mailer
166
+ #
167
+ # Afterwards, every time you call Message#deliver it will be delivered by the
168
+ # MockMailer.
169
+ class MockMailer
170
+ attr_reader :messages_sent
171
+
172
+ def initialize; flush; end
173
+
174
+ # Clear out the history of messages sent.
175
+ def flush; @messages_sent = []; end
176
+
177
+ # Pretends to send an email, recording it in +messages_sent+ to allow
178
+ # inspection later.
179
+ def send_email(email)
180
+ email.verify_sendable
181
+ @messages_sent << email
182
+ end
183
+ end
184
+
185
+ # The SimpleTemplateMessage is designed to let a programmer define a simple
186
+ # set of tags for a certain sort of message and then substitute them in-code.
187
+ # It's intended to be simple enough that non-programmers can edit it without
188
+ # too much confusion.
189
+ #
190
+ # As an example, let's say you've got an email to send out whenever somebody
191
+ # signs up to your website. The template could look like this:
192
+ #
193
+ # $ cat /Users/francis/Desktop/template.txt
194
+ # Subject: Thanks for joining Website.com!
195
+ #
196
+ # Hi! Thanks for joining Website.com. For your reference, here's your signup
197
+ # information:
198
+ #
199
+ # Email: <% email %>
200
+ # Password: <% password %>
201
+ #
202
+ # Thanks!
203
+ #
204
+ # Note that the first line needs to be in the format "Subject: [ SUBJECT ]",
205
+ # and should be followed by a blank line.
206
+ #
207
+ # Then you create an instance of SimpleTemplateMessage and use
208
+ # SimpleTemplateMessage#substitute to change the contents:
209
+ #
210
+ # irb> msg = RhizMail::SimpleTemplateMessage.new(
211
+ # 'john.doe@email.com', 'webmaster@website.com',
212
+ # '/Users/francis/Desktop/template.txt'
213
+ # )
214
+ # irb> msg.substitute( 'email', 'john.doe@email.com' )
215
+ # irb> msg.substitute( 'password', 'p4ssw0rd' )
216
+ # irb> puts msg.body
217
+ # Hi! Thanks for joining Website.com. For your reference, here's your signup
218
+ # information:
219
+ #
220
+ # Email: john.doe@email.com
221
+ # Password: p4ssw0rd
222
+ #
223
+ # Thanks!
224
+ # irb> msg.deliver
225
+ #
226
+ # If you call SimpleTemplateMessage#deliver without doing all the
227
+ # substitutions required by the template, it will raise an InvalidStateError.
228
+ # (Getting an exception is probably better than sending a customer an email
229
+ # with funny symbols in it.)
230
+ class SimpleTemplateMessage < Message
231
+ def initialize( to_address, from_address, template_file )
232
+ template = ''
233
+ MockFS.file.open( template_file ) { |file| template = file.gets nil }
234
+ template =~ /Subject: (.*)/
235
+ subject = $1
236
+ super( subject, to_address, from_address )
237
+ @body = generate_body template
238
+ end
239
+
240
+ def body_template( template ) # :nodoc:
241
+ body = ''
242
+ blank_line_found = false
243
+ template.each { |line|
244
+ if blank_line_found
245
+ body += line
246
+ else
247
+ blank_line_found = true if line =~ /^\n/
248
+ end
249
+ }
250
+ body
251
+ end
252
+
253
+ def generate_body( template ) # :nodoc:
254
+ body = body_template( template )
255
+ tokens = body.scan(/<%\s*(\S*)\s*%>/).collect { |matchArray|
256
+ matchArray[0]
257
+ }
258
+ tokens.each { |token|
259
+ if ( proc = substitutions[token] ); substitute( token, proc ); end
260
+ }
261
+ body
262
+ end
263
+
264
+ def substitute(token, value_or_proc )
265
+ regexp = Regexp.new( "<%\s*#{ token }\s*%>", true )
266
+ @body.gsub!( regexp ){ |match|
267
+ value_or_proc.class <= Proc ? value_or_proc.call : value_or_proc
268
+ }
269
+ end
270
+
271
+ # If you want to define a subclass of SimpleTemplateMessage, you can
272
+ # override +substitutions+ to automatically define a set of substitutions
273
+ # to make when you create a new instance. +substitutions+ should always
274
+ # return a hash of tokens to either values or Procs.
275
+ def substitutions; {}; end
276
+
277
+ def verify_sendable
278
+ super
279
+ if @body =~ /<%/ || @body =~ /%>/
280
+ raise InvalidStateError, "substitution failed: #{ @body }", caller
281
+ elsif @subject =~/<%/ || @subject =~ /%>/
282
+ raise( InvalidStateError,
283
+ "substitution failed with subject: #{ @subject }",
284
+ caller )
285
+ elsif @body == ''
286
+ raise( InvalidStateError, "can't send email with blank body", caller )
287
+ end
288
+ end
289
+ end
290
+ end
data/lib/rhizmail.rb~ ADDED
@@ -0,0 +1,260 @@
1
+ # RhizMail is a test-friendly library for sending out customized emails. This
2
+ # is the library we use day-to-day at Rhizome.org, where we send out more than
3
+ # 100 customized emails a day.
4
+ #
5
+ # You can view the Rubyforge project page at
6
+ # http://rubyforge.org/projects/rhizmail.
7
+ #
8
+ # To use RhizMail, create an email and deliver it.
9
+ #
10
+ # def send_site_message( to_address, to_name, body )
11
+ # msg = RhizMail::Message.new(
12
+ # 'New features at website.com', to_address, 'webmaster@mysite.com',
13
+ # body
14
+ # )
15
+ # msg.to_name = to_name
16
+ # msg.deliver
17
+ # end
18
+ #
19
+ # If you want to test this message, you can set the MockMailer to take place
20
+ # of the default Mailer:
21
+ #
22
+ # def test_send_site_message
23
+ # mock_mailer = RhizMail::MockMailer.new
24
+ # RhizMail::set_mailer mock_mailer
25
+ # send_site_message( 'bill@yahoo.com', 'Bill Smith', 'Hi, Bill1' )
26
+ # assert_equal( 1, mock_mailer.messages_sent )
27
+ # assert_equal( 'Bill Smith', mock_mailer.messages_sent.first.to_name )
28
+ # end
29
+
30
+ require 'lafcadio'
31
+ require 'mockfs'
32
+ require 'net/smtp'
33
+
34
+ module RhizMail
35
+ Version = '0.1.0'
36
+
37
+ # Returns a boolean value describing whether <tt>address</tt> is a plausible
38
+ # email address format.
39
+ def self.valid_address(address); (address =~ /\w@\w*\./) != nil; end
40
+
41
+ # InvalidStateError is raised by SimpleTemplateMessage if you try to send it
42
+ # out without doing all the necessary template substitutions.
43
+ class InvalidStateError < RuntimeError
44
+ end
45
+
46
+ # The Mailer is the class used to handle mail delivery. However, you usually
47
+ # don't need to manipulate it directly; it's usually sufficient to call
48
+ # Message#deliver.
49
+ class Mailer < Lafcadio::ContextualService
50
+ @@smtpServer = 'localhost'
51
+ @@smtpClass = Net::SMTP
52
+ @@messagesSent = []
53
+
54
+ # Resets record of messages sent.
55
+ def self.reset; @@messagesSent = []; end
56
+
57
+ def self.set_smtp_class(smtpClass) # :nodoc:
58
+ @@smtpClass = smtpClass;
59
+ end
60
+
61
+ # Sends an email message.
62
+ def send_email(email)
63
+ email.verify_sendable
64
+ msg = []
65
+ email.headers.each { |header| msg << "#{header}\n" }
66
+ msg << "\n"
67
+ msg << email.body
68
+ begin
69
+ @@smtpClass.start( @@smtpServer ) { |smtp|
70
+ smtp.sendmail(msg, email.from_address, [ email.to_address ])
71
+ }
72
+ @@messagesSent << email
73
+ rescue Net::ProtoFatalError, TimeoutError, Errno::ECONNREFUSED,
74
+ Errno::ECONNRESET
75
+ # whatever
76
+ end
77
+ end
78
+
79
+ # Returns an array of what messages have been sent.
80
+ def messages_sent; @@messagesSent; end
81
+ end
82
+
83
+ class Message
84
+ HTML_CONTENT_TYPE = 0
85
+ MULTIPART_CONTENT_TYPE = 1
86
+
87
+ attr_accessor :body, :char_set, :content_type, :from_address, :from_name,
88
+ :subject, :to_address, :to_name
89
+
90
+ # Unless they are explicitly set using accessors, the following defaults
91
+ # apply:
92
+ # [to_name] nil
93
+ # [from_name] nil
94
+ # [content_type] nil
95
+ # [char_set] 'iso-8859-1'
96
+ def initialize( subject, to_address, from_address, body = nil )
97
+ @subject = subject
98
+ @to_address = to_address
99
+ @from_address = from_address
100
+ @char_set = 'iso-8859-1'
101
+ @body = body
102
+ end
103
+
104
+ # Sends the email.
105
+ def deliver; Mailer.get_mailer.send_email( self ); end
106
+
107
+ # Returns an array of strings describing the headers for this email
108
+ # message.
109
+ def headers
110
+ headers = []
111
+ headers << "Subject: #{@subject}"
112
+ toHeader = "To: "
113
+ if @to_name
114
+ toHeader += " #{@to_name} <#{@to_address}>"
115
+ else
116
+ toHeader += " #{@to_address}"
117
+ end
118
+ headers << toHeader
119
+ fromHeader = "From: "
120
+ if @from_name
121
+ fromHeader += "#{@from_name} <#{@from_address}>"
122
+ else
123
+ fromHeader += "#{@from_address}"
124
+ end
125
+ headers << fromHeader
126
+ if content_type == HTML_CONTENT_TYPE
127
+ headers << "Content-Type: text/html; charset=\"#{@char_set}\""
128
+ headers << "MIME-Version: 1.0"
129
+ elsif content_type == MULTIPART_CONTENT_TYPE
130
+ headers << "Content-Type: Multipart/Alternative; charset=\"#{@char_set}\""
131
+ headers << "MIME-Version: 1.0"
132
+ end
133
+ headers
134
+ end
135
+
136
+ # Emailer calls this before sending a message; subclasses can override this
137
+ # if they want to ensure that certain parts of the message are valid before
138
+ # sending.
139
+ def verify_sendable
140
+ [ :subject, :from_address, :to_address, :body ].each { |field|
141
+ if self.send( field ).nil?
142
+ raise( InvalidStateError,
143
+ "Can't send email with blank #{ field.id2name }" )
144
+ end
145
+ }
146
+ end
147
+ end
148
+
149
+ # The MockMailer can be swapped out for the Mailer in test code, and then
150
+ # queried to make sure that the right emails were sent out. To set this, call
151
+ #
152
+ # mock_mailer = RhizMail::MockMailer.new
153
+ # RhizMail::set_mailer mock_mailer
154
+ #
155
+ # Afterwards, every time you call Message#deliver it will be delivered by the
156
+ # MockMailer.
157
+ class MockMailer
158
+ attr_reader :messages_sent
159
+
160
+ def initialize; flush; end
161
+
162
+ # Clear out the history of messages sent.
163
+ def flush; @messages_sent = []; end
164
+
165
+ # Pretends to send an email.
166
+ def send_email(email)
167
+ email.verify_sendable
168
+ @messages_sent << email
169
+ end
170
+ end
171
+
172
+ # The SimpleTemplateMessage is designed to let a programmer define a simple
173
+ # set of tags for a certain sort of message and then substitute them in-code.
174
+ # It's intended to be simple enough that non-programmers can edit it without
175
+ # too much confusion.
176
+ #
177
+ # As an example, let's say you've got an email to send out whenever somebody
178
+ # signs up to your website. The template could look like this:
179
+ #
180
+ # $ cat ~/Desktop/template.txt
181
+ # Subject: Thanks for joining Website.com!
182
+ #
183
+ # Hi! Thanks for joining Website.com. For your reference, here's your signup
184
+ # information:
185
+ #
186
+ # Email: <% email %>
187
+ # Password: <% password %>
188
+ #
189
+ # Thanks!
190
+ #
191
+ # Note that the first line needs to be in the format "Subject: [ SUBJECT ]",
192
+ # and should be followed by a blank line.
193
+ #
194
+ # Then you create an instance of SimpleTemplateMessage and use
195
+ # SimpleTemplateMessage#substitute to change the :
196
+ #
197
+ #
198
+
199
+ pointing to that file
200
+ class SimpleTemplateMessage < Message
201
+ def initialize( to_address, from_address, template_file )
202
+ template = ''
203
+ MockFS.get_file.open( template_file ) { |file|
204
+ template = file.gets nil
205
+ }
206
+ template =~ /Subject: (.*)/
207
+ subject = $1
208
+ super( subject, to_address, from_address )
209
+ set_body( template )
210
+ end
211
+
212
+ def get_body( template )
213
+ body = ''
214
+ blank_line_found = false
215
+ template.each { |line|
216
+ if blank_line_found
217
+ body += line
218
+ else
219
+ blank_line_found = true if line =~ /^\n/
220
+ end
221
+ }
222
+ body
223
+ end
224
+
225
+ def get_regexp( token ); Regexp.new( "<%\s*#{ token }\s*%>", true ); end
226
+
227
+ def get_substitions; {}; end
228
+
229
+ def set_body( template )
230
+ @body = get_body( template )
231
+ tokens = @body.scan(/<%\s*(\S*)\s*%>/).collect { |matchArray|
232
+ matchArray[0]
233
+ }
234
+ substitutions = get_substitions
235
+ tokens.each { |token|
236
+ if ( proc = substitutions[token] ); substitute( token, proc ); end
237
+ }
238
+ end
239
+
240
+ def substitute(token, value_or_proc )
241
+ regexp = get_regexp( token )
242
+ @body.gsub!( regexp ){ |match|
243
+ value_or_proc.class <= Proc ? value_or_proc.call : value_or_proc
244
+ }
245
+ end
246
+
247
+ def verify_sendable
248
+ super
249
+ if @body =~ /<%/ || @body =~ /%>/
250
+ raise InvalidStateError, "substitution failed: #{ @body }", caller
251
+ elsif @subject =~/<%/ || @subject =~ /%>/
252
+ raise( InvalidStateError,
253
+ "substitution failed with subject: #{ @subject }",
254
+ caller )
255
+ elsif @body == ''
256
+ raise( InvalidStateError, "can't send email with blank body", caller )
257
+ end
258
+ end
259
+ end
260
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.6
3
+ specification_version: 1
4
+ name: rhizmail
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2005-03-19
8
+ summary: RhizMail is a test-friendly library for sending out customized emails.
9
+ require_paths:
10
+ - lib
11
+ email: sera@fhwang.net
12
+ homepage: http://rhizmail.rubyforge.org/
13
+ rubyforge_project:
14
+ description: "RhizMail is a test-friendly library for sending out customized emails. This is
15
+ the library we use day-to-day at http://rhizome.org, where we send out more than
16
+ 100 customized emails a day"
17
+ autorequire: rhizmail
18
+ default_executable:
19
+ bindir: bin
20
+ has_rdoc: false
21
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
22
+ requirements:
23
+ -
24
+ - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.0
27
+ version:
28
+ platform: ruby
29
+ authors:
30
+ - Francis Hwang
31
+ files:
32
+ - lib/rhizmail.rb
33
+ - lib/rhizmail.rb~
34
+ test_files: []
35
+ rdoc_options: []
36
+ extra_rdoc_files: []
37
+ executables: []
38
+ extensions: []
39
+ requirements: []
40
+ dependencies:
41
+ - !ruby/object:Gem::Dependency
42
+ name: lafcadio
43
+ version_requirement:
44
+ version_requirements: !ruby/object:Gem::Version::Requirement
45
+ requirements:
46
+ -
47
+ - ">"
48
+ - !ruby/object:Gem::Version
49
+ version: 0.0.0
50
+ version:
51
+ - !ruby/object:Gem::Dependency
52
+ name: mockfs
53
+ version_requirement:
54
+ version_requirements: !ruby/object:Gem::Version::Requirement
55
+ requirements:
56
+ -
57
+ - ">"
58
+ - !ruby/object:Gem::Version
59
+ version: 0.0.0
60
+ version: