Class: RhizMail::SimpleTemplateMessage

Inherits:
Message
  • Object
show all
Defined in:
lib/rhizmail.rb

Overview

The SimpleTemplateMessage is designed to let a programmer define a simple set of tags for a certain sort of message and then substitute them in-code. It’s intended to be simple enough that non-programmers can edit it without too much confusion.

As an example, let’s say you’ve got an email to send out whenever somebody signs up to your website. The template could look like this:

$ cat /Users/francis/Desktop/template.txt 
Subject: Thanks for joining Website.com!

Hi! Thanks for joining Website.com. For your reference, here's your signup
information:

Email: <% email %>
Password: <% password %>

Thanks!

The Subject and From information can be set explicitly in the template, with lines like:

Subject: Thanks for joining Website.com!
From: "Bill Smith" <[email protected]>

These header lines need to be at the top of the template, and they should be followed by a blank line dividing them from the template body. Header lines are optional; you can leave them and set them explicitly using SimpleTemplateMessage#subject=, SimpleTemplateMessage#from_name=, and SimpleTemplateMessage#from_address= instead.

Then you create an instance of SimpleTemplateMessage and use SimpleTemplateMessage#substitute to change the contents:

msg = RhizMail::SimpleTemplateMessage.new(
  :to_address => '[email protected]',
  :from_address => '[email protected]',
  :template_file => '/Users/francis/Desktop/template.txt'
)
msg.substitute( 'email', '[email protected]' )
msg.substitute( 'password', 'p4ssw0rd' )
puts msg.body

Which prints this:

Hi! Thanks for joining Website.com. For your reference, here's your signup
information:

Email: [email protected]
Password: p4ssw0rd

Thanks!

This is the sort of email you’re likely to send out a lot, so you can encapsulate a lot of specific information when you define a subclass:

class IntroEmail < RhizMail::SimpleTemplateMessage
  from_address '[email protected]'
  template     '/Users/francis/Desktop/template.txt'
  substitute   'email', Proc.new { |msg| msg.user.email }
  substitute   'password', Proc.new { |msg| msg.user.password }

  attr_reader :user

  def initialize( user )
    @user = user
    super( :to_address => user.email )
  end
end

User = Struct.new( :email, :password )

u = User.new( '[email protected]', 'p4ssw0rd' )
email = IntroEmail.new u

This accomplishes the same thing.

Note that a Proc passed to SimpleTemplateMessage.substitute needs to take the message itself as a parameter; this is because the Proc belongs to the class, and doesn’t know which instance to use unless it’s specified. Don’t forget to use attr_reader to give the Proc access to the message variables it needs.

A child of SimpleTemplateMessage should call super at the end of its initialize method; this will automatically take care of the defined substitutions.

If you call SimpleTemplateMessage#deliver without doing all the substitutions required by the template, it will raise an InvalidStateError. (Getting an exception is probably better than sending somebody an email with funny symbols in it.)

Defined Under Namespace

Classes: SubclassAttributes

Constant Summary collapse

@@subclass_attributes =
Hash.new { |hash, key|
	hash[key] = SubclassAttributes.new( {} )
}

Instance Attribute Summary

Attributes inherited from Message

#charset, #content_type, #from_address, #from_name, #subject, #to_address, #to_name

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Message

#deliver, #from_header, #headers, #to_header

Constructor Details

#initialize(*args) ⇒ SimpleTemplateMessage

Pass a hash to SimpleTemplateMessage.new to create it:

msg = SimpleTemplateMessage.new(
  :to_address => '[email protected]',
  :from_address => '[email protected]',
  :template_file => '/Users/francis/Desktop/template.txt'
)

:template_file is a required key. Other valid keys are :from_address, :from_name, :to_address, :to_name.

Defaults for :template_file and :from_address can be set with SimpleTemplateMessage.template and SimpleTemplateMessage.from_address.

By default, SimpleTemplateMessage#body wraps to 72 columns. You can change this behavior by passing :body_wrap in through the hash; pass false to turn off wrapping, any other number to set the number of columns to wrap to.

An older, deprecated style of creation is to take specific parameters:

msg = SimpleTemplateMessage.new(
  '[email protected]', '[email protected]',
  '/Users/francis/Desktop/template.txt'
)


421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/rhizmail.rb', line 421

def initialize( *args )
	if args.first.is_a? Hash
		h = args.first.clone
		initialize_from_hash h
		super h
	else
		to_address, from_address, template_file = args
		read_template template_file
		super( @subject, to_address, from_address )
	end
	generate_body @template
end

Class Method Details

.attributesObject

:nodoc:



362
363
364
# File 'lib/rhizmail.rb', line 362

def self.attributes # :nodoc:
	@@subclass_attributes[self]
end

.from_address(from_address) ⇒ Object

Sets the from_address of every message of this class; use this to parameterize children of SimpleTemplateMessage.



368
369
370
# File 'lib/rhizmail.rb', line 368

def self.from_address( from_address )
	attributes.from_address = from_address
end

.substitute(token, proc) ⇒ Object

Adds a substitution to use on every message of this class. Use this to parameterize children of SimpleTemplateMessage, like so:

