Last updated by jkuehn 5 years ago

Proposal for Email Integration in Grails

Spring Mail

Default implementation for email integration will use Spring framework's JavaMailSender implementation. However, we should create test or development mail senders for environments that are not set to send email. A test mail sender will dump emails sent ( and headers ) into a text file or database for easy viewing.

Configuration

Configuration should be done similar to data sources, with XxxMailSender.groovy files under the grails-app/conf directory.

class ProductionMailSender {
  String host="localhost"
  String username="user"
  String password="pass"
}
TestMailSender may look like this:
class TestMailSender {
  String mailSender="org.codehaus.groovy.grails.commons.TestMailSender"
  String directory="/WEB-INF/emails/"
}

Lifecycle

When application is loading mail sender will be placed in the Spring context under 'mailSender', where it will be available to the grails framework.

MVC Architecture

Since email is really a view component, emails should be handled in an MVC way. We do not want to have code similar to the following in our controllers:

class SomeController {

def sendEmail = {

… def mailSender = appContext.getBean( "mailSender" ) def message = new SimpleMailMessage()

// Set properties message.to = 'user@domain.com' message.subject = 'subject'

def body = "Dear ${session.user}," body = body + "rn You have a new message" body = body + " to view this message go here." body = body + "rnhttp://domain.com/send/email"

message.body = body

mailSender.send( message )

}

}

Instead we should have something similar to the following:
class SomeController {

SomeMailer someMailer

def sendEmail = {

… def message = someMailer.createMessage(user: session.user) message.to = 'user@domain.com' someMailer.sendMessageNotification( message )

}

}

// views directory - messageNotification.gsp Dear ${session.user}, You have a new message to view this message go here. http://domain.com/send/email/${session.user.id}

The properties to, cc, bcc and from of the message object should be able to take either a Map of email-address:name pairs, a List of email addresses or just a singular email address.

Here we use a convention based approached combined with Spring dependency injection.

  • First the required mailer is injected into the controller.
  • Then a message is created using the createMessage method of the mailer that takes the model
  • We then invoke the send method on the someMailer. The send method uses the latter part of its name, in this case "MessageNotification" to map to delegate to the appropriate view that contains the e-mail message (in this case messageNotification.gsp)

Mailer(s) - The alternative approach.

_The difference is that we don't build an email in the controller or service. We delegate the building of the email to a closure within the Mailer class. This approach keeps your code very clean._

A mailer prepares an email for sending. They set the recipients, attach any files, build or generate the body(s). To create a mailer you simply create a class whose name ends with "Mailer" and place it within the "grails-app/mailers" directory.

Mailers are injectable into other classes such as controllers, services and jobs.

Sending emails

An mailer can have multiple closure properties. Each of these properties maps to a different email you may wish to build:

class OrderMailer {
  def conformation {
    to = ["smith@example.com":"Smith"]
    subject = "Order Conformation"

body(text:"Text Message" ) body(html:"<H1>HTML Message</H1>" )

attach(url:"/path/to/file.pdf") } }

To use this mailer from a controller or service we would inject it into the object like this:

OrderMailer orderMailer

Then to send the conformation email we would call the property name of the closure prefixed with send. This would look like this:

orderMailer.sendConformation()

We could pass parameters to the email just as you would with any closure to build a more dynamic email.

class OrderMailer {
  def conformation {order |
    to = order.email:order.name
    subject = "Order Comformation for ${order.name}"

body """Dear ${order.name}, Your orders coming. Thanks""" } }

