rhizmail 0.1.0

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