How to Send Email using Spring Boot 2 and FreeMarker HTML Template

In one of our previous blog posts, we have developed a web application for User Registration and OAuth2 Social login with Spring Boot. In a typical web application like this, we may need to send different types of emails like registration verification email, password reset email, contact email, etc., to the users of the application. This tutorial shows how to send such emails with inline images and attachments using Spring Boot and FreeMarker email templates in multiple languages.

What you’ll build

A Spring MVC web application with Freemarker templating engine to send emails in English and German languages.

Home Page

Home page

Mail sent success page

English version

Mail sent success page in english

German version

Mail sent success page in german

Emails english version

Mails in english

Registration Confirmation Email

Registration Confirmation Email in english

Reset Password Email

Reset Password mail in english

Contact Email

Contact email in english

Emails german version

Mails in german

Registration Confirmation Email

Registration Confirmation Email in german

Reset Password Email

Reset password mail in german

Contact Email

Contact mail in german

What you’ll need

  • Spring Tool Suite 4
  • JDK 11
  • Maven

Tech Stack

  • Spring Boot 2.2.5
  • JDK 11
  • FreeMarker

FreeMarker

FreeMarker is a server-side Java template engine for both web and standalone environments. Templates are written in the Freemarker Template Language (FTL), which is a simple, specialized language.

Bootstrap your application

You can create your Spring Boot application with the Web, Freemarker, Mail & Devtools dependencies and download it from here

Project Structure

+---pom.xml
|
+---src
    +---main
        +---java
        |   \---com
        |       \---javachinna
        |           |   SpringBootFreemarkerApplication.java
        |           |   
        |           +---config
        |           |       AppConfig.java
        |           |       
        |           +---controller
        |           |       PageController.java
        |           |       
        |           +---model
        |           |       Mail.java
        |           |       User.java
        |           |       
        |           \---service
        |               |   MailService.java
        |               |   MessageService.java
        |               |   
        |               \---impl
        |                       MailServiceImpl.java
        |                       
        \---resources
            |   application.properties
            |   javachinna-logo.png
            |   javachinna.jpg
            |   messages.properties
            |   messages_de.properties
            |   
            \---templates
                |   home.ftlh
                |   mail.ftlh
                |   
                \---mail
                    |   contact.ftl
                    |   verification.ftl
                    |   
                    \---layout
                            defaultLayout.ftl
                            footer.ftl
                            header.ftl

Project Dependencies

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.5.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.javachinna</groupId>
	<artifactId>spring-boot-freemarker-email-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot-freemarker-email-demo</name>
	<description>Demo project for Spring Boot Freemarker Email Template</description>

	<properties>
		<java.version>11</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-freemarker</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-mail</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<!-- <scope>runtime</scope> -->
		</dependency>

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

Create Spring Configuration

AppConfig.java

In order for our application to be able to determine which locale is currently being used, we need to add a LocaleResolver bean.

The LocaleResolver interface has implementations that determine the current locale based on the session, cookies, the Accept-Language header, or a fixed value.

In our example, we have used the session-based resolver SessionLocaleResolver and set a default locale with value US.

Also, we need to add an interceptor bean that will switch to a new locale based on the value of the lang parameter appended to a request

In order to take effect, this interceptor bean needs to be added to the application’s interceptor registry.

To achieve this, our @Configuration class has to implement the WebMvcConfigurer interface and override the addInterceptors() method as shown below

package com.javachinna.config;

import java.util.Locale;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

@Configuration
public class AppConfig implements WebMvcConfigurer {

	@Bean
	public LocaleResolver localeResolver() {
		SessionLocaleResolver slr = new SessionLocaleResolver();
		slr.setDefaultLocale(Locale.US);
		return slr;
	}

	@Override
	public void addInterceptors(final InterceptorRegistry registry) {
		final LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
		localeChangeInterceptor.setParamName("lang");
		registry.addInterceptor(localeChangeInterceptor);
	}
}

Create Controller

PageController.java

Controller mapping to display the home page and the mail sent success page.

package com.javachinna.controller;

import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import com.javachinna.model.Mail;
import com.javachinna.model.User;
import com.javachinna.service.MailService;

@Controller
public class PageController {

	@Autowired
	MailService mailService;

	@GetMapping("/")
	public String mail() {
		return "home";
	}

