Thymeleaf

在 Spring 中使用 Thymeleaf 发送邮件

本文将会展示如何使用 Thymeleaf 模板来构建多种类型的电子邮件消息,并将其与 Spring 的邮件工具集成,以配置一个简单但功能强大的邮件系统。

请注意,尽管本文和相应的示例应用程序使用了 Spring 框架,但 Thymeleaf 同样可以用于没有 Spring 的应用中处理电子邮件模板。另外,请注意此示例应用是一个 Web 应用程序,但使用 Thymeleaf 发送电子邮件的应用并不需要具备 Web 功能。

前提条件

本文假设您已经熟悉 Thymeleaf 和 Spring 4。我们不会深入讲解 Spring Mail 的细节,如需更多信息,请参阅Spring 官方文档中的邮件章节.

示例应用程序

本文中的所有代码均来自一个实际运行的示例应用。您可以通过以下链接查看或下载源码:其 GitHub 仓库强烈建议下载该应用、执行并查看其源代码(请注意,您需要在src/main/resources/configuration.properties).

使用 Spring 发送邮件

首先,您需要在 Spring 配置中配置一个邮件发送器(Mail Sender)对象,如下述代码所示(您的具体配置需求可能会有所不同):

@Configuration
@PropertySource("classpath:mail/emailconfig.properties")
public class SpringMailConfig implements ApplicationContextAware, EnvironmentAware {

    private static final String JAVA_MAIL_FILE = "classpath:mail/javamail.properties";

    private ApplicationContext applicationContext;
    private Environment environment;

    ...

    @Bean
    public JavaMailSender mailSender() throws IOException {

        final JavaMailSenderImpl mailSender = new JavaMailSenderImpl();

        // Basic mail sender configuration, based on emailconfig.properties
        mailSender.setHost(this.environment.getProperty(HOST));
        mailSender.setPort(Integer.parseInt(this.environment.getProperty(PORT)));
        mailSender.setProtocol(this.environment.getProperty(PROTOCOL));
        mailSender.setUsername(this.environment.getProperty(USERNAME));
        mailSender.setPassword(this.environment.getProperty(PASSWORD));

        // JavaMail-specific mail sender configuration, based on javamail.properties
        final Properties javaMailProperties = new Properties();
        javaMailProperties.load(this.applicationContext.getResource(JAVA_MAIL_FILE).getInputStream());
        mailSender.setJavaMailProperties(javaMailProperties);

        return mailSender;

    }

    ...

}

注意,上述代码会从类路径下的属性文件中获取配置信息。mail/emailconfig.propertiesmail/javamail.properties类路径下的属性文件中获取配置信息。

Spring 提供了一个名为MimeMessageHelper的类来简化电子邮件消息的创建过程。让我们看看如何将它与我们的mailSender.

final MimeMessage mimeMessage = this.mailSender.createMimeMessage();
final MimeMessageHelper message = new MimeMessageHelper(mimeMessage, "UTF-8");
message.setFrom("sender@example.com");
message.setTo("recipient@example.com");
message.setSubject("This is the message subject");
message.setText("This is the message body");
this.mailSender.send(mimeMessage);

Thymeleaf 邮件模板结合使用。

使用 Thymeleaf 处理我们的邮件模板可以让我们利用一些有趣的功能:

  • Spring EL 表达式
  • 流程控制:迭代, 条件判断,……
  • 实用函数:日期/数字格式化、处理列表和数组等。
  • 国际化支持i18n:与我们应用程序的 Spring 国际化基础架构无缝集成。
  • 自然模板:我们的邮件模板可以是静态原型,由 UI 设计师编写。
  • 等等……

此外,由于 Thymeleaf 不依赖于 Servlet API,因此完全没有必要必须在 Web 应用中创建或发送邮件。这里介绍的技术几乎无需更改即可用于没有任何 Web 界面的独立应用程序。

我们的目标

我们的示例应用将发送五种类型的电子邮件:

  1. 文本(非 HTML)邮件。
  2. 简单 HTML 邮件(带国际化问候语)。
  3. 带附件的 HTML 邮件。
  4. 嵌入图片的 HTML 邮件。
  5. 用户编辑的 HTML 邮件。

Spring 配置

为了处理我们的模板,我们需要配置一个TemplateEngine特别为邮件处理定制的模板引擎,在我们的 Spring 邮件配置中:

@Configuration
@PropertySource("classpath:mail/emailconfig.properties")
public class SpringMailConfig implements ApplicationContextAware, EnvironmentAware {

