Grails uses mailService from Spring. That service is synchronous, which means if SMTP goes down temporarily, application functioning is affected badly (HTTP 500).
I want to decouple the application from SMTP server.
The plan is to save ready-to-be-sent emails into an outbound queue and send them by timer, with retries. For my own code, when I call mailService directly, it is rather trivial - make a wrapper service and call it instead. But some of the plugins that my application uses (e.g. EmailConfirmation plugin) use the same mailService, and still fail, effectively blocking sign-up process, for ins开发者_如何转开发tance.
I wonder how can I replace/wrap the definition of mailService to make all code, my own and plugins, transparently use my own service?
I.e.
- Plugin code injects mailService
- But instead of Spring default mailService my own code is injected
- When plugin sends a email the email object is saved to DB instead
- On timer a job wakes up, gets next N emails and tries to send them
Any ideas how to approach this problem?
P.S. I know about AsynchronousMail plugin. Unfortunately, its service must be called explicitely, i.e. it is not a drop-in replacement for mailService.
A simple solution for this is using a locally installed mail server. There are the well known and full blown MTAs like Postfix, Sendmail or Exim available as well as lightweight replacements like http://packages.qa.debian.org/s/ssmtp.html.
Configure the used MTA package to relay all its emails to the real SMTP server of your domain. The Grails application would then simply use 127.0.0.1 as SMTP host.
This has also the advantage of improved response time in your application, since email sending no longer requires any non-local IP traffic in first place.
The Asynchronous mail plugin now supports overriding the mail plugin Just add
asynchronous.mail.override=true
to your config. See http://grails.org/plugin/asynchronous-mail
OK, it wasn't that hard, after all. Easy steps first:
Step one: prepare database table to store pending email records:
class PendingEmail {
Date sentAt = new Date()
String fileName
static constraints = {
sentAt nullable: false
fileName nullable: false, blank:false
}
}
Step two: create a periodic task to send the pending emails. Note mailSender
injection - it is part of original Grails Mail Plugin, so the sending (and configuration of it!) is made via Mail Plugin:
import javax.mail.internet.MimeMessage
class BackgroundEmailSenderJob {
def concurrent = false
def mailSender
static triggers = {
simple startDelay:15000l, repeatInterval: 30000l, name: "Background Email Sender"
}
def execute(context){
log.debug("sending pending emails via ${mailSender}")
// 100 at a time only
PendingEmail.list(max:100,sort:"sentAt",order:"asc").each { pe ->
// FIXME: do in transaction
try {
log.info("email ${pe.id} is to be sent")
// try to send
MimeMessage mm = mailSender.createMimeMessage(new FileInputStream(pe.fileName))
mailSender.send(mm)
// delete message
log.info("email ${pe.id} has been sent, deleting the record")
pe.delete(flush:true)
// delete file too
new File(pe.fileName).delete();
} catch( Exception ex ) {
log.error(ex);
}
}
}
}
Step three: create a drop-in replacement of mailService that could be used by any Grails code, including plugins. Note mmbf injection: this is mailMessageBuilderFactory
from Mail Plugin. The service uses the factory to serialize the incoming Closure calls into a valid MIME message, and then save it to the file system:
import java.io.File;
import org.springframework.mail.MailMessage
import org.springframework.mail.javamail.MimeMailMessage
class MyMailService {
def mmbf
MailMessage sendMail(Closure callable) {
log.info("sending mail using ${mmbf}")
if (isDisabled()) {
log.warn("No mail is going to be sent; mailing disabled")
return
}
def messageBuilder = mmbf.createBuilder(mailConfig)
callable.delegate = messageBuilder
callable.resolveStrategy = Closure.DELEGATE_FIRST
callable.call()
def m = messageBuilder.finishMessage()
if( m instanceof MimeMailMessage ) {
def fil = File.createTempFile("mail", ".mime")
log.debug("writing content to ${fil.name}")
m.mimeMessage.writeTo(new FileOutputStream(fil))
def pe = new PendingEmail(fileName: fil.absolutePath)
assert pe.save(flush:true)
log.debug("message saved for sending later: id ${pe.id}")
} else {
throw new IllegalArgumentException("expected MIME")
}
}
def getMailConfig() {
org.codehaus.groovy.grails.commons.ConfigurationHolder.config.grails.mail
}
boolean isDisabled() {
mailConfig.disabled
}
}
Step four: replace Mail Plugin's mailService with the modified version, injecting it with the factory. In grails-app/conf/spring/resources.groovy
:
beans = {
mailService(MyMailService) {
mmbf = ref("mailMessageBuilderFactory")
}
}
Done!
From now on, any plugin or Grails code that uses/injects mailService will get a reference to MyMailService. The service will take requests to send the email, but instead of sending it it will serialize it onto disk, saving a record into database. A periodic task will load a bunch of such records every 30s and try to send them using the original Mail Plugin services.
I have tested it, and it seems to work OK. I need to do cleanup here and there, add transactional scope around sending, make parameters configurable and so on, but the skeleton is a workable code already.
Hope that helps someone.
精彩评论