class IntroEmail < RhizMail::SimpleTemplateMessage
  substitute   'email', Proc.new { |msg| msg.user.email }
  substitute   'password', Proc.new { |msg| msg.user.password }
  ...
end

Note that a Proc passed to SimpleTemplateMessage.substitute needs to take the message itself as a parameter; this is because the Proc belongs to the class, and doesn’t know which instance to use unless it’s specified. Don’t forget to use attr_reader to give the Proc access to the message variables it needs.



392
393
394
# File 'lib/rhizmail.rb', line 392

def self.substitute( token, proc )
	attributes.substitutions[token] = proc
end

.template(template_file) ⇒ Object

Sets the template for every message of this class; use this to parameterize children of SimpleTemplateMessage.



374
375
376
# File 'lib/rhizmail.rb', line 374

def self.template( template_file )
	attributes.template = template_file
end

Instance Method Details

#bodyObject



434
435
436
437
438
439
440
441
442
# File 'lib/rhizmail.rb', line 434

def body
	if @body_wrap or @body_wrap.nil?
		format = Text::Format.new
		format.columns = @body_wrap if @body_wrap
		format.first_indent = 0
		@body = format.paragraphs @body
	end
	@body
end

#body_template(template) ⇒ Object

:nodoc:



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/rhizmail.rb', line 444

def body_template( template ) # :nodoc:
	body = ''
	blank_line_found = false
	headers_included = true
	template.each { |line|
		if blank_line_found or !headers_included
			body += line
		elsif line =~ /: /
			blank_line_found = true if line =~ /^\n/
		else
			headers_included = false
			body += line
		end
	}
	body
end

#class_attributesObject

:nodoc:



461
462
463
# File 'lib/rhizmail.rb', line 461

def class_attributes # :nodoc:
	self.class.attributes
end

#generate_body(template) ⇒ Object

:nodoc:



465
466
467
468
469
470
471
472
473
474
475
476
477
# File 'lib/rhizmail.rb', line 465

def generate_body( template ) # :nodoc:
	@body = body_template( template )
	tokens = @body.scan(/<%\s*(\S*)\s*%>/).collect { |matchArray|
		matchArray[0]
	}
	tokens.each { |token|
		if ( proc = substitutions[token] )
			substitute( token, proc )
		elsif ( proc = class_attributes.substitutions[token] )
			substitute( token, proc )
		end
	}
end

#initialize_from_hash(h) ⇒ Object

:nodoc:

Raises:



479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/rhizmail.rb', line 479

def initialize_from_hash( h ) # :nodoc:
	verboten_keys = [ :body, :charset, :content_type, :subject ]
	raise InvalidStateError unless ( h.keys & verboten_keys ).empty?
	template_file = template_file_from_hash h
	raise InvalidStateError if template_file.nil?
	read_template template_file
	if class_attributes.from_address and !h[:from_address]
		h[:from_address] = class_attributes.from_address
	end
	unless h[:body_wrap].nil?
		@body_wrap = h.delete :body_wrap
	end
end

#read_template(template_file) ⇒ Object

:nodoc:



493
494
495
496
497
498
499
500
501
502
# File 'lib/rhizmail.rb', line 493

def read_template( template_file ) # :nodoc:
	@template = ''
	MockFS.file.open( template_file ) { |file| @template = file.gets nil }
	@template =~ /Subject: (.*)/
	@subject = ( $1 or '' )
	if @template =~ /^From: "(.*?)" \<(.*?)\>$/m
		self.from_name = $1
		self.from_address = $2
	end
end

#substitute(token, value_or_proc) ⇒ Object

Substitute a token with a value in the email body.



505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'lib/rhizmail.rb', line 505

def substitute(token, value_or_proc )
	regexp = Regexp.new( "<%\s*#{ token }\s*%>", true )
	@body.gsub!( regexp ){ |match|
		if value_or_proc.is_a? Proc
			if value_or_proc.arity == 1
				value_or_proc.call self
			else
				value_or_proc.call
			end
		else
			value_or_proc
		end
	}
end

#substitutionsObject

If you want to define a subclass of SimpleTemplateMessage, you can override substitutions to automatically define a set of substitutions to make when you create a new instance. substitutions should always return a hash of tokens to either values or Procs.



524
# File 'lib/rhizmail.rb', line 524

def substitutions; {}; end

#template_file_from_hash(h) ⇒ Object

:nodoc:



526
527
528
529
530
531
532
# File 'lib/rhizmail.rb', line 526

def template_file_from_hash( h ) # :nodoc:
	if h[:template_file]
		h.delete :template_file
	else
		template_file = class_attributes.template
	end
end

#verify_sendableObject

Mailer will call this before sending this message; this will fail if any tokens are left unsubstituted.



536
537
538
539
540
541
542
543
544
545
546
547
# File 'lib/rhizmail.rb', line 536

def verify_sendable
	super
	if @body =~ /<%/ || @body =~ /%>/
		raise InvalidStateError, "substitution failed: #{ @body }", caller
	elsif @subject =~/<%/ || @subject =~ /%>/
		raise( InvalidStateError,
					 "substitution failed with subject: #{ @subject }",
					 caller )
	elsif @body == ''
		raise( InvalidStateError, "can't send email with blank body", caller )
	end
end