Sending email via a web API

This is part four in a series* on using web APIs to accomplish useful things in a PicoStack web application.

We have talked about consuming web APIs in a read-only fashion, safeguarding web API secrets when they are required, and wrapping the complexity of an API in a ruleset used as a module.

In this post, we'll use all of these techniques and demonstrate how to use a web API to send simple text email messages.

Thinking about how to send email

Should we use SMTP or a web API?

There are basically two broad options for sending email messages: SMTP**, the standard protocol; or selecting one from the very many commercial services that provide a web API. 

Should we build the functionality into a KRL library?

A third possibility is to choose an open source code base and integrate it into the pico engine as a library. This was done in the very first version of KRL, which had a sendgrid library built in***. We chose not to pursue this path, since it would require a great deal of time and coding in Node JS, preferring to use a KRL only approach with a web API.

Choosing a web API

The quickest way from zero to a sent email message, we decided, was to choose a commercial service that offers an API.

We did a quick web search, and without much in the way of due diligence, chose mailjet. It offers full support for transactional email messaging at volume, and seems to have been around for awhile. Lots of room to grow, yet starting with a free tier allowing many email messages a day.

Setting up service

The first step was to sign up, supplying an email address and password, along with a few other questions, like our organization (we used "self") and our website (the author's resume). They sent us an email message to verify our control of the email address, and registered it as an email address that can be used to send email through their API.

The second step was to request an API key and a secret key. We recorded these in a text file, along with the secrets from a previous post. This file now looks like this (with the actual secrets redacted):

$ cat secrets.tsv 
org.themoviedb.sdk	{"api_key":"redacted"}
com.mailjet.sdk	{"api_key":"s1","secret_key":"s2","email":"s3","name":"s4"}

Of the mailjet secrets, only the secret_key actually needs to be kept secret, so that the others could be hard-coded into a wrapper. But the four items listed are things that are peculiar to a mailjet account, and we'll want to be configuring our SDK wrapper with these things so that it can be used by any pico programmer who installs it.

$ grep mailjet secrets.tsv | cut -f 2 | pbcopy 

Above is the command that we use to extract just the map of secrets for our mailjet wrapper, and place them in the copy buffer (on a macbook). Then we can conveniently paste into the "Config" box when we are installing the wrapper ruleset.

Writing a wrapper

We've been hinting that there will be a wrapper ruleset. Here is the first part of it:

ruleset com.mailjet.sdk {
  meta {
    provides send_first, send_text
  }
  global {
    api_key = meta:rulesetConfig{"api_key"}
    secret_key = meta:rulesetConfig{"secret_key"}
    basic = {"username":api_key,"password":secret_key}
    email = meta:rulesetConfig{"email"}
    from_name = meta:rulesetConfig{"name"}

As before, we used a ruleset identifier (RID) that would belong to mailjet**** because it's going to be about their API. This ruleset will be used as a module, and provides a send_text defined action (whose definition we'll see in the second part).

The first five lines of the global block give us names for the various configuration items. They also prepare the information for basic authentication, in a map named basic. Whereas our earlier API required us to pass the API key in the URL, this one requires the use of an HTTP header to pass both the API key and the secret key. We'll see how that's done in this second part:

    send_url = "https://api.mailjet.com/v3.1/send"
    send_text = defaction(to,subject,text){
      msg = {
        "From":{"Email":email,"Name":from_name},
        "To": [{"Email":to}],
        "Subject": subject,
        "TextPart": text,
      }
      msgs = {}.put("Messages",[msg])
      http:post(send_url,auth=basic,json=msgs) setting(response)
      return response
    }

The actual call of the API to send a simple text message is the http:post in the second-last line of the defaction but first we have to prepare the message in the format required by the API (as described here in mailjet's documentation). We have included only what they strictly require, including our registered email address and configured name for the "From" item.

Notice the use of the auth parameter in the http:post action to implement the required basic authentication. See also the documentation of the KRL http library.

The third part (not shown here) of the wrapper is an implementation of a defined action, send_first, that is the KRL version of one of the first steps mailjet guides a new developer through. The complete wrapper can be installed from here. Be sure to configure the four items when you install it.

Sending a simple text message

Here is an extract from a test ruleset, which uses the wrapper ruleset as a module:

ruleset test_email {
  meta {
    use module com.mailjet.sdk alias email
  ...
  }
  rule sendMinimalMessage {
    select when test_email new_text_message
      to re#(.+)# subject re#(.+)# setting(to,subject)
    pre {
      text = event:attr("text") || ""
    }
    email:send_text(to,subject,text) setting(response)
    fired {
      raise test_email event "message_sent" attributes response
    }
  }
  ...
}

Notice that the module usage specifies an alias, email, which is used to call the action send_text defined in the wrapper.

This test rule requires an attribute to giving the recipient's email address, and a non-empty attribute subject for the subject of the text message, whereas the text attribute is not required, and defaults to the empty string.

Handling the response

The mailjet API responds with a standard HTTP response as a JSON object, which for now we simply store in an entity variable;

  rule handleResponse {
    select when test_email message_sent
    fired {
      ent:lastResponse := event:attrs
    }
  }

The test ruleset also shares a couple of functions to allow us to look at this response:

ruleset test_email {
  meta {
    ...
    shares last_response, last_content
  }
  global {
    last_response = function(){
      ent:lastResponse
    }
    last_content = function(){
      ent:lastResponse
        .get("content")
        .decode()
    }
  }

From mailjet, the content of the response is a string that can be decoded into a JSON object (a KRL map) for display purposes.

Putting it to work

  1. Sign up for a developer account with mailjet
  2. Obtain and save the secrets, adding to them your email address and name
  3. Install the wrapper ruleset in a pico, configuring it with the secrets
  4. Install the test ruleset
  5. Add a new channel allowing all events and queries for test_email
  6. Use the Testing tab to send your first email message, supplying your name
  7. Examine the response
  8. Use the email:send_text action defined in the wrapper from your own rules

Notes

* the fact that there is a series was unplanned; the series has emerged

** SMTP is the Simple Mail Transport Protocol which has been used on the Internet for many years. The reader can find many blog posts about the choice between SMTP and a web API by searching. Most of these are written partly to advertise some commercial service that offers an API. A recent post from Philo Hermans is indicative and appears neutral.

*** as used in a blog post from Phil Windley

**** they provide a number of wrappers, and "suggest using these official wrappers to access the Mailjet API." [emphasis added]

No comments:

Post a Comment