Sunday, September 3, 2017

Spring Boot - Example Email with Thymeleaf HTML Templates

I have a Spring Boot backend application that needed to send emails.

Java application email libraries include: Java mail native, Spring, and apache email (and others).  Lots of articles say apache email API is more intuitive, but others say if you are already using a Java/Spring application, then to go ahead and use the Spring one.   I decided to try the Spring Boot one first, since we have a Java/Spring application.

The Spring boot library satisfied the three criteria below, and seems to work great on my local environment:
  1. the ability to send simple text email 
  2. the ability to send HTML email (support for HTML emails already had the Thymeleaf HTML templating library provided with Spring Boot)
  3. the ability to attach a file(s) to the email
The application is using the "Starters" spring boot jars (i.e. spring-boot-starter-web, spring-boot-starter-data-jpa, spring-boot-starter-security, etc), so I just needed to make the application aware of the spring-boot-starter-thymeleaf jar file for the application to use it.

Environment:
Windows 7
Oracle Java 1.8.0_66
Spring Boot 1.3.1.RELEASE
Sprint Boot 1.3.1.RELEASE spring-boot-starter-thymeleaf

The example below supports criteria number 2 to send HTML emails using Thymeleaf email templates.  I've also included unescaped HTML, passing multiple variables to HTML template, and inline image in the example below as well.


src/main/resources/templates/logo_cherryshoe.png: cherryshoe logo to put as inline image in HTML email

src/main/resources/application.properties:  add applicable properties
# Email
spring.mail.host=<email smtp host>
spring.mail.port=<email smtp port>
spring.mail.defaultEncoding=UTF-8
spring.mail.properties.mail.smtp.connecttimeout=5000
spring.mail.properties.mail.smtp.timeout=3000
spring.mail.properties.mail.smtp.writetimeout=5000
cherryshoe.mail.defaultFrom=<email>
# must match thyme html template name under templates resource folder
cherryshoe.mail.defaultMailTemplate=email

src/main/resources/templates/email.html: Thyme HTML Email template, used as the default email template
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head><title>CherryShoe Html Email Example</title>
<style>
body {
    background-color: white;
}

/* White space is preserved with \n\n\n\n and css class a, span, tr { white-space: pre; } */
a, span, tr { white-space: pre; }

body {
 color: black;
 font-family: 'Arial', sans-serif;
 font-weight: normal;
 font-style: normal;
 font-size: x-small;
}

table {
    border-collapse: collapse;
    width: 100%;
}

table, th, td {
    border: 1px solid black;
}

.ten {
 width: 10%;
}
.forty {
 width: 40%;
}

.twenty-five {
 width: 25%;
}

th {
    background-color: #C6DEFF;
    color: black;
    vertical-align: middle;
}

th, td {
    padding: 1px;
    text-align: left;
    vertical-align: middle;
}

.cherryshoe-font-bold {
 color: black;
 font-family: 'Arial', sans-serif;
 font-weight: bold;
 font-style: normal;
 font-size: x-small;
}

</style>
</head>
<body>
Hi <span th:text="${name}"></span>, <br></br><br></br>

<!-- Need br formatted this way for SAX Parser to parse with no errors -->
<br></br><br></br>
<span class="cherryshoe-font-bold">Example Table Data generated from backend service</span>
<table>
<tr>
<th class="twenty-five">Column 1</th>
<th class="ten">Column 2</th>
<th class="twenty-five">Column 3</th>
<th class="forty">Column 4</th>
</tr>
<!-- Use th:utext to show unescaped html -->
<span th:utext="${table1}"></span>
</table>

<br></br><br></br>
Thank you
<br></br>
<img src="logo_cherryshoe.png" th:src="'cid:' + ${imageResourceName}" alt="CherryShoe Logo"/>

</body>
</html>


src/main/java/com/cherryshoe.service.email.SendEmailService.java
package com.cherryshoe.service.email;

import java.io.IOException;

public interface SendEmailService {

 /**
  * Send html emails.
  *
  * @throws IOException
  */
 public void sendHtmlEmail() throws IOException;
}


src/main/java/com/cherryshoe.service.email.SendEmailServiceSpringBootImpl
package com.cherryshoe.service.email;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@Service
/*
 * White space is preserved in text email with \n. It is also preserved in HTML
 * email with the css in the html template
 */
public class SendEmailServiceSpringBootImpl implements SendEmailService {

