Message Tasks

Sending emails is something very common in process automation so in this tutorial we will focus on how to send emails using Send Tasks.

Send Tasks are quite similar to Service Tasks in the following way: (a) they are automatically executed by the process engine; and (b) they are configured the same way (see our other tutorial Service Tasks)

So let’s change the Travel Plan Process to include a Send Task that aims at sending an email to the user with a summary of the travel plan at the end of the process:

message-task-new-message-task

To configure this new task, click on the wrench icon and then select the type Send Task.

message-task-new-message-task-configure-task-type

Then configure the delegate on the property tab as follow:

  • Implementation: Delegate Expression
  • Delegate Expression: ${emailTravelPlanSummaryDelegate}

message-task-new-message-task-configure-message-task

Now create a java delegate class EmailTravelPlanSummaryDelegate that implements the interface org.camunda.bpm.engine.delegate.JavaDelegate.

package com.mycompany.myapp.delegate;

import com.mycompany.myapp.service.dto.TravelPlanDTO;
import com.mycompany.myapp.service.dto.TravelPlanProcessDTO;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Component;

@Component
public class EmailTravelPlanSummaryDelegate implements JavaDelegate {

    @Override
    public void execute(DelegateExecution delegateExecution) throws Exception {
        TravelPlanProcessDTO travelPlanProcess = (TravelPlanProcessDTO) delegateExecution.getVariable("processInstance");
        TravelPlanDTO travelPlan = travelPlanProcess.getTravelPlan();
        //TODO: there are some coding to be done here using the processBinding entity (TravelPlanProcess)...
    }
}

So far we have configured the Send Task in the BPMN file and created an empty java delegate EmailTravelPlanSummaryDelegate. We still have some code to do to enable the delegate to actually send the email. An easy way to send emails in our reference architecture is by using the MailService component.

MailService Component

The MailService component has a few methods specialized on sending emails. We will explore here the method sendEmail that is pretty straighforward and receives three parameters: to, subject, and content. So let’s see how to build these parameters starting by the destination email address.

String to = travelPlan.getUserMail();

The destination email address (represented here by the to variable) comes from the TravelPlan domain entity.

String subject = "[AgileKip] Summary of your travel " + travelPlan.getTravelName();

The email subject (represented here by the subject variable) is also calculated using the TravelPlan domain entity.

To build the email content, we will use the framework Thymeleaf:

Context context = new Context(Locale.getDefault());
context.setVariable("travelPlan", travelPlan);
String content = templateEngine.process("travelPlanProcess/travelPlanSummaryEmail", context);

As you see, the building of the email content is a little bit trickier. Basically you have to:

  • Create a context object to encapsule all the data Thymeleaf will need. In our example, the travelPlan object has all the data required to build the email content.
  • Create a Thymeleaf template for the email content.

A Thymeleaf template is an html file annotated with some Thymeleaf extensions. The framework searchs for the templates in the folder main/resources/template. So, we have to create the file main/resources/template/travelPlanProcess/travelPlanSummaryEmail.html.
The project structure and the content of this file is shown below:

message-task-project-structure-for-tempates

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:lang="${#locale.language}" lang="en">
  <head>
    <title>Travel Plan Summary</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
    <p>Dear <span th:text="${travelPlan.userName}">userName</span></p>
    <p>Here is a summary of your travel <b th:text="${travelPlan.travelName}">travelName</b></p>
    <ul>
        <li><b>Start</b>: <span th:text="${travelPlan.startDate }"></span></li>
        <li><b>End</b>: <span th:text="${travelPlan.endDate }"></span></li>
        <li><b>Flight</b>: <span th:text="${travelPlan.airlineCompanyName}"></span> / <span th:text="${travelPlan.airlineTicketNumber}"></span></li>
        <li><b>Hotel</b>: <span th:text="${travelPlan.hotelName}"></span> / <span th:text="${travelPlan.hotelBookingNumber}"></span></li>
        <li><b>Rental Car</b>: <span th:text="${travelPlan.carCompanyName}"></span> / <span th:text="${travelPlan.carBookingNumber}"></span></li>
    </ul>
    <p>
      Regards,
      <br />
      <em>AgileKip Team</em>
    </p>
  </body>
</html>

You can find more information about the Thymeleaf framework on its official website: https://www.thymeleaf.org/

Let’s put everything together and see the final code of the EmailTravelPlanSummaryDelegate delegate:

package com.mycompany.myapp.delegate;

import com.mycompany.myapp.service.MailService;
import com.mycompany.myapp.service.dto.TravelPlanDTO;
import com.mycompany.myapp.service.dto.TravelPlanProcessDTO;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine;

