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
Mail sent success page
English version
German version
Emails english version
Registration Confirmation Email
Reset Password Email
Contact Email
Emails german version
Registration Confirmation Email
Reset Password Email
Contact Email
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 thesrc
attribute inside the<img/>
element. This image will be attached as an attachment in the email service by theMimeMessageHelper
class. Here thecid
stands forcontentId
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.