	@GetMapping("/send")
	public String send() {
		User user = new User(1L, "JavaChinna", "[email protected]");

		// Sending verification mail
		mailService.sendVerificationToken(UUID.randomUUID().toString(), user);

		// Sending password reset mail
		mailService.resetPasswordToken(UUID.randomUUID().toString(), user);

		// Sending contact mail
		Mail mail = new Mail();
		mail.setFromName("JavaChinna");
		mail.setFrom("[email protected]");
		mail.setSubject("Spring Boot - Email with FreeMarker template");
		mail.setBody("contact message body goes here");
		mailService.sendContactMail(mail);
		System.out.println("Done!");
		return "mail";
	}
}

Create model classes

Mail.java

package com.javachinna.model;

public class Mail {

	private String from;
	private String fromName;
	private String body;
	private String to;
	private String cc;
	private String bcc;
	private String subject;

	public Mail() {
	}
// Getters and Setters goes here

}

User.java

package com.javachinna.model;

public class User {

	public User(Long id, String displayName, String email) {
		this.id = id;
		this.displayName = displayName;
		this.email = email;
	}

	private Long id;
	private String displayName;
	private String email;

// Getters and Setters goes here
}

Create service layer

MessageService.java

This service class is responsible for getting the defined messages from the language-specific messages.properties file based on the locale.

package com.javachinna.service;

import javax.annotation.Resource;

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;

@Component
public class MessageService {

	@Resource
	private MessageSource messageSource;

	public String getMessage(String code) {
		return messageSource.getMessage(code, null, LocaleContextHolder.getLocale());
	}
}

MailService.java

package com.javachinna.service;

import com.javachinna.model.Mail;
import com.javachinna.model.User;

public interface MailService {

	void sendVerificationToken(String token, User user);

	void resetPasswordToken(final String token, final User user);

	void sendContactMail(Mail mailDTO);

}

MailServiceImpl.java

This class is responsible for generating the mails based on the current locale and sending them.

package com.javachinna.service.impl;

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;

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

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;

import com.javachinna.model.Mail;
import com.javachinna.model.User;
import com.javachinna.service.MailService;
import com.javachinna.service.MessageService;

import freemarker.template.Configuration;

/**
 * @author Chinna
 *
 */
@Service
public class MailServiceImpl implements MailService {

	private final Logger logger = LogManager.getLogger(getClass());
	public final static String BASE_URL = "baseUrl";
	public static final String LINE_BREAK = "<br>";

	@Value("${base-url}")
	private String baseUrl;

	@Value("${support.email}")
	private String supportEmail;

	@Autowired
	private MessageService messageService;

	@Autowired
	private JavaMailSender mailSender;

	@Autowired
	Configuration freemarkerConfiguration;

	@Override
	public void sendVerificationToken(String token, User user) {
		final String confirmationUrl = baseUrl + "/registrationConfirm?token=" + token;
		final String message = messageService.getMessage("message.verificationMail");
		sendHtmlEmail(messageService.getMessage("message.verification"), message + LINE_BREAK + confirmationUrl, user);
	}

	@Override
	public void resetPasswordToken(String token, User user) {
		final String url = baseUrl + "/resetPassword?id=" + user.getId() + "&token=" + token;
		final String message = messageService.getMessage("message.resetPasswordEmail");
		sendHtmlEmail(messageService.getMessage("message.resetPassword"), message + LINE_BREAK + url, user);
	}

	private String geFreeMarkerTemplateContent(Map<String, Object> model, String templateName) {
		StringBuffer content = new StringBuffer();
		try {
			content.append(FreeMarkerTemplateUtils.processTemplateIntoString(freemarkerConfiguration.getTemplate(templateName), model));
			return content.toString();
		} catch (Exception e) {
			System.out.println("Exception occured while processing fmtemplate:" + e.getMessage());
		}
		return "";
	}

	@Override
	public void sendContactMail(Mail mailDTO) {
		String subject = MessageFormat.format(messageService.getMessage("mail.contact.subject"), (mailDTO.getFromName() + " [ " + mailDTO.getFrom() + " ] "));
		Map<String, Object> model = new HashMap<String, Object>();
		model.put("title", "New Contact Email"); // so that we can reference it from HTML
		model.put("message", mailDTO);
		model.put("greeting", messageService.getMessage("mail.contact.greeting"));
		model.put(BASE_URL, baseUrl);
		try {
			sendHtmlMail(mailDTO.getFrom(), supportEmail, subject, geFreeMarkerTemplateContent(model, "mail/contact.ftl"), true);
			logger.info(String.format("Contact Email sent from: %s", mailDTO.getFrom()));
		} catch (MessagingException e) {
			logger.error("Failed to send contact mail", e);
		}
	}

