Grails:当 SMTP 服务器暂时关闭时如何缓冲出站电子邮件?

发布于 2024-10-21 17:30:15 字数 629 浏览 2 评论 0原文

Grails 使用 Spring 的 mailService。该服务是同步的,这意味着如果 SMTP 暂时中断,应用程序功能会受到严重影响 (HTTP 500)。

我想将应用程序与 SMTP 服务器分离。

该计划是将准备发送的电子邮件保存到出站队列中,并通过计时器发送它们,并重试。对于我自己的代码,当我直接调用 mailService 时,这是相当简单的 - 创建一个包装服务并调用它。但是我的应用程序使用的一些插件(例如 EmailConfirmation 插件)使用相同的 mailService,但仍然失败,例如有效地阻止了注册过程。

我想知道如何替换/包装 mailService 的定义以使所有代码(我自己的代码和插件)透明地使用我自己的服务?

  • 插件代码注入mailService,
  • 但不是Spring默认的mailService,我自己的代码被注入
  • 当插件发送电子邮件时,电子邮件对象被保存到数据库,而不是
  • 在计时器上,作业醒来,获取接下来的N封电子邮件并尝试发送它们

任何想法如何处理这个问题?

PS 我知道 AsynchronousMail 插件。不幸的是,它的服务必须显式调用,即它不是 mailService 的直接替代品。

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 instance.

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

热风软妹 2024-10-28 17:30:15

一个简单的解决方案是使用本地安装的邮件服务器。有众所周知的、成熟的 MTA,如 Postfix、Sendmail 或 Exim,以及轻量级替代品,如 http://packages.qa.debian.org/s/ssmtp.html

配置使用的 MTA 包以将其所有电子邮件中继到您所在域的真实 SMTP 服务器。然后,Grails 应用程序将简单地使用 127.0.0.1 作为 SMTP 主机。

这还具有提高应用程序响应时间的优点,因为电子邮件发送不再首先需要任何非本地 IP 流量。

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.

亣腦蒛氧 2024-10-28 17:30:15

异步邮件插件现在支持覆盖邮件插件
只需添加

asynchronous.mail.override=true

到您的配置中即可。请参阅http://grails.org/plugin/asynchronous-mail

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

执笏见 2024-10-28 17:30:15

好吧,毕竟这并不难。首先简单的步骤:

第一步:准备数据库表来存储待处理电子邮件记录:

class PendingEmail {
    Date sentAt = new Date()
    String fileName

    static constraints = {
        sentAt nullable: false
        fileName nullable: false, blank:false
    }
}

第二步:创建定期任务来发送待处理电子邮件。注意 mailSender 注入 - 它是原始 Grails 邮件插件的一部分,因此发送(及其配置!)是通过邮件插件进行的:

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);
            }
        }
    }
}

第三步:创建一个 drop-替换可由任何 Grails 代码(包括插件)使用的 mailService。注意 mmbf 注入:这是来自 Mail Plugin 的 mailMessageBuilderFactory。该服务使用工厂将传入的 Closure 调用序列化为有效的 MIME 消息,然后将其保存到文件系统:

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
    }
}

第四步:用修改后的版本替换 Mail Plugin 的 mailService,将其注入工厂。在 grails-app/conf/spring/resources.groovy 中:

beans = {
    mailService(MyMailService) {
        mmbf = ref("mailMessageBuilderFactory")
    }
}

完成!

从现在开始,任何使用/注入 mailService 的插件或 Grails 代码都将获得对 MyMailService 的引用。该服务将接受发送电子邮件的请求,但它不会发送电子邮件,而是将其序列化到磁盘上,并将记录保存到数据库中。定期任务将每 30 秒加载一堆此类记录,并尝试使用原始邮件插件服务发送它们。

我已经测试过,它似乎工作正常。我需要到处进行清理,在发送周围添加事务范围,使参数可配置等等,但框架已经是一个可行的代码。

希望对某人有帮助。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文