import java.util.Locale;

@Component
public class EmailTravelPlanSummaryDelegate implements JavaDelegate {

    @Autowired
    MailService mailService;

    @Autowired
    SpringTemplateEngine templateEngine;

    @Override
    public void execute(DelegateExecution delegateExecution) throws Exception {
        TravelPlanProcessDTO travelPlanProcess = (TravelPlanProcessDTO) delegateExecution.getVariable("processInstance");
        TravelPlanDTO travelPlan = travelPlanProcess.getTravelPlan();
        String to = travelPlan.getUserEmail();
        String subject = "[AgileKip] Summary of your travel " + travelPlan.getTravelName();
        Context context = new Context(Locale.getDefault());
        context.setVariable("travelPlan", travelPlan);
        String content = templateEngine.process("travelPlanProcess/travelPlanSummaryEmail", context);
        mailService.sendEmail(to, subject, content, false, true);
    }
}

Note: You can checkout the tag part4-message-task from the TravelPlan repository on Github to get the BPMN file src/main/resources/TravelPlanProcess.bpmn, the delegate EmailTravelPlanSummaryDelegate, and the template travelPlanSummaryEmail.html used in this tutorial. git checkout part4-message-task.

We have already done the coding necessary to send the desired email. However, if you execute the TravelPlan process on development mode, you are probably not receiving any email at all and are getting instead the following error:

2021-11-05 18:58:50.216  WARN 22861 --- [vel-plan-task-2] com.mycompany.myapp.service.MailService  : Email could not be sent to user 'ulisses.telemaco@gmail.com'

org.springframework.mail.MailSendException: Mail server connection failed; nested exception is com.sun.mail.util.MailConnectException: Couldn't connect to host, port: localhost, 25; timeout -1;
  nested exception is:
	java.net.ConnectException: Connection refused (Connection refused). Failed messages: com.sun.mail.util.MailConnectException: Couldn't connect to host, port: localhost, 25; timeout -1;
  nested exception is:
	java.net.ConnectException: Connection refused (Connection refused)
	at org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:448)
	at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:361)
	at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:356)
	at com.mycompany.myapp.service.MailService.sendEmail(MailService.java:73)
	at com.mycompany.myapp.service.MailService$$FastClassBySpringCGLIB$$2ee4e862.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:64)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)
	at com.mycompany.myapp.aop.logging.LoggingAspect.logAround(LoggingAspect.java:105)
	at jdk.internal.reflect.GeneratedMethodAccessor195.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:634)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:624)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:72)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115)
	at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java)
	at tech.jhipster.async.ExceptionHandlingAsyncTaskExecutor.lambda$createWrappedRunnable$1(ExceptionHandlingAsyncTaskExecutor.java:78)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: com.sun.mail.util.MailConnectException: Couldn't connect to host, port: localhost, 25; timeout -1
	at com.sun.mail.smtp.SMTPTransport.openServer(SMTPTransport.java:2210)
	at com.sun.mail.smtp.SMTPTransport.protocolConnect(SMTPTransport.java:722)
	at javax.mail.Service.connect(Service.java:342)
	at org.springframework.mail.javamail.JavaMailSenderImpl.connectTransport(JavaMailSenderImpl.java:518)
	at org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:437)
	... 31 common frames omitted
Caused by: java.net.ConnectException: Connection refused (Connection refused)
	at java.base/java.net.PlainSocketImpl.socketConnect(Native Method)
	at java.base/java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:399)
	at java.base/java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:242)
	at java.base/java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:224)
	at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
	at java.base/java.net.Socket.connect(Socket.java:608)
	at java.base/java.net.Socket.connect(Socket.java:557)
	at com.sun.mail.util.SocketFetcher.createSocket(SocketFetcher.java:335)
	at com.sun.mail.util.SocketFetcher.getSocket(SocketFetcher.java:214)
	at com.sun.mail.smtp.SMTPTransport.openServer(SMTPTransport.java:2160)
	... 35 common frames omitted

This is happening because we don’t have an SMTP server configured in our development environment. To fix this problem, we recommend the use of the tool FakeSMTP. This is a very handy tool that can be used on development mode to mock an SMTP server.

After downloading the tool and executing it, set the port to 25 and start the server:

message-task-fakesmtp-configuration

message-task-fakesmtp-waiting-emails

The fakeSMTP is running and waiting for emails. Now start the TravelPlan process, execute all the user tasks (Flight, Hotel, and Car) and check on the fakeSMTP interface for the new email:

message-task-fakesmtp-email-received

message-task-email-example

The email with the Travel Plan summary was generated and sent to the user who started the process.

Congratulations, you’ve just learned how to send emails on our platform.