	private void sendHtmlEmail(String subject, String msg, User user) {
		Map<String, Object> model = new HashMap<String, Object>();
		model.put("name", user.getDisplayName());
		model.put("msg", msg);
		model.put("title", subject);
		model.put(BASE_URL, baseUrl);
		try {
			sendHtmlMail(supportEmail, user.getEmail(), subject, geFreeMarkerTemplateContent(model, "mail/verification.ftl"), false);
		} catch (MessagingException e) {
			logger.error("Failed to send mail", e);
		}
	}

	public void sendHtmlMail(String from, String to, String subject, String body, boolean attachImage) throws MessagingException {
		MimeMessage mail = mailSender.createMimeMessage();
		MimeMessageHelper helper = new MimeMessageHelper(mail, true, "UTF-8");

		helper.setFrom(from);
		if (to.contains(",")) {
			helper.setTo(to.split(","));
		} else {
			helper.setTo(to);
		}
		helper.setSubject(subject);
		helper.setText(body, true);
		if (attachImage) {
			// Inline image
			helper.addInline("logo.png", new ClassPathResource("javachinna-logo.png"));
			// attachment
			helper.addAttachment("javachinna.jpg", new ClassPathResource("javachinna.jpg"));
		}

		mailSender.send(mail);
		logger.info("Sent mail: {0}", subject);
	}
}

The geFreeMarkerTemplateContent() method replaces the placeholders like ${name} with model attribute values in the Freemarker template and returns the HTML as a string.

Create Spring boot application class

SpringBootFreemarkerApplication.java

package com.javachinna;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootFreemarkerEmailDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootFreemarkerEmailDemoApplication.class, args);
	}
}

Create view pages

home.ftlh

<!DOCTYPE html>
<html>
    <head>
        <title>Home page</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>

    <body>
        <button value="en" style="width:50%;padding: 10px;margin-bottom: 20px;">Send email in English</button>
        <button value="de" style="width:50%;padding: 10px;margin-bottom: 50px;">Send email in German</button>
    </body>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js">
</script>
<script type="text/javascript">
$(document).ready(function() {
	$("body").on("click tap", "button", function(event) {
		var $target = $(event.target);
		var val = $target.val();
		$target.attr('disabled', 'disabled');
		window.location.replace('send?lang=' + val);
	});
});
</script>
</html>

The jQuery script will get the button value when clicked, disables the button, and calls the /send?lang= URL with the clicked button value i.e. locale code

mail.ftlh

<!DOCTYPE html>
<html>
    <head>
        <title>Home page</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>

    <body>
        <@spring.message "message.mail.success"/> <a href="/"><@spring.message "message.back.home"/></a>
    </body>
</html>

Create mail template

These template files will be used to create email-specific files in order to generate the registration email, verification email, and contact email with the same layout, header, and footer.

defaultLayout.ftl

This file defines the basic layout of the mail which includes the header and footer. To learn more about the Freemarker directives like macro, include, nested, etc., please refer to the official Freemarker documentation

<#macro myLayout>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
    <body style="width:100%;height:100%">
      <table cellspacing="0" cellpadding="0" style="width:100%;height:100%">
        <tr>
          <td colspan="2" align="center">
            <#include "header.ftl"/>
          </td>
        </tr>
        <tr>
          <td>
            <#nested/>
          </td>
        </tr>
        <tr>
          <td colspan="2">
            <#include "footer.ftl"/>
          </td>
        </tr>
      </table>
    </body>
  </html>
</#macro>

header.ftl

<img src="${baseUrl}/wp-content/uploads/2020/02/cropped-JavaChinna_logo.jpg" alt="https://www.javachinna.com" style="display: block;" width="130" height="50"/>
<h3><span style="border-bottom: 4px solid #32CD32;">${title}</span></h3>

footer.ftl

<div style="background: #F0F0F0; text-align: center; padding: 5px; margin-top: 40px;">
            Message Generated from: javachinna.com
</div>

Create mail specific files using the above template

contact.ftl

This file is used to generate the contact emails sent from the “Contact” page of a website

<#import "layout/defaultLayout.ftl" as layout>
<@layout.myLayout>
<div>
<h3>${greeting}</h3>
<p>
    From ${message.fromName}
</p>
<h3>
    Comment
</h3>
<p>
    ${message.body}
</p>
<br>
<img src="cid:logo.png" alt="https://www.javachinna.com" style="display: block;" />
</div>
</@layout.myLayout>