    ...

    @Bean
    public ResourceBundleMessageSource emailMessageSource() {
        final ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("mail/MailMessages");
        return messageSource;
    }

    ...

    @Bean
    public TemplateEngine emailTemplateEngine() {
        final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        // Resolver for TEXT emails
        templateEngine.addTemplateResolver(textTemplateResolver());
        // Resolver for HTML emails (except the editable one)
        templateEngine.addTemplateResolver(htmlTemplateResolver());
        // Resolver for HTML editable emails (which will be treated as a String)
        templateEngine.addTemplateResolver(stringTemplateResolver());
        // Message source, internationalization specific to emails
        templateEngine.setTemplateEngineMessageSource(emailMessageSource());
        return templateEngine;
    }

    private ITemplateResolver textTemplateResolver() {
        final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
        templateResolver.setOrder(Integer.valueOf(1));
        templateResolver.setResolvablePatterns(Collections.singleton("text/*"));
        templateResolver.setPrefix("/mail/");
        templateResolver.setSuffix(".txt");
        templateResolver.setTemplateMode(TemplateMode.TEXT);
        templateResolver.setCharacterEncoding(EMAIL_TEMPLATE_ENCODING);
        templateResolver.setCacheable(false);
        return templateResolver;
    }

    private ITemplateResolver htmlTemplateResolver() {
        final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
        templateResolver.setOrder(Integer.valueOf(2));
        templateResolver.setResolvablePatterns(Collections.singleton("html/*"));
        templateResolver.setPrefix("/mail/");
        templateResolver.setSuffix(".html");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        templateResolver.setCharacterEncoding(EMAIL_TEMPLATE_ENCODING);
        templateResolver.setCacheable(false);
        return templateResolver;
    }

    private ITemplateResolver stringTemplateResolver() {
        final StringTemplateResolver templateResolver = new StringTemplateResolver();
        templateResolver.setOrder(Integer.valueOf(3));
        // No resolvable pattern, will simply process as a String template everything not previously matched
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(false);
        return templateResolver;
    }

    ...

}

注意我们为此邮件专用的引擎配置了三个模板解析器:一个用于 TEXT 模板,另一个用于 HTML 模板,第三个用于可编辑的 HTML 模板,我们允许用户修改这些模板,并且它们会在修改后仅作为一个String字符串传递给模板引擎。

所有三个模板解析器都按顺序排列,依次尝试匹配它们的可解析模式模板名称,仅当模板名称匹配时才进行解析。

还请注意这个TemplateEngine引擎专为邮件处理设计,并且与用于 Web 界面的引擎完全不同。用于 Web 界面的模板引擎通过TemplateEngineThymeleafViewResolverThymeleafViewResolver与 Spring MVC 集成,实际上定义在另一个继承自@Configuration的不同WebMvcConfigurerAdapter文件中(此处我们将不再展示它,以便专注于邮件处理)。

执行模板引擎

在代码的某个位置,我们需要执行模板引擎来生成邮件内容。我们选择在一个EmailService类中完成这项工作,这样可以明确表明我们认为这是业务层(而不是Web 层)。

)的责任。上下文包含我们在模板执行过程中需要用到的所有变量。鉴于我们的邮件处理不依赖于 Web,使用Context的实例就足够了:

final Context ctx = new Context(locale);
ctx.setVariable("name", recipientName);
ctx.setVariable("subscriptionDate", new Date());
ctx.setVariable("hobbies", Arrays.asList("Cinema", "Sports", "Music"));
ctx.setVariable("imageResourceName", imageResourceName); // so that we can reference it from HTML

final String htmlContent = this.templateEngine.process("html/email-inlineimage.html", ctx);

我们的email-inlineimage.html是我们用来发送嵌入图片邮件的模板文件,它的内容如下所示:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title th:remove="all">Template for HTML email with inline image</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
    <p th:text="#{greeting(${name})}">
      Hello, Peter Static!
    </p>
    <p th:if="${name.length() > 10}">
      Wow! You've got a long name (more than 10 chars)!
    </p>
    <p>
      You have been successfully subscribed to the <b>Fake newsletter</b> on
      <span th:text="${#dates.format(subscriptionDate)}">28-12-2012</span>
    </p>
    <p>Your hobbies are:</p>
    <ul th:remove="all-but-first">
      <li th:each="hobby : ${hobbies}" th:text="${hobby}">Reading</li>
      <li>Writing</li>
      <li>Bowling</li>
    </ul>
    <p>
      You can find <b>your inlined image</b> just below this text.
    </p>
    <p>
      <img src="sample.png" th:src="|cid:${imageResourceName}|" />
    </p>
    <p>
      Regards, <br />
      <em>The Thymeleaf Team</em>
    </p>
  </body>