 // JavaMailSenderImpl is thread safe after it's constructed
 @Autowired
 private JavaMailSender mailSender;

 // needed for HTML email templating
 @Autowired
 private TemplateEngine templateEngine;

 @Value("${cherryshoe.mail.defaultFrom}")
 private String defaultFrom;

 // The default configuration of Thymeleaf expects that all HTML files are
 // placed under resources/templates directory and ends with the .html
 // extension.
 // Can also externalize the templates/*.html files
 @Value("${cherryshoe.mail.defaultMailTemplate}")
 private String defaultMailTemplate;

 private Logger log = LoggerFactory.getLogger(getClass());

 private final ThreadLocal<DateFormat> threadLocalDf = new ThreadLocal<DateFormat>() {
  @Override
  protected DateFormat initialValue() {
   return new SimpleDateFormat("MM/dd/yyyy");
  }
 };

 @Override
 public void sendHtmlEmail() throws IOException {
  MimeMessage mail = mailSender.createMimeMessage();
  InputStream imageIs = null;
  try {

   // set multiple context variables key/value pairs for email template
   String templateMailBodyNameKey = "name";
   String templateMailBodyNameVal = "[Name]";
   String templateMailBodyTable1Key = "table1";
   StringBuffer templateMailBodyTable1Val = new StringBuffer();
   String templateMailBodyImageKey = "imageResourceName";
   String templateMailBodyImageVal = "logo_cherryshoe.png";

   // table 1 get data
   for (int i = 0; i < 5; i++) {
    templateMailBodyTable1Val.append("<tr><td class=\"twenty-five\">Column 1 Data - ");
    templateMailBodyTable1Val.append(i);
    templateMailBodyTable1Val.append("</td>");
    templateMailBodyTable1Val.append("<td class=\"ten\">Column 2 Data -  ");
    templateMailBodyTable1Val.append(i);
    templateMailBodyTable1Val.append("</td>");
    templateMailBodyTable1Val.append("<td class=\"twenty-five\">Column 3 Data - ");
    templateMailBodyTable1Val.append(i);
    templateMailBodyTable1Val.append("</td>");
    templateMailBodyTable1Val.append("<td class=\"forty\">Column 4 Data - ");
    templateMailBodyTable1Val.append(i);
    templateMailBodyTable1Val.append("</td></tr>");
   }

   Map<String, Object> variables = new HashMap<String, Object>();
   variables.put(templateMailBodyNameKey, templateMailBodyNameVal);
   variables.put(templateMailBodyTable1Key, templateMailBodyTable1Val.toString());
   variables.put(templateMailBodyImageKey, templateMailBodyImageVal);

   Context context = new Context();
   variables.forEach((name, value) -> context.setVariable(name, value));

   String content = templateEngine.process(defaultMailTemplate, context);

   MimeMessageHelper helper = new MimeMessageHelper(mail, true);
   helper.setTo(defaultFrom);
   helper.setReplyTo(defaultFrom);
   helper.setFrom(defaultFrom);
   helper.setSubject("CherryShoe Example Html Email - " + threadLocalDf.get().format(new Date()));
   helper.setText(content, true); // make html email

   // Add the inline image, must go after setText. Referenced from the
   // mail template as "cid:${imageResourceName}"
   imageIs = this.getClass().getClassLoader().getResourceAsStream("templates/" + templateMailBodyImageVal);
   byte[] imageByteArray = IOUtils.toByteArray(imageIs);

   final InputStreamSource imageSource = new ByteArrayResource(imageByteArray);
   helper.addInline(templateMailBodyImageVal, imageSource, "image/png");

   mailSender.send(mail);
  } catch (MessagingException e) {
   log.warn(e.getMessage());
  } finally {
   if (imageIs != null)
    imageIs.close();
  }

 }
}

src/main/test/com/cherryshoe/email/service.SendEmailServiceITTest
package com.cherryshoe.service.email;

import javax.inject.Named;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.transaction.annotation.Transactional;
import com.cherryshoe.CherryShoeAppApplication;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = CherryShoeAppApplication.class)
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class })
@Transactional
@WebAppConfiguration
public class SendEmailServiceITTest {

 @Autowired
 @Named("sendEmailServiceSpringBootImpl")
 private SendEmailService emailServiceSpringBoot;

 @Test
 public void sendHtmlEmailTest() throws Exception {
  try {
   emailServiceSpringBoot.sendHtmlEmail();
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
}


1 comment:

I appreciate your time in leaving a comment!