Last updated by 5 years ago

Page: Grails Standard Mail, Version:0

Work in progress - Marc Palmer

Grails Standard Mail

We already have two documents detailing email send/receive proposals for Grails - Mail from Grails and Messaging Integration. There are good ideas in both of these, and this document is trying to pull this all together.

There are some specific objectives coming to light now that Grails is further along the development roadmap since those documents were written.

Objectives

We have to agree and whittle down the objectives for a Mail convention/API as tightly as possible to succeed. There are complex interaction issues at stake.

  1. Transport abstraction - it must be possible to provide alternative implementations of mail delivery/sending.
  2. Message payload abstraction - for (1) to be possible, the payload must use a Grails-defined API or other transport-neutral mechanism
  3. Define a Grails Mail standard - required to prevent a disaster where many different plugins require many different mail plugins. Sending/receiving mail is such a common feature that failure to pin this down in the same way Controllers are the de facto way of handling views will cause major complications for plugin users and authors
  4. No configuration - a convention based approach, ideally no configuration other than mail server host and port and auth details
  5. Ease of use - it must be trivial to send mails from within any artefact in grails, i.e. from controllers, services or jobs
  6. Concept reuse - interoperability with abstracted messaging (JMS) if this does not unnecessarily complicate Mail usage.

Recap of ideas submitted so far as relates to the objectives

Ideas that stand out so far for me and my thoughts...

Use of xxxxMailer convention to define closures (like controller actions) that "render" and send an email

This seems like a nice pattern to follow, but requires something to simulate the request dispatcher in grails, for example a dynamic "mail" method available to all artefacts.

Use of builders to construct message payload

This is a fundamental concept I think we will have to use. Using a builder to construct message payloads abstracts us from the underlying mail transport implementation (i.e. JavaMail or other). However the builder will be specific to mail so has implications for JMS interop in API terms.

A "mail" method much like the "render" method

This addresses the issue of trivial invokation of mail sending, acting like the request dispatcher that decides which controller/action to use. This would also permit some kind of message name to mailer/action mapping in the vein of Grails 0.5's URLMappings, if this might be useful.

Use of dyamic closure names on mailers to control what transport is used to send the message

In the messaging integration proposal there is reference to sendSomethingToServerName where ServerName is pulled out and resolved to an outgoing mail host configuration - but in the JMS case would resolve to a "topic" configuration.

Use of GSP or similar as views for email bodies

While not appropriate for all usages, and almost certainly not for JMS, this will be very useful and if done right could allow the reuse of the same GSP fragments to render previews of emails on the website.

Use of events to receive mails

This is a no brainer really, but I don't think the user should have to configure any jobs manually to do this - the transport implementation should handle this and use conventions on mailers or MessageSink(s) to determine poll intervals etc.

How to move forward

While we have some good ideas I feel we're still a little way off. The messaging integration proposals to me seem to over complicate mail usage, but work for JMS. I'm hoping we can rework this proposal to suit JMS and email equally.

I think we need to simplify things by splitting sending and receiving up conceptually during this planning phase, and also using concrete use cases for email to guide the approach. We should come up with a first rate solution for email and then see if we can bend it very slightly to integrate with JMS. If not, we should keep the two separate.

Sending email

I think the best bet here is for us to have a custom builder to construct the message payload, with the option to render views for the body sections. The transport to use should be configured on the mailer or in the message, and sending should be a one line call with no injection required:

class BookController {
  def submitReview = {
    def review = new Review()
    review.properties = params
    if (review.save()) {
       mail( mailer: 'reviews', 
         action: 'submitReview', 
         model: [submittedReview: review, user: session.user])
    } else { … }
  }
}

class LostUserReminderJob { def timeout = 1000*60*60*24

def execute = { // find everyone who hans't logged in in last 30 days def users = User.findAllByLastVisitLessThan( new Date() - 30)

users.each() { if (user.staleMailSendCount < 3) { mail( mailer: 'user', action: 'longTimeNoSee', model: [user: it]) } } } }

The code above will cause the following mailers and actions to be used:

class ReviewsMailer {
  static transport = "localSMTP"

def submitReview = { subject('Review submitted') from('bookreviews@mybookstore.com') cc('manager@mybookstore.com') to('reviewer@mybookstore.com') headers.priority = 'low'

// By default a GSP view "grails-views/_mailers/reviews/submitReview.gsp" // will be used to render a single body MIME part using the model supplied // The GSP will have access to the current flash scope, web request and session if any. } }

class UserMailer { // no transport property implies use the default

def longTimeNoSee = { subject( "${user.name}, where have you been?") from('someonewhocares@mybookstore.com') bcc('customercare@mybookstore.com') to(user.emailAddress)

header['X-mymookstore-staleuser-mail-counter'] = user.staleMailSendCount++

if (user.prefersPlainText) { // model will be passed in automatically body( view: "plainText.gsp", contentType: 'text/xml' ) } else { body( view: "rich.gsp", contentType: 'text/html' ) body( view: "plainText.gsp", contentType: 'text/xml' ) } } }

If we then generalize this to allow JMS and other message sending technologies, can end up with:

if (review.save()) {
       send( sender: 'reviews', 
         action: 'submitReview', 
         model: [submittedReview: review, user: session.user])
Changing "mail" and "mailer" to "send" and "sender" generalizes this trivially. All that is then needed is a generalization of the model that would work for JMS/others. We then just change xxxMailer to xxxSender to have a sender convention that works, and they are responsible for determining their own mapping.

The joy of this is that the code that calls send() does not even need to know if the target is email or some other mechanism, the xxxxSender maps it to whatever transport is required.

The mailer is configured to use the following transport configuration, which will likely need to live in the proposed (for this and other config elements) ApplicationConfig:

static localSMTPMailTransport = {
    host('localhost')
    port(25)
    authUser('server')
    authPassword('letmein')
  }

static defaultMailTransport = localSMTPMailTransport }

With send/sender abstraction in place, the transport could even be used to determine how the payload builder behaves.

Receiving email