Vision Statement
Create a very lightweight email micro-service that will be self-contained, easy to deploy and integrate with. Micro-service could not use any out of the box email servers or email as a service solutions.
Use Case
Suppose, you have an application that needs to send user emails to update a forgotten password or to invite new users to the system. The email capability of your application should be designed as a shared component that can be later extended to send text messages. Email templates should be consistent across different applications and maintained in one place. Email server configuration can be complex, so it needs to be isolated and easily reproducible.
Design Considerations
Spring boot or no spring boot, that is the question. For the docker-compose
Implementation Details
Jersey, Tomcat, and HK2
To start building micro-service using Jersey, Tomcat, and HK2 drop theses dependencies in pom.xml
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey2.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>${jersey2.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey2.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.bundles</groupId>
<artifactId>jaxrs-ri</artifactId>
<version>${jersey2.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.hk2</groupId>
<artifactId>hk2-extras</artifactId>
<version>${hk2.version}</version>
</dependency>
Application Class
The main application class looks like this:
public class EmailApplication
{
private static final Logger LOGGER = LoggerFactory.getLogger(EmailApplication.class);
public static void main(String[] args) {
try {
start();
} catch (Exception e) {
LOGGER.error("ITZap Email application failed to run", e);
}
}
private static void start() throws Exception {
String contextPath = "";
String appBase = ".";
String port = System.getenv("PORT");
if (port == null || port.isEmpty()) {
port = "8025";
}
Tomcat tomcat = new Tomcat();
tomcat.setPort(Integer.parseInt(port));
tomcat.getHost().setAppBase(appBase);
Context context = tomcat.addWebapp(contextPath, appBase);
Tomcat.addServlet(context, "jersey-container-servlet",
new ServletContainer(resourceConfig()));
context.addServletMappingDecoded(UDecoder.URLDecode("/v1/itzap/message/*", Charset.defaultCharset()),
"jersey-container-servlet");
// initialize Tomcat. (Since version 9.xx need to add tomcat.getConnector() here)
tomcat.getConnector();
tomcat.start();
tomcat.getServer().await();
}
private static ResourceConfig resourceConfig() {
return new JerseyConfig();
}
}HK2 Dependency Injection
I drop all factories as static nested classes in one Factories class for convenience.
public final class Factories {
private static final Config CONFIG = ConfigFactory.load();
private Factories() {}
public static class MailServiceFactory extends AbstractFactory<EMailService> {
public MailServiceFactory() {
}
@Override
public EMailService provide() {
return new EMailService(CONFIG);
}
}
}Put it all together.
public class JerseyConfig extends ResourceConfig {
JerseyConfig() {
register(JacksonFeature.class);
// register endpoints
packages("com.itzap.message.rest");
register(new ContainerLifecycleListener()
{
public void onStartup(Container container)
{
// access the ServiceLocator here
// serviceLocator = container.getApplicationHandler().
ServiceLocator serviceLocator = ((ImmediateHk2InjectionManager)container
.getApplicationHandler().getInjectionManager()).getServiceLocator();
enableTopicDistribution(serviceLocator);
// ... do what you need with ServiceLocator ...
}
public void onReload(Container container) {/*...*/}
public void onShutdown(Container container) {/*...*/}
});
register(new AbstractBinder() {
@Override
protected void configure() {
// hk2 bindings
bindFactory(Factories.MailServiceFactory.class)
.to(EMailService.class)
.in(Singleton.class);
}
});
}
}
More Jersey
One of the features of Jersey that I like is @Provider mechanism. Here is an example of how to centralize error handling in the Jersey application.
@Provider
public class IZapExceptionMapper implements ExceptionMapper<IZapException> {
@Override
public Response toResponse(IZapException e) {
ErrorResponse response = MapperUtils.toReturn(e);
return Response.status(response.getStatus())
.entity(response)
.type(MediaType.APPLICATION_JSON_TYPE)
.build();
}
}Not shown here, but Jersey can actually inject beans into @Provider classes.
Other
I think one of the undervalued configuration/properties management libraries is com
<dependency> <groupId>com.typesafe</groupId> <artifactId>config</artifactId> </dependency>
private static final Config CONFIG = ConfigFactory.load();
// do some stuff
String emailFrom = CONFIG.getString("email.from");
Build
Some manual work needs to be done to package an application into a single executable Jar. Take a look at the following maven plugin:
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<archive>
<manifest>
<mainClass>com.itzap.message.app.EmailApplication</mainClass>
</manifest>
</archive>
<descriptors>
<descriptor>src/main/resources/assembly/itzap-message.xml</descriptor>
</descriptors>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</plugin>This plugin is using itzap-message.xml descriptor file
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>itzap-message</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>src</directory>
<excludes>
<exclude>**</exclude>
</excludes>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<unpack>true</unpack>
<unpackOptions>
<excludes>
<exclude>*.log</exclude>
<exclude>assembly/**</exclude>
<exclude>databases/**</exclude>
</excludes>
</unpackOptions>
</dependencySet>
</dependencySets>
</assembly>
I usually, unpack all dependencies and merge them into a final jar. There are other strategies for building executable Jar that can be configured in the descriptor file.
Deployment
I already mentioned docker-composedocker-compose.yml
version: '3.2'
services:
mail:
image: boky/postfix
environment:
ALLOWED_SENDER_DOMAINS: ${ALLOWED_DOMAINS}
ports:
- 1587:587
message:
# Builds message service
build: "."
image: message-service:latest
environment:
MAIL_HOST: mail
MAIL_FROM: ${MAIL_FROM}
privileged: true
ports:
- 8025:8025
links:
- mail
depends_on:
- mail
Here, you can see two mailmessageDockerfile
# Alpine Linux with OpenJDK JRE FROM openjdk:8-jre-alpine COPY message-impl/target/message-impl-0.0.1-SNAPSHOT.jar /opt/lib/ ENTRYPOINT ["/usr/bin/java"] CMD ["-jar", "/opt/lib/message-impl-0.0.1-SNAPSHOT.jar"] EXPOSE 8025
Notes

Readme
itzap-message micro-service project designed to send email messages. Visit my ITZap blog to read more about this project.
itzap-message desinged to run in a Docker container along with postfix that is also running in a Docker. Using docker-compose itzap-message can be deployed anyware and ready to send emails.
- Clone the following projects:
git clone git@github.com:avinokurov/itzap-parent.gitgit clone git@github.com:avinokurov/itzap-common.gitgit clone git@github.com:avinokurov/itzap-rxjava.gitgit clone git@github.com:avinokurov/itzap-message.git
- Build all projects
cd itzap-parent && mvn clean installcd ../itzap-common && mvn clean installcd ../itzap-rxjava && mvn clean installcd ../itzap-message && mvn clean install
- Running
- Before running set environment variables
export MAIL_FROM = mailer@test.comto whater email address will be used to send messages from andexport ALLOWED_DOMAINS=test.comdomain used to send emails. - To run the itzap-message micro-service
docker-compose up
- Before running set environment variables
- Testing
- Once both Docker containers are running open Postman and call micro-service API to send an email
POST http://localhost:8025/v1/itzap/message/emailBody
{ "messageId": "new-user", "subject": "test", "addresses": ["avinokurov@itzap.com"], "transport": "email" } - Once both Docker containers are running open Postman and call micro-service API to send an email
I in addition to my friends have been checking out the excellent tricks from your site while all of the sudden I got an awful feeling I never expressed respect to the web site owner for those tips. Those young men had been totally thrilled to study them and already have in truth been tapping into them. Thanks for simply being well considerate as well as for finding some ideal guides millions of individuals are really desperate to be aware of. My very own honest regret for not saying thanks to sooner.