Monday, 11 August 2008

Sending emails from Grails apps

We're trying this on for size...

import org.springframework.mail.MailSender
import org.springframework.mail.SimpleMailMessage

class EmailService {

boolean transactional = false
MailSender mailSender
SimpleMailMessage mailMessage // A template message auto-wired from resources.groovy

boolean sendMessage(String to, String subject, String body) {
boolean result = true
try {
SimpleMailMessage message = new SimpleMailMessage(mailMessage)
message.to = [to]
message.subject = subject
message.text = body
mailSender.send(message)
} catch(Exception e) {
log.error("Unable to send message to ${to} with subject ${subject}", e)
result = false
}
return result
}
}

import org.springframework.context.MessageSourceAware
import org.codehaus.groovy.grails.web.pages.GroovyPagesTemplateEngine
import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder
import org.codehaus.groovy.grails.web.pages.GroovyPageTemplate

class NotificationService implements MessageSourceAware {

EmailService emailService
GroovyPagesTemplateEngine groovyPagesTemplateEngine
MessageSource messageSource

boolean confirmRegistration(Person person) {
String subject = getMessage('email.registration.confirmation.subject', [])
return emailService.sendMessage(person.email, subject, getEmailBody('registrationConfirmation', [person: person]))
}

boolean registrationSuccess(Person person) {
String subject = getMessage('email.registration.successful.subject', [])
return emailService.sendMessage(person.email, subject, getEmailBody('registrationSuccessful', [person: person]))
}

private String getEmailBody(String templateName, Map model) {
return render("/modules/emails/_${templateName}.gsp", model)
}

private String render(String templateUri, Map model) {
// I wonder if templates can be cached
GroovyPageTemplate template = groovyPagesTemplateEngine.createTemplate(templateUri)
Writable w = template.make(model)
StringWriter sw = new StringWriter()
PrintWriter pw = new PrintWriter(sw)
w.writeTo(pw)
return sw.toString()
}

private String getMessage(String code, List args) {
return messageSource.getMessage(code, (Object[]) args, LocaleContextHolder.locale)
}

void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource
}
}


Few gotcha's though...

1. TagLibs don't work in the GSP templates. This causes a real headache if you need to use <g:message ... /> etc.

2. I'm not sure whether the templates can be cached. It's probably expensive to keep creatating them each time.

Testing

We're using wiser to stub the SMTP server, e.g.

import org.subethamail.wiser.Wiser
import org.subethamail.wiser.WiserMessage

class EmailServiceTests extends GroovyTestCase {

Wiser wiser
EmailService emailService

void setUp() {
wiser.messages.clear()
}

void testThatICanSendAnEmail() {
assertEquals(0, wiser.messages.size())
emailService.sendMessage('roundhouse@beta.xyz.com', 'Cool beans', 'Well done, you managed to register')
assertEquals(1, wiser.messages.size())

WiserMessage message = wiser.messages.iterator().next()
assertEquals('roundhouse@beta.xyz.com', message.envelopeReceiver)
assertEquals('Cool beans', message.mimeMessage.subject)
assertEquals('Well done, you managed to register', message.mimeMessage.content)
}

void testThatEmailServiceReturnsFalseOnFailure() {
emailService.log.logger.setLevel(org.apache.log4j.Level.OFF)
wiser.stop()
assertFalse('Unexpected successful email send', emailService.sendMessage('roundhouse@beta.xyz.com', 'subject', 'message'))
wiser.start()
emailService.log.logger.setLevel(org.apache.log4j.Level.ERROR)
}
}

We also use wiser in our selenium tests by adding the following action to our test fixture controller,


def smtp = {
List messages = wiser.messages.collect { message ->
String to = message.envelopeReceiver
String from = message.envelopeSender
String subject = message.mimeMessage.subject
String content = message.mimeMessage.content
return [to: to, from: from, subject: subject, content: content]
}
Map model = getJsonModel(messages)
render(view:'index', model: model)
}


Config

It's easy to pick whether to use the real smtp server or wiser in resources.groovy

switch(GrailsUtil.environment) {
case 'prod':
mailSender(org.springframework.mail.javamail.JavaMailSenderImpl) {
host = 'mail1.xyz.com'
}
mailMessage(org.springframework.mail.SimpleMailMessage) {
from = 'info@ xyz.com'
}
break
case ['qa', 'beta']:
mailSender(org.springframework.mail.javamail.JavaMailSenderImpl) {
host = 'mail1.xyz.com'
username = 'info@beta.xyz.com'
password = 'shh'
}
mailMessage(org.springframework.mail.SimpleMailMessage) {
from = 'info@beta.xyz.com'
}
break
default:
wiser(org.subethamail.wiser.Wiser) { bean ->
bean.initMethod = 'start'
bean.destroyMethod = 'stop'
bean.lazyInit = true
port = 2500
}
mailSender(org.springframework.mail.javamail.JavaMailSenderImpl) {
host = 'localhost'
port = 2500
}
mailMessage(org.springframework.mail.SimpleMailMessage) {
from = 'info@beta. xyz.com'
}
break
}

No comments: