package org.superbiz;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import jakarta.ejb.Stateless;
import jakarta.mail.*;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import java.io.StringWriter;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
@Stateless
public class EMailServiceImpl {
private static final Logger LOGGER = LoggerFactory.getLogger(EMailServiceImpl.class);
private static final String HEADER_HTML_EMAIL = "text/html; charset=UTF-8";
private static final String TEMPLATE_DIRECTORY = "templates/";
private static final String VELOCITY_RESOURCE_CLASS_LOADER_KEY = "resource.loader.class.class";
private static final String VELOCITY_RESOURCE_CLASS_LOADER = "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader";
private static final String VELOCITY_RESOURCE_LOADER_KEY = "resource.loaders";
private static final String VELOCITY_RESOURCE_LOADER = "class";
@Resource(mappedName = "java:comp/env/tomee/mail/exampleSMTP")
private Session mailSession;
private VelocityEngine velocityEngine;
@PostConstruct
public void init() {
// Properties documented here: https://wiki.apache.org/velocity/VelocityAndWeblogic
final Properties prop = new Properties();
prop.setProperty(VELOCITY_RESOURCE_LOADER_KEY, VELOCITY_RESOURCE_LOADER);
prop.setProperty(VELOCITY_RESOURCE_CLASS_LOADER_KEY, VELOCITY_RESOURCE_CLASS_LOADER);
velocityEngine = new VelocityEngine();
velocityEngine.init(prop);
/* Ensures that smtp authentication mechanism works as configured */
boolean authenticate = "true".equals(mailSession.getProperty("mail.smtp.auth"));
if (authenticate) {
final String username = mailSession.getProperty("mail.smtp.user");
final String password = mailSession.getProperty("mail.smtp.password");
final URLName url = new URLName(
mailSession.getProperty("mail.transport.protocol"),
mailSession.getProperty("mail.smtp.host"), -1, null,
username, null);
mailSession.setPasswordAuthentication(url, new PasswordAuthentication(username, password));
} else {
LOGGER.warn("Using EMailService without SMTP auth configured. This might be valid, but could also be dangerous!");
}
}
public void sendMail(EMail eMail, String htmlTemplate, Map<String, String> templateResources) {
if (!eMail.getMailType().equals(MailType.MAIL_HTML)) {
throw new RuntimeException("You can't send an HTML eMail with the Mail instance provided: '" + eMail.getMailType().toString() + "'!");
} else {
htmlTemplate = TEMPLATE_DIRECTORY + htmlTemplate;
try {
MimeMessage message = createMimeMessage(eMail);
if (!velocityEngine.resourceExists(htmlTemplate)) {
throw new RuntimeException("Could not find the given email template '" + htmlTemplate + "' in the classpath.");
} else {
final Template template = velocityEngine.getTemplate(htmlTemplate);
final VelocityContext velocityContext = new VelocityContext();
for (Map.Entry<String, String> templateEntry : templateResources.entrySet()) {
velocityContext.put(templateEntry.getKey(), templateEntry.getValue());
}
final StringWriter stringWriter = new StringWriter();
template.merge(velocityContext, stringWriter);
// setting the eMail's content as HTML mail body
final Multipart mp = new MimeMultipart();
final MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(stringWriter.toString(), HEADER_HTML_EMAIL);
mp.addBodyPart(htmlPart);
message.setContent(mp);
Transport.send(message);
// mark this eMail as sent with the current date
eMail.setSentDate(new Date());
}
} catch (MessagingException ex) {
LOGGER.warn("Could not send template HTML eMail: {}", ex.getLocalizedMessage());
throw new RuntimeException(ex.getLocalizedMessage(), ex);
}
}
}
private MimeMessage createMimeMessage(EMail eMail) throws MessagingException {
MimeMessage message = new MimeMessage(mailSession);
message.setFrom(new InternetAddress(eMail.getMailFrom()));
for (String mailTo : eMail.getMailTo()) {
message.addRecipient(Message.RecipientType.TO, new InternetAddress(mailTo));
}
message.setSubject(eMail.getMailSubject());
message.setSentDate(new Date());
for (String ccRecipient : eMail.getMailCc()) {
message.addRecipient(Message.RecipientType.CC, new InternetAddress(ccRecipient));
}
for (String bccRecipient : eMail.getMailBcc()) {
message.addRecipient(Message.RecipientType.BCC, new InternetAddress(bccRecipient));
}
return message;
}
@PreDestroy
public void close() {
if (mailSession != null) {
mailSession = null;
}
}
}
Jakarta Mail API with Apache Velocity Templating
This examples demonstrates the use of Jakarta Mail API in combination with Apache Velocity to create templated HTML Emails.
A simple @Stateless service using the Javamail API
Here we see a very simple @Stateless
service that can be called to send an Email.
It uses Apache Velocity to load velocity templates from a pre-defined location templates
, which is located in the resources
folder.
Please note, that we need to use some additional velocity configuration options to specify org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
as a resource loader in order to actually load the templates when running inside TomEE.
The configuration of the mail session can be done via a resource.xml
, which looks like
<?xml version="1.0" encoding="utf-8"?>
<resources>
<Resource id="tomee/mail/exampleSMTP" type="jakarta.mail.Session">
mail.debug=false
mail.transport.protocol=smtp
mail.smtp.starttls.enable=true
mail.smtp.starttls.required=true
<!-- mail.smtp.ssl.protocols=TLSv1.2 TLSv1.3 -->
<!-- mail.smtp.ssl.ciphersuites=TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 -->
mail.smtp.host=mail.mymailprovider.com
mail.smtp.port=587
mail.smtp.auth=true
mail.smtp.user=myself@mymailprovider.com
<!-- your password, and not 'mail.smtp.password' -->
password=mypassword
</Resource>
</resources>
You can tune this resource.xml
for your specific Email provider. Please note, that you can specifiy the ssl.protocols
and ciphersuites
, which are used to connect to the specific mail server.
If not specified, JVM defaults are used.
Testing
Test for the EMailService
The test uses the ApplicationComposer to make testing easy. To test our service, we rely on GreenMail, which allows us to spawn a catch-all smtp server during the unit test.
The idea is to create our EMailServiceImpl
by creating a EjbJar
on the fly.
To do so, we add @Classes
annotation to define the set of classes to use in the EjbJar
.
In addition, we use @Configuration
to define the Mail Session Resource for the test context to ensure,
that we are not bound to a pre-defined port.As mentioned above, the resource.xml
can also be used to configure the mail session..
Finally, we use our service to send an Email to our catch-all smtp server and check the related results.
package org.superbiz;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
import org.apache.openejb.jee.EjbJar;
import org.apache.openejb.junit5.RunWithApplicationComposer;
import org.apache.openejb.testing.Classes;
import org.apache.openejb.testing.Configuration;
import org.apache.openejb.testing.Module;
import org.apache.openejb.util.NetworkUtil;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;
import jakarta.mail.BodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import static org.junit.jupiter.api.Assertions.*;
@RunWithApplicationComposer
public class EMailServiceTest {
private static final int SMTP_TEST_PORT = NetworkUtil.getNextAvailablePort();
private static final String USER_PASSWORD = "s3cr3t";
private static final String USER_NAME = "admin@localhost";
private static final String EMAIL_USER_ADDRESS = "admin@localhost";
private static GreenMail mailServer;
private static CountDownLatch started = new CountDownLatch(1);
@Module
@Classes(cdi = true, value = {EMailServiceImpl.class})
public EjbJar beans() {
return new EjbJar("javamail-velocity");
}
@Configuration
public Properties config() {
//Note: We can also configure this via a resource.xml or via tomee.xml
Properties properties = new Properties();
properties.put("tomee/mail/mySMTP", "new://Resource?type=jakarta.mail.Session");
properties.put("tomee/mail/mySMTP.mail.debug", "false");
properties.put("tomee/mail/mySMTP.mail.transport.protocol", "smtp");
properties.put("tomee/mail/mySMTP.mail.smtp.host", "localhost");
properties.put("tomee/mail/mySMTP.mail.smtp.port", SMTP_TEST_PORT);
properties.put("tomee/mail/mySMTP.mail.smtp.auth", "true");
properties.put("tomee/mail/mySMTP.mail.smtp.user", USER_NAME);
properties.put("tomee/mail/mySMTP.password", USER_PASSWORD);
return properties;
}
@Inject
private EMailServiceImpl eMailService;
@BeforeAll
public static void setUp() throws InterruptedException {
mailServer = new CustomGreenMailServer(new ServerSetup(SMTP_TEST_PORT, null, "smtp"));
mailServer.start();
//wait for the server startup...
started.await();
// create user on mail server
mailServer.setUser(EMAIL_USER_ADDRESS, USER_NAME, USER_PASSWORD);
}
@AfterAll
public static void tearDown() {
if (mailServer != null) {
mailServer.stop();
}
}
@Test
public void testSendMailHTMLTemplate() throws Exception {
// prepare
String eMailTemplateName = "email-html-template.vm";
Map<String, String> mailTemplateProps = new HashMap<>();
mailTemplateProps.put("name", "Jon Doe");
String fromMail = "admin@localhost";
String toEmail = "john@localhost.com";
String subject = "Template HTML email!";
Collection<String> toRecipients = new ArrayList<>();
toRecipients.add(toEmail);
EMail eMail = new EMail(MailType.MAIL_HTML,toRecipients, subject, "", Collections.emptyList(),Collections.emptyList());
eMail.setMailFrom(fromMail);
// test
assertNull(eMail.getSentDate());
eMailService.sendMail(eMail, eMailTemplateName, mailTemplateProps);
assertNotNull(eMail.getSentDate());
// fetch messages from server
MimeMessage[] messages = mailServer.getReceivedMessages();
assertNotNull(messages);
assertEquals(1, messages.length);
MimeMessage msg = messages[0];
assertTrue(msg.getContentType().contains("multipart/mixed;"));
assertEquals(subject, msg.getSubject());
MimeMultipart message = (MimeMultipart) msg.getContent();
BodyPart bodyPart = message.getBodyPart(0);
assertEquals("text/html; charset=UTF-8", bodyPart.getHeader("Content-Type")[0]);
String receivedMailContent = String.valueOf(bodyPart.getContent());
assertTrue(receivedMailContent.contains("Dear Jon Doe"));
assertTrue(receivedMailContent.contains("templated"));
assertEquals(fromMail, msg.getFrom()[0].toString());
}
public static class CustomGreenMailServer extends GreenMail {
public CustomGreenMailServer(ServerSetup config) {
super(new ServerSetup[]{config});
}
public synchronized void start() {
super.start();
started.countDown();
}
}
}
Running
Running the example is fairly simple. In the javamail-velocity
directory run:
$ mvn clean install
Which should create output as follows:
[INFO] Running org.superbiz.EMailServiceTest
Okt 25, 2021 4:38:24 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Created new singletonService org.apache.openejb.cdi.ThreadSingletonServiceImpl@55fe41ea
Okt 25, 2021 4:38:24 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Succeeded in installing singleton service
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Cannot find the configuration file [conf/openejb.xml]. Will attempt to create one for the beans deployed.
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Configuring Service(id=Default Security Service, type=SecurityService, provider-id=Default Security Service)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Configuring Service(id=Default Transaction Manager, type=TransactionManager, provider-id=Default Transaction Manager)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Creating TransactionManager(id=Default Transaction Manager)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Creating SecurityService(id=Default Security Service)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Configuring enterprise application: /home/rzo1/coding/tomee/examples/javamail-velocity/EMailServiceTest
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-deploying ejb EMailServiceImpl: EjbDeployment(deployment-id=EMailServiceImpl)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Configuring Service(id=EMailServiceTest/tomee/mail/mySMTP, type=Resource, provider-id=Default Mail Session)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Creating Resource(id=EMailServiceTest/tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Configuring Service(id=Default Managed Container, type=Container, provider-id=Default Managed Container)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-creating a container for bean org.superbiz.EMailServiceTest: Container(type=MANAGED, id=Default Managed Container)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Creating Container(id=Default Managed Container)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Using directory /tmp for stateful session passivation
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-linking resource-ref 'openejb/Resource/EMailServiceTest/tomee/mail/mySMTP' in bean org.superbiz.EMailServiceTest to Resource(id=EMailServiceTest/tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-linking resource-ref 'openejb/Resource/tomee/mail/mySMTP' in bean org.superbiz.EMailServiceTest to Resource(id=EMailServiceTest/tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-linking resource-ref 'openejb/Resource/EMailServiceTest/tomee/mail/mySMTP' in bean EjbModule652176954.Comp937277082 to Resource(id=EMailServiceTest/tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-linking resource-ref 'openejb/Resource/tomee/mail/mySMTP' in bean EjbModule652176954.Comp937277082 to Resource(id=EMailServiceTest/tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Configuring Service(id=Default Stateless Container, type=Container, provider-id=Default Stateless Container)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-creating a container for bean EMailServiceImpl: Container(type=STATELESS, id=Default Stateless Container)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Creating Container(id=Default Stateless Container)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-linking resource-ref 'java:comp/env/org.superbiz.EMailServiceImpl/mailSession' in bean EMailServiceImpl to Resource(id=tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-linking resource-ref 'openejb/Resource/EMailServiceTest/tomee/mail/mySMTP' in bean EMailServiceImpl to Resource(id=EMailServiceTest/tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-linking resource-ref 'openejb/Resource/tomee/mail/mySMTP' in bean EMailServiceImpl to Resource(id=EMailServiceTest/tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-linking resource-ref 'java:comp/env/org.superbiz.EMailServiceImpl/mailSession' in bean javamail-velocity.Comp234740890 to Resource(id=tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-linking resource-ref 'openejb/Resource/EMailServiceTest/tomee/mail/mySMTP' in bean javamail-velocity.Comp234740890 to Resource(id=EMailServiceTest/tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Auto-linking resource-ref 'openejb/Resource/tomee/mail/mySMTP' in bean javamail-velocity.Comp234740890 to Resource(id=EMailServiceTest/tomee/mail/mySMTP)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Enterprise application "/home/rzo1/coding/tomee/examples/javamail-velocity/EMailServiceTest" loaded.
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Not creating another application classloader for EMailServiceTest
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Assembling app: /home/rzo1/coding/tomee/examples/javamail-velocity/EMailServiceTest
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Jndi(name=EMailServiceImplLocalBean) --> Ejb(deployment-id=EMailServiceImpl)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Jndi(name=global/EMailServiceTest/javamail-velocity/EMailServiceImpl!org.superbiz.EMailServiceImpl) --> Ejb(deployment-id=EMailServiceImpl)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Jndi(name=global/EMailServiceTest/javamail-velocity/EMailServiceImpl) --> Ejb(deployment-id=EMailServiceImpl)
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Existing thread singleton service in SystemInstance(): org.apache.openejb.cdi.ThreadSingletonServiceImpl@55fe41ea
Okt 25, 2021 4:38:25 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: OpenWebBeans Container is starting...
Okt 25, 2021 4:38:25 PM org.apache.webbeans.plugins.PluginLoader startUp
INFORMATION: Adding OpenWebBeansPlugin : [CdiPlugin]
Okt 25, 2021 4:38:26 PM org.apache.webbeans.config.BeansDeployer validateInjectionPoints
INFORMATION: All injection points were validated successfully.
Okt 25, 2021 4:38:26 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: OpenWebBeans Container has started, it took 758 ms.
Okt 25, 2021 4:38:26 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Created Ejb(deployment-id=EMailServiceImpl, ejb-name=EMailServiceImpl, container=Default Stateless Container)
Okt 25, 2021 4:38:26 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Started Ejb(deployment-id=EMailServiceImpl, ejb-name=EMailServiceImpl, container=Default Stateless Container)
Okt 25, 2021 4:38:26 PM org.apache.batchee.container.services.ServicesManager init
WARNUNG: You didn't specify org.apache.batchee.jmx.application and JMX is already registered, skipping
Okt 25, 2021 4:38:26 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Deployed Application(path=/home/rzo1/coding/tomee/examples/javamail-velocity/EMailServiceTest)
Okt 25, 2021 4:38:26 PM com.icegreen.greenmail.smtp.SmtpManager$Incoming handle
INFORMATION: Created user login john@localhost.com for address john@localhost.com with password john@localhost.com because it didn't exist before.
Okt 25, 2021 4:38:26 PM org.apache.openejb.util.LogStreamAsync run
INFORMATION: Undeploying app: /home/rzo1/coding/tomee/examples/javamail-velocity/EMailServiceTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.179 s - in org.superbiz.EMailServiceTest