class OrderService { OrderMailer orderMailer

def void processOrder(Order order) {

// Process the order..

// Send conformation email. orderMailer.sendConformation(order) } }

The properties to, cc, bcc and from of the message object should be able to take either a Map of email-address:name pairs, a List of email addresses or just a singular email address.

to = [test@example.com:"Test",webmaster@yahoo.com:"webmaster"]
  cc = "test@example.com"
  bcc = ["test@example.com":"Test"]

Behind the scenes when the send method is called the email is built and the actual sending to of this email is delegated to the mailSender configured for your environment.

Email Properties

  • to (optional) - The recipient(s). String, Map or List
  • cc (optional) - The carbon copy recipient(s). String, Map or List
  • bcc (optional) - The blind carbon copy recipient(s). String, Map or List
  • subject (optional) - The email subject
  • headers (optional) - Map of additional header information
  • from (optional) - The from address. String or Map
  • bounce (optional) - The bounce address
  • replyTo (optional) - the reply to address. String or Map
_It would be nice if default values for headers, from and bounce could be set by the current mailSender but overridden at an email scope._

Email Dynamic Methods

body

Description

A multi-purpose method for rendering the body of emails. This can be called multiple times to add bodies of different content type. LIke the render method on controllers it is best illustrated with a few examples!

Parameters
  • text (optional) - The text to render in the body
  • html (optional) - The html to render in the body. Sets contentType = text/html automagicly
  • builder (optional) - The builder to use when rendering markup
  • view (optional) - The view to delegate the rendering to
  • template (optional) - The template to render (default = name of calling closure)
  • var (optional) - The name of the variable to be passed into a template, defaults to the groovy default argument 'it' if not specified
  • bean (optional) - The beanto use in rendering
  • model (optional) - The model to use in rendering
  • collection (optional) - For rendering a template against each item in a collection
  • contentType (optional) - The contentType of the body (default = text/plain)
  • encoding (optional) - The encoding of the response
Examples

// renders text for the body of the email
body "some text"

// renders text for a specified content-type/encoding body(text:"<h1>Hello World</h1>",contentType:"text/html",encoding:"UTF-8")

// render a template for the body for the specified model body(template:"book",model:[book:new Book(title:'The Shining',author:'Stephen King')])

// render each item in the collection using the specified template body(template:"book",collection:[b1, b2, b3])

// render a template to the response for the specified bean body(template:"book",bean:new Book(title:'The Shining',author:'Stephen King'))

// render some markup to the body body { h1() { "some body text" } }

// render some HTML markup to the body body(contentType:"text/html") { books { for(b in books) { book(title:b.title,author:b.author) } } }

attach

Description

A method for adding attachments to an email.

Parameters
  • name (optional) - Name of attachment
  • url (optional) - Url to the file
  • file (optional) - The actual file
  • description (optional) Description of the attachment
  • disposition (optional) Either attachment or inline (default = attachment)

Receive Emails

Our xxxMailer can be configured to receive incoming email and interact with the domain model of the application on the basis of the content. We handle these incoming emails by defining a method called receive() that takes and Email object (Yet to define what an email object actualy consists of).

class xxxMailer {

def receive(Email email) { // Perform some action on domain object def user = User.findByEmail(email.from) }

// Method for checking your email… def checkMail() { // checking code implemented by developer. // for (message in xzyz.getMessages()) { // receive(new Email(message)); // }

} }

The purpose of the checkMail method is to check your mailbox and call the receive method for each new message downloaded. I envisage that the checkMail method would be called by creating a Quartz job and injecting into it the xxxMailer. A mail checking solution that suites everybody's needs probably isn't going to be possible but we could support the basic IMAP & Pop3 protocols that JavaMail gives us, and leave the suggestion that the developer can override this method to perform anything more elaborate.

class MyJob {

// Injected here by spring def XxxMailer xxxMailer

def cronExpression = "0 0 6 * * ?"

def execute(){ xxxMailer.checkMail() } }

Another variation/hybrid

My feeling is that we must use the MVC model for the messages. This means having controllers populate the model for the mail view, without requiring service injection, which in my view messes things up and requires controllers to know what is handling the mail of a certain type.

The Web Controller

class SomeController {

def sendEmail = {

...

mail( 'usermessages/mail', user: session.user ) // This is a "magic" method like render() mail( 'sysmessages/mail', [to: 'sysadmin@localhost', user: session.user] ) mail( 'otherfolder/customMailView', [to: 'sysadmin@localhost', user: session.user] ) mail( 'otherfolder/customMailView') { encoding( 'ISO-8859-1') to('sysadmin@localhost') to(user.emailAddress) subject( from('webserver@mydomain.com') attach( 'http://grails.org/Controllers' ) // attach contents of a URL, autodetect attach( new File( '/etc/passwd') ) // attach file }

redirect(action:mailThankYou) // prevent refresh = double post }

}

The above lets us send multiple mails easily, and lets us determine in an MVC way which mail view to use.

The builder in the last mail() example is used to configure advanced options. This could be removed if we provide GSP tags or other mechanisms (context helper) within the view template to configure the message itself, although arguably these should not be in the view.

We have:

mail( view, model)
mail( view, object)
mail( model/object) // uses 'views/controllername/mail/actionID' as view
mail( model) { builder code }
mail( view, model) { builder code }

Proposal Three: Messaging Integration

A third proposal is to not specifically bind the messaging system to e-mail, but instead have the abstract concept of publishers,subscribers and topics. This is detailed in Messaging Integration