Note: we are linking to an email attachment by using the cid: inside the src attribute inside the <img/> element. This image will be attached as an attachment in the email service by the MimeMessageHelper class. Here the cid stands for contentId

verification.ftl

This file is used to generate the Registration confirmation and Password Reset emails.

<#import "layout/defaultLayout.ftl" as layout>
<@layout.myLayout>
<div>
<table align="center" border="0" cellpadding="0" cellspacing="0" style="border-collapse: collapse;">
		<tr>
			<td style="padding: 0px 30px 0px 30px;">
				<p>Dear ${name},</p>
				<p>${msg}</p>
			</td>
		</tr>
		<tr>
			<td style="padding-left: 30px;">
				<p>
					Regards, <br /> <em>JavaChinna</em>
				</p>
			</td>
		</tr>
	</table>
</div>
</@layout.myLayout>

Create properties files

application.properties

Note: Spring Boot recently changed the default extension from .ftl to .ftlh for the freemarker template view files. This was done to enable HTML escaping as per the github issue here. Hence we don’t have to specify the spring.freemarker.suffix property explicitly since we have created the view files with .ftlh extensions. If you want to use the .ftl extension, then you can uncomment this property. However, this setting does not affect freemarker mail template files since they are being used only for mail generation.

spring.freemarker.settings.auto_import property is used to import the Spring provided spring.ftl file in order to get the locale-specific messages from the respective properties file by using the <@spring.message "message.key"/> tag in the template view files.

GMAIL SMTP server is being used for sending the mails.

base-url=https://www.javachinna.com
spring.freemarker.template-loader-path=classpath:/templates
#spring.freemarker.suffix=.ftl
spring.freemarker.settings.auto_import=spring.ftl as spring
################### JavaMail Configuration ##########################
[email protected]

################### GMail Configuration ##########################
spring.mail.host=smtp.gmail.com
spring.mail.port=465
spring.mail.protocol=smtps
[email protected]
spring.mail.password=secret
spring.mail.properties.mail.transport.protocol=smtps
spring.mail.properties.mail.smtps.auth=true
spring.mail.properties.mail.smtps.starttls.enable=true
spring.mail.properties.mail.smtps.timeout=8000

Defining the Message Sources

By default, a Spring Boot application will look for message files containing internationalization keys and values in the src/main/resources folder.

The file for the default locale will have the name messages.properties, and files for each locale will be named messages_XX.properties, where XX is the locale code.

The keys for the values that will be localized have to be the same in every file, with values appropriate to the language they correspond to.

If a key does not exist in a certain requested locale, then the application will fall back to the default locale value.

messages.properties

message.verification=Registration Confirmation
message.resetPassword=Reset Password
message.verificationMail=Thank you for creating account. Please click the link below to activate your account. This link will expire in 24 hours.
message.sendResetToken=Instructions on how to reset your password has been sent to your email account
message.resetPasswordEmail=You requested to reset the password for your account with this e-mail address. Please click this link to reset your password.
message.mail.success=Mail sent successfully
message.back.home=Back to Home
mail.contact.subject=New Contact Email from {0}
mail.contact.greeting=Hi, you have a new Contact Message!

messages_de.properties

message.verification=Anmeldebestätigung
message.resetPassword=Passwort zurücksetzen
message.verificationMail=Vielen Dank, dass Sie ein Konto erstellt haben. Bitte klicken Sie auf den Link unten, um Ihr Konto zu aktivieren. Dieser Link läuft in 24 Stunden ab.
message.sendResetToken=Anweisungen zum Zurücksetzen Ihres Passworts wurden an Ihr E-Mail-Konto gesendet
message.resetPasswordEmail=Sie haben angefordert, das Passwort für Ihr Konto mit dieser E-Mail-Adresse zurückzusetzen. Bitte klicken Sie auf diesen Link, um Ihr Passwort zurückzusetzen.
message.mail.success=Mail erfolgreich gesendet
message.back.home=Zurück nach Hause
mail.contact.subject=Neue Kontakt-E-Mail von {0}
mail.contact.greeting=Hallo, du hast eine neue Kontaktnachricht!

Run with Maven

You can run the application using mvn clean spring-boot:run and visit http://localhost:8080

Source code

https://github.com/JavaChinna/spring-boot-freemarker-email

References

https://www.baeldung.com/spring-boot-internationalization

Conclusion

That’s all folks! In this tutorial, you’ve learned how to get send mail using Spring Boot FreeMarker template in multiple languages.

I hope you enjoyed this tutorial. Thank you for reading.

Leave a Reply