Simple email notification micro service using Eclipse Vert.x

Yuri Mednikov
5 min readJun 15, 2020

Almost any more or less complex application has to deliver notifications. As usual, it is done via traditional emails, especially in cases of password reset messages or transactional notifications etc. There is a number of cloud solutions that abstract mail flow via consumable REST API. For example, Postmark. I used Postmark for some time, but stopped with a question of scalability. As I have several apps, that need to send emails to users, I decided to create a unified service that will do this dirty job.

Under the hood, I use SMTP server, but I abstract it via REST API, so consumers can interact with it in easy and what is most important, as most of them written as reactive services, in non/blocking manner. There is a lack of reliable non-blocking SMTP libraries for Java, and from the other hand, it can be painful to sit and wrap existing blocking solutions. Much better is to decompose such functionality into a separate microservice, that will work with SMTP server. In this post I will briefly overivew a simplified version of such app. It uses Vertx and exposes HTTP REST API to external callers.

The initial prototype for this post used AQMP to interact with consumers, however I decided to write this post about more widespread HTTP option.

How service does work

I want to separate a question of app’s architecture into two levels: a helicopter view on a whole flow (e.g. how app is built-in in a process) and an overview of app’s structure itself.

Let first note, how this service works with others. Assume, that we have external apps that would need to send emails to users. They send post request to the service, and the service in its turn trigger SMTP server in order to deliver email. Take a look on the graph below:

From other side, the service is written as a typical Eclipse Vertx app and consists of several components: verticles (independent units of computation), configuration provider (used to abstract various configuration stores from the code) and application class (an entry point of execution). Verticles are deployed and connected with each other using a simple request-response messaging model implemented with async Vertx Eventbus communication. Observe the diagram below:

That is what we would build throughout this post. Let start with HttpVerticle – a component, which is responsible for listening for incoming HTTP requests and providing responses.

Listen for requests (HttpVerticle)

In the world of Vertx we use Vertx-Web library in order to handle tasks, connected to web servers. From a technical point of view, we have several components here:

  • Router = this class handles all stuff with routing and paths
  • HttpServer = this class works with HTTP requests

Take a look on the code snippet below, which consumes incoming POST request and deals with response:

@Override
public void start(Promise<Void> startPromise) throws Exception {
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
EventBus eventBus = vertx.eventBus();

router.route("/*").handler(BodyHandler.create());
router.post("/message").handler(context -> {

// here comes handler
context.response().end("Hello!");
});

server.requestHandler(router::accept);
server.listen(config().getInteger("app.port"), result -> {
if (result.succeeded()) {
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
});
}

Another part of this service is sending a request to the SmtpVerticle and processing its response. We already mentioned a request response messaging architecture, that we use in this example. In Vertx, messaging between components is handled using EventBus. That means we need to specify request() handler:

//...
router.post("/message").handler(context -> {
JsonObject payload = context.getBodyAsJson();
eventBus.request("smtp.send", payload, result -> {
if (result.succeeded()) {
JsonObject response = JsonObject.mapFrom(result.result().body());
context.response().end(response.encodePrettily());
} else {
String cause = result.cause().getLocalizedMessage();
JsonObject response = new JsonObject();
response.put("status", cause);
context.response().setStatusCode(500).end(response.encodePrettily());
}
});
});

That is how we communicate with SmtpVerticle and receive a result of async message sending. Next step is to implement it.

Send emails (SmtpVerticle)

Our app does not send messages “as it is”; rather we incorporate an exisiting SMTP server to do this actual job for us. But in order to communicate with such server from Java code, we need to have a library. Lucky, Vertx ecosystem has Vertx-Mail package that allows to send messages in non-blocking manner. Let observe how messaging flow works:

  1. Prepare MailConfig object with configuration options
  2. Create MailClient instance = this is actual SMTP client
  3. Create MailMessage = this entity represents a mail message that can be sent via the MailClient
  4. Call MailClient.sendMail() method to trigger actual sending. This method provides asynchronous handler, that we will use to get a result of operation

First we need to register EventBus ‘s consumer that will listen to messages:

@Override
public void start(Promise<Void> startPromise) throws Exception {
EventBus eventBus = vertx.eventBus();
eventBus.consumer("smtp.send", this::sendMessage);
MailConfig mailConfig = new MailConfig();
mailConfig.setHostname(config().getString("mail.hostname"));
mailConfig.setPort(config().getInteger("mail.port"));
mailConfig.setUsername(config().getString("mail.username"));
mailConfig.setPassword(config().getString("mail.password"));
this.mailClient = MailClient.create(vertx, mailConfig);
startPromise.complete();
}

Here we can now implement the aforesaid email flow:

private void sendMessage (Message<Object> message) {
JsonObject payload = JsonObject.mapFrom(message.body());
String to = payload.getString("to");
String from = payload.getString("from");
String subject = payload.getString("subject");
String body = payload.getString("body");
MailMessage mailMessage = new MailMessage();
mailMessage.setFrom(from);
mailMessage.setHtml(body);
mailMessage.setSubject(subject);
mailMessage.setTo(to);
mailClient.sendMail(mailMessage, result ->{
if (result.succeeded()){
JsonObject response = new JsonObject();
response.put("status", true);
message.reply(response);
} else {
message.fail(0, result.cause().getLocalizedMessage());
}
});
}

As for now we have two working verticles. The last part is to assemble them in an app. Also, you could note a mysterious config() method, that I use in order to get configuration options, such as port number or SMTP credentials. In the next section we will talk about it more in details.

Configuration questions

A service can receive configuration from various sources. In this example we can use local properties for testing and env vars for deployment. In order to abstract the difference, Vertx uses Vertx-Config library. It offers a facade for different config sources. To use it, we need to obtain ConfigRetriever. Once this component got the actual config, we can supply it with DeploymentOptions to verticles. Here we just call built-in config() method to obtain it.

Let see how deployment process is done:

public static void main(String[] args) {

//..get config retriever options

Vertx vertx = Vertx.vertx();
ConfigRetriever configRetriever =
ConfigRetriever.create(vertx, configRetrieverOptions);

WebVerticle web = new WebVerticle();
SmtpVerticle smtp = new SmtpVerticle();

configRetriever.getConfig(config -> {
if (config.succeeded()) {
DeploymentOptions options = new DeploymentOptions();
options.setConfig(config.result());
vertx.deployVerticle(smtp, options, result -> {
if (result.succeeded()) {
vertx.deployVerticle(web, options, result2 -> {
if (result2.succeeded()) {
System.out.println("App deployed");
} else {
System.out.println(result2.cause().getLocalizedMessage());
vertx.close();
}
});
} else {
System.out.println(result.cause().getLocalizedMessage());
vertx.close();
}
});
} else {
config.cause().printStackTrace();
vertx.close();
}
});
}

Vertx uses ConfigRetrieverOptions to define possible stores. In this example we use local properties configuration. Take a look on the code snippet below:

ConfigStoreOptions configStoreOptions = new ConfigStoreOptions();
configStoreOptions.setType("file");
configStoreOptions.setFormat("properties");
configStoreOptions.setConfig(new JsonObject()
.put("path", "config.properties"));
ConfigRetrieverOptions configRetrieverOptions = new ConfigRetrieverOptions();
configRetrieverOptions.addStore(configStoreOptions);

///...

And in the end, app runs using old good main() method. You can test it and verify that it works, using your SMTP server credentials.

Of course, this is just a proof of concept. Many important topics are ommited, for instance, authentication and security, or better error handling flow. So, feel free to think on them. If you have questions regarding this post, drop them below or use my contacts to connect with me.

--

--