</html>

让我们强调几点:

  • 上述模板完全支持所见即所得;您可以直接在浏览器中打开它查看效果。这比发送邮件来预览效果要方便得多,不是吗?
Image inlined in email
邮件中嵌入的图片
  • 我们可以使用所有的 Thymeleaf 功能。例如在这里,我们使用了带有参数表达式的国际化#{...}表达式th:each来遍历一个列表,#dates来格式化日期……
  • 这个img元素有一个硬编码的src值——这对于原型开发非常友好——在运行时会被类似cid:image.jpg的值替换,对应附加图片的文件名。

文本(非 HTML)邮件

那么文本邮件呢?我们已经为文本邮件模板配置了一个模板解析器,所以我们只需创建一个使用 Thymeleaf 文本语法的模板,就像这样:

[( #{greeting(${name})} )]

[# th:if="${name.length() gt 10}"]Wow! You've got a long name (more than 10 chars)![/]

You have been successfully subscribed to the Fake newsletter on [( ${#dates.format(subscriptionDate)} )].

Your hobbies are:
[# th:each="hobby : ${hobbies}"]
 - [( ${hobby} )]
[/]

Regards,
    The Thymeleaf Team

整合所有内容

服务类

最后,让我们看看在我们的EmailService服务类中执行此邮件模板的方法会是什么样子:

public void sendMailWithInline(
  final String recipientName, final String recipientEmail, final String imageResourceName,
  final byte[] imageBytes, final String imageContentType, final Locale locale)
  throws MessagingException {

    // Prepare the evaluation context
    final Context ctx = new Context(locale);
    ctx.setVariable("name", recipientName);
    ctx.setVariable("subscriptionDate", new Date());
    ctx.setVariable("hobbies", Arrays.asList("Cinema", "Sports", "Music"));
    ctx.setVariable("imageResourceName", imageResourceName); // so that we can reference it from HTML

    // Prepare message using a Spring helper
    final MimeMessage mimeMessage = this.mailSender.createMimeMessage();
    final MimeMessageHelper message =
        new MimeMessageHelper(mimeMessage, true, "UTF-8"); // true = multipart
    message.setSubject("Example HTML email with inline image");
    message.setFrom("thymeleaf@example.com");
    message.setTo(recipientEmail);

    // Create the HTML body using Thymeleaf
    final String htmlContent = this.templateEngine.process("email-inlineimage.html", ctx);
    message.setText(htmlContent, true); // true = isHtml

    // Add the inline image, referenced from the HTML code as "cid:${imageResourceName}"
    final InputStreamSource imageSource = new ByteArrayResource(imageBytes);
    message.addInline(imageResourceName, imageSource, imageContentType);

    // Send mail
    this.mailSender.send(mimeMessage);

}

注意我们使用了org.springframework.core.io.ByteArrayResource对象来附加用户上传的图片,这是我们之前将其转换为byte[].

你还可以利用FileSystemResource从文件系统直接附加文件——从而避免将其加载到内存中——或者UrlResource附加远程文件。

控制器

现在来看调用我们服务的控制器方法:

/*
* Send HTML mail with inline image
*/
@RequestMapping(value = "/sendMailWithInlineImage", method = RequestMethod.POST)
public String sendMailWithInline(
  @RequestParam("recipientName") final String recipientName,
  @RequestParam("recipientEmail") final String recipientEmail,
  @RequestParam("image") final MultipartFile image,
  final Locale locale)
  throws MessagingException, IOException {

    this.emailService.sendMailWithInline(
        recipientName, recipientEmail, image.getName(),
        image.getBytes(), image.getContentType(), locale);
    return "redirect:sent.html";

}

再简单不过了。注意我们是如何使用 Spring MVC 的MultipartFile对象来建模上传的文件,并将其内容传递给服务的。

更多示例

为了简洁起见,我们只详细说明了我们的应用程序能够发送的五种电子邮件类型中的一种。但是,你可以在springmail你可以从以下位置下载的示例应用程序中看到创建所有五种电子邮件类型所需的源代码文档页面.

无噪 Logo
无噪文档
中文文档 · 复刻官网
查看所有 ↗