Litmus Instant API Ruby

A ruby API client library for interacting with Litmus Instant API - the fastest way to include email previews from real clients in your application.

Installation

Add this line to your application's Gemfile:

gem "litmus-instant"

And then execute:

$ bundle

Or install it yourself as:

$ gem install litmus-instant

Usage

Require the library and set your Instant API key

require "litmus/instant"
Litmus::Instant.api_key = "<YOUR INSTANT API KEY>"

Prepare an email for capture

email_guid = Litmus::Instant.create_email(
  plain_text: "Aloha World!"
)["email_guid"]
# => "755d1f9f-ad28-460f-8e45-632e0eceab32"

Construct a preview URL for embedding client side or for downloading

@preview_url = Litmus::Instant.preview_image_url(email_guid, "OL2010")
# => "https://OL2010.instant-api.litmus.com/v1/emails/755d1f9f-ad28-460f-8e45-632e0eceab32/previews/OL2010/full"

This could be used in a Rails erb template like so

<%= image_tag @preview_url %>

OAuth and API client instantiation

Individual API client objects can be instantiated and expose an identical interface to the class methods on Litmus::Instant, this the recommended approach for acting on behalf of multiple Litmus users within the same application, as each client can be configured with an OAuth token for each authorized user in a thread safe manner:

api_client = Litmus::Instant::Client.new(oauth_token: "XXX")
api_client.create_email(plain_text: "Goodbye world.")

Performance

In the example above the capture wouldn't be initiated until the end user's browser made the HTTP GET request to the preview URL. This would mean waiting the full capture time (a number of seconds) before the image data began to transfer.

Valuable time can be shaved here by pre-requesting required capture configurations as early as it's known they're needed. By the time the browser comes to request the preview the capture will be in progress or completed, so transfer of the image data will begin sooner. This also avoids browser connection limits delaying the initiation of capture for previews queued behind other requests.

This pre-request can be provided during email creation

Litmus::Instant.create_email(
  plain_text: "Aloha World!",
  configurations: [
    { client: "OL2010" },
    { client: "OL2013", images: "blocked" }
  ]
)
# => {"email_guid"=>"99e313eb-5256-4824-b26f-2f9e031fa8b5",
#     "configurations"=>
#       [{"orientation"=>"vertical", "images"=>"allowed", "client"=>"OL2010"},
#        {"orientation"=>"vertical", "images"=>"blocked", "client"=>"OL2013"}]}

or via the dedicated method

Litmus::Instant.prefetch_previews(
  email_guid, [
    { client: "OL2010" },
    { client: "OL2013", images: "blocked" }
  ]
)
# => {"configurations"=>
#     [{"orientation"=>"vertical", "images"=>"allowed", "client"=>"OL2010"},
#      {"orientation"=>"vertical", "images"=>"blocked", "client"=>"OL2013"}]}

For further information see the performance section of the Instant API documentation.

Handling errors

Various errors may occur can occur during normal usage of the API, please code defensively to handle these.

begin
  # make Litmus::Instant api call
rescue Litmus::Instant::AuthenticationError => e
  # eg an invalid API key, or API key not set
rescue Litmus::Instant::RequestError => e
  # eg an invalid client configuration was requested
rescue Litmus::Instant::NotFound => e
  # The most likely cause of this is an invalid email_guid, or expired email
rescue Litmus::Instant::TimeoutError => e
  # An email client may timeout for various reasons, including upstream service
  # issues for webmail clients, or unexpected behaviour with problematic email
  # source
rescue Litmus::Instant::ServiceError => e
  # eg a capacity issue or unexpected infrastracture issue
  # This should occur extremely rarely
rescue Litmus::Instant::ApiError => e
  # Catch all the above and any other API failure responses
rescue Litmus::Instant::NetworkError => e
  # Convenience wrapper around general ruby networking errors
rescue Litmus::Instant::Error => e
  # Base error class, parent of everything above
end

Note that the default behaviour of the the embedable URL returned from Litmus::Instant.preview_image_url is to redirect to a fallback image if an error occurs during capture. This behavior can be overriden to raise errors by setting the fallback option to false.

Downloading previews server-side

The gem itself doesn't provide a dedicated method for downloading raw image data, only generating the URLs to download from. This was a conscious decision to leave the choice of tooling used for downloading the binary data open to the developer.

In the simplest case, downloading a single preview to a tempfile can be achieved with Ruby's OpenURI

require "open-uri"
open @preview_url
# ...blocks for a few seconds...
# => #<File:/var/folders/st/813n6d9d0ts_hj8ktmx0p0hh0000gn/T/open-uri20151007-51192-k3a53m>

A common use case is downloading a batch of previews for a selection of email clients. Often doing this as fast as possible is important for the desired experience in a waiting client application.

In this situation we recommend:

  • pre-requesting known clients before download
  • making parallel HTTP requests, with concurrency tuned to minimise wait time, maximise bandwidth usage, but minimise network contention issues.

When constrained by bandwidth, simply pre-requesting and then sequentially downloading one preview after another may yield the fastest completion time. In most situations, exceeding 15 concurrent connections is unlikely to improve overall completion time.

For managing parallel downloads in ruby, you could use multiple threads and Net:HTTP, em-http-request, or shell out to wget or aria2, however our example below uses Typhoeus which wraps libcurl.

require "typhoeus"

# prerequest capture on all clients in their default configuration
clients = Litmus::Instant.clients
configurations = clients.map { |client| { client: client } }
Litmus::Instant.prefetch_previews(email_guid, configurations)

hydra = Typhoeus::Hydra.new(max_concurrency: 15)
clients.each do |client|
  preview_url = Litmus::Instant.preview_image_url(email_guid, client, capture_size: "thumb")
  request = Typhoeus::Request.new(preview_url, followlocation: true)
  request.on_complete do |response|
    File.write("/tmp/#{email_guid}-#{client}.png", response.body)
  end
  hydra.queue(request)
end
hydra.run

Development

After checking out the repo, run bin/setup to install dependencies. Then, with the API_KEY environment variable set appropriately, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/litmus/instant-api-ruby.

License

The gem is available as open source under the terms of the MIT License.