require 'sailthru/helpers'
require 'sailthru/version'
require 'net/http'
require 'net/http/post/multipart'
require 'uri'
require 'cgi'
require 'json'
module Sailthru
class Client
DEFAULT_API_URI = 'https://api.sailthru.com'
include Helpers
attr_accessor :verify_ssl
# params:
# api_key, String
# secret, String
# api_uri, String
#
# Instantiate a new client; constructor optionally takes overrides for key/secret/uri and proxy server settings.
def initialize(api_key=nil, secret=nil, api_uri=nil, proxy_host=nil, proxy_port=nil, opts={})
@api_key = api_key || Sailthru.api_key || raise(ArgumentError, "You must provide an API key or call Sailthru.credentials() first")
@secret = secret || Sailthru.secret || raise(ArgumentError, "You must provide your secret or call Sailthru.credentials() first")
@api_uri = api_uri.nil? ? DEFAULT_API_URI : api_uri
@proxy_host = proxy_host
@proxy_port = proxy_port
@verify_ssl = true
@opts = opts
@last_rate_limit_info = {}
end
# params:
# template_name, String
# email, String
# vars, Hash
# options, Hash
# replyto: override Reply-To header
# test: send as test email (subject line will be marked, will not count towards stats)
# returns:
# Hash, response data from server
def send_email(template_name, email, vars={}, options = {}, schedule_time = nil, limit = {})
post = {}
post[:template] = template_name
post[:email] = email
post[:vars] = vars if vars.length >= 1
post[:options] = options if options.length >= 1
post[:schedule_time] = schedule_time if !schedule_time.nil?
post[:limit] = limit if limit.length >= 1
api_post(:send, post)
end
def multi_send(template_name, emails, vars={}, options = {}, schedule_time = nil, evars = {})
post = {}
post[:template] = template_name
post[:email] = emails
post[:vars] = vars if vars.length >= 1
post[:options] = options if options.length >= 1
post[:schedule_time] = schedule_time if !schedule_time.nil?
post[:evars] = evars if evars.length >= 1
api_post(:send, post)
end
# params:
# send_id, Fixnum
# returns:
# Hash, response data from server
#
# Get the status of a send.
def get_send(send_id)
api_get(:send, {:send_id => send_id.to_s})
end
def cancel_send(send_id)
api_delete(:send, {:send_id => send_id.to_s})
end
# params:
# name, String
# list, String
# schedule_time, String
# from_name, String
# from_email, String
# subject, String
# content_html, String
# content_text, String
# options, Hash
# returns:
# Hash, response data from server
#
# Schedule a mass mail blast
def schedule_blast(name, list, schedule_time, from_name, from_email, subject, content_html, content_text, options = {})
post = options ? options : {}
post[:name] = name
post[:list] = list
post[:schedule_time] = schedule_time
post[:from_name] = from_name
post[:from_email] = from_email
post[:subject] = subject
post[:content_html] = content_html
post[:content_text] = content_text
api_post(:blast, post)
end
# Schedule a mass mail blast from template
def schedule_blast_from_template(template, list, schedule_time, options={})
post = options ? options : {}
post[:copy_template] = template
post[:list] = list
post[:schedule_time] = schedule_time
api_post(:blast, post)
end
# Schedule a mass mail blast from previous blast
def schedule_blast_from_blast(blast_id, schedule_time, options={})
post = options ? options : {}
post[:copy_blast] = blast_id
#post[:name] = name
post[:schedule_time] = schedule_time
api_post(:blast, post)
end
# params
# blast_id, Fixnum | String
# name, String
# list, String
# schedule_time, String
# from_name, String
# from_email, String
# subject, String
# content_html, String
# content_text, String
# options, hash
#
# updates existing blast
def update_blast(blast_id, name = nil, list = nil, schedule_time = nil, from_name = nil, from_email = nil, subject = nil, content_html = nil, content_text = nil, options = {})
data = options ? options : {}
data[:blast_id] = blast_id
if name != nil
data[:name] = name
end
if list != nil
data[:list] = list
end
if schedule_time != nil
data[:schedule_time] = schedule_time
end
if from_name != nil
data[:from_name] = from_name
end
if from_email != nil
data[:from_email] = from_email
end
if subject != nil
data[:subject] = subject
end
if content_html != nil
data[:content_html] = content_html
end
if content_text != nil
data[:content_text] = content_text
end
api_post(:blast, data)
end
# params:
# blast_id, Fixnum | String
# options, hash
# returns:
# Hash, response data from server
#
# Get information on a previously scheduled email blast
def get_blast(blast_id, options={})
options[:blast_id] = blast_id.to_s
api_get(:blast, options)
end
# params:
# blast_id, Fixnum | String
#
# Cancel a scheduled Blast
def cancel_blast(blast_id)
api_post(:blast, {:blast_id => blast_id, :schedule_time => ''})
end
# params:
# blast_id, Fixnum | String
#
# Delete a Blast
def delete_blast(blast_id)
api_delete(:blast, {:blast_id => blast_id})
end
# params:
# email, String
# returns:
# Hash, response data from server
#
# Return information about an email address, including replacement vars and lists.
def get_email(email)
api_get(:email, {:email => email})
end
# params:
# email, String
# vars, Hash
# lists, Hash mapping list name => 1 for subscribed, 0 for unsubscribed
# options, Hash mapping optional parameters
# returns:
# Hash, response data from server
#
# Set replacement vars and/or list subscriptions for an email address.
def set_email(email, vars = {}, lists = {}, templates = {}, options = {})
data = options
data[:email] = email
data[:vars] = vars unless vars.empty?
data[:lists] = lists unless lists.empty?
data[:templates] = templates unless templates.empty?
api_post(:email, data)
end
# params:
# new_email, String
# old_email, String
# options, Hash mapping optional parameters
# returns:
# Hash of response data.
#
# change a user's email address.
def change_email(new_email, old_email, options = {})
data = options
data[:email] = new_email
data[:change_email] = old_email
api_post(:email, data)
end
# returns:
# Hash of response data.
#
# Get all templates
def get_templates(templates = {})
api_get(:template, templates)
end
# params:
# template_name, String
# returns:
# Hash of response data.
#
# Get a template.
def get_template(template_name)
api_get(:template, {:template => template_name})
end
# params:
# template_name, String
# template_fields, Hash
# returns:
# Hash containg response from the server.
#
# Save a template.
def save_template(template_name, template_fields)
data = template_fields
data[:template] = template_name
api_post(:template, data)
end
# params:
# template_name, String
# returns:
# Hash of response data.
#
# Delete a template.
def delete_template(template_name)
api_delete(:template, {:template => template_name})
end
# params:
# params, Hash
# request, String
# returns:
# boolean, Returns true if the incoming request is an authenticated verify post.
def receive_verify_post(params, request)
if request.post?
[:action, :email, :send_id, :sig].each { |key| return false unless params.has_key?(key) }
return false unless params[:action] == :verify
sig = params.delete(:sig)
params.delete(:controller)
return false unless sig == get_signature_hash(params, @secret)
_send = get_send(params[:send_id])
return false unless _send.has_key?('email')
return false unless _send['email'] == params[:email]
return true
else
return false
end
end
# params:
# params, Hash
# request, String
# returns:
# TrueClass or FalseClass, Returns true if the incoming request is an authenticated optout post.
def receive_optout_post(params, request)
if request.post?
[:action, :email, :sig].each { |key| return false unless params.has_key?(key) }
return false unless params[:action] == 'optout'
sig = params.delete(:sig)
params.delete(:controller)
sig == get_signature_hash(params, @secret)
else
false
end
end
# List Postbacks must be enabled by Sailthru
# Contact your account manager or contact support to have this enabled
#
# params:
# params, Hash
# request, String
# returns:
# TrueClass or FalseClass, Returns true if the incoming request is an authenticated list post.
def receive_list_post(params, request)
if request.post?
[:action, :email, :sig].each { |key| return false unless params.has_key?(key) }
return false unless params[:action] == 'update'
sig = params.delete(:sig)
params.delete(:controller)
sig == get_signature_hash(params, @secret)
else
false
end
end
# params:
# params, Hash
# request, String
# returns:
# TrueClass or FalseClass, Returns true if the incoming request is an authenticated hardbounce post.
def receive_hardbounce_post(params, request)
if request.post?
[:action, :email, :sig].each { |key| return false unless params.has_key?(key) }
return false unless params[:action] == 'hardbounce'
sig = params.delete(:sig)
params.delete(:controller)
sig == get_signature_hash(params, @secret)
else
false
end
end
# params:
# email, String
# items, Array of Hashes
# incomplete, Integer
# message_id, String
# options, Hash
# returns:
# hash, response from server
#
# Record that a user has made a purchase, or has added items to their purchase total.
def purchase(email, items, incomplete = nil, message_id = nil, options = {})
data = options
data[:email] = email
data[:items] = items
if incomplete != nil
data[:incomplete] = incomplete.to_i
end
if message_id != nil
data[:message_id] = message_id
end
api_post(:purchase, data)
end
# DEPRECATED: Please use either stats_list or stats_blast
# params:
# stat, String
#
# returns:
# hash, response from server
# Request various stats from Sailthru.
def get_stats(stat)
warn "[DEPRECATION] `get_stats` is deprecated. Please use `stats_list` and `stats_blast` instead"
api_get(:stats, {:stat => stat})
end
# params
# list, String
# date, String
#
# returns:
# hash, response from server
# Retrieve information about your subscriber counts on a particular list, on a particular day.
def stats_list(list = nil, date = nil)
data = {}
if list != nil
data[:list] = list
end
if date != nil
data[:date] = date
end
data[:stat] = 'list'
api_get(:stats, data)
end
# params
# blast_id, String
# start_date, String
# end_date, String
# options, Hash
#
# returns:
# hash, response from server
# Retrieve information about a particular blast or aggregated information from all of blasts over a specified date range
def stats_blast(blast_id = nil, start_date = nil, end_date = nil, options = {})
data = options
if blast_id != nil
data[:blast_id] = blast_id
end
if start_date != nil
data[:start_date] = start_date
end
if end_date != nil
data[:end_date] = end_date
end
data[:stat] = 'blast'
api_get(:stats, data)
end
# params
# template, String
# start_date, String
# end_date, String
# options, Hash
#
# returns:
# hash, response from server
# Retrieve information about a particular blast or aggregated information from all of blasts over a specified date range
def stats_send(template = nil, start_date = nil, end_date = nil, options = {})
data = options
if template != nil
data[:template] = template
end
if start_date != nil
data[:start_date] = start_date
end
if end_date != nil
data[:end_date] = end_date
end
data[:stat] = 'send'
api_get(:stats, data)
end
# DEPRECATED: Please use save_content
# params
# title, String
# url, String
# date, String
# tags, Array or Comma separated string
# vars, Hash
# options, Hash
#
# Push a new piece of content to Sailthru, triggering any applicable alerts.
# http://docs.sailthru.com/api/content
def push_content(title, url, date = nil, tags = nil, vars = {}, options = {})
data = options
data[:title] = title
data[:url] = url
if date != nil
data[:date] = date
end
if tags != nil
if tags.class == Array
tags = tags.join(',')
end
data[:tags] = tags
end
if vars.length > 0
data[:vars] = vars
end
api_post(:content, data)
end
# params
# id, String – An identifier for the item (by default, the item’s URL).
# options, Hash - Containing any of the parameters described on
# https://getstarted.sailthru.com/developers/api/content/#POST_Mode
#
# Push a new piece of content to Sailthru, triggering any applicable alerts.
# http://docs.sailthru.com/api/content
def save_content(id, options)
data = options
data[:id] = id
data[:tags] = data[:tags].join(',') if data[:tags].respond_to?(:join)
api_post(:content, data)
end
# params
# list, String
#
# Get information about a list.
def get_list(list)
api_get(:list, {:list => list})
end
# params
#
# Get information about all lists
def get_lists
api_get(:list, {})
end
# params
# list, String
# options, Hash
# Create a list, or update a list.
def save_list(list, options = {})
data = options
data[:list] = list
api_post(:list, data)
end
# params
# list, String
#
# Deletes a list
def delete_list(list)
api_delete(:list, {:list => list})
end
# params
# email, String
#
# get user alert data
def get_alert(email)
api_get(:alert, {:email => email})
end
# params
# email, String
# type, String
# template, String
# _when, String
# options, hash
#
# Add a new alert to a user. You can add either a realtime or a summary alert (daily/weekly).
# _when is only required when alert type is weekly or daily
def save_alert(email, type, template, _when = nil, options = {})
data = options
data[:email] = email
data[:type] = type
data[:template] = template
if (type == 'weekly' || type == 'daily')
data[:when] = _when
end
api_post(:alert, data)
end
# params
# email, String
# alert_id, String
#
# delete user alert
def delete_alert(email, alert_id)
data = {:email => email, :alert_id => alert_id}
api_delete(:alert, data)
end
# params
# job, String
# options, hash
# report_email, String
# postback_url, String
# binary_key, String
#
# interface for making request to job call
def process_job(job, options = {}, report_email = nil, postback_url = nil, binary_key = nil)
data = options
data['job'] = job
if !report_email.nil?
data['report_email'] = report_email
end
if !postback_url.nil?
data['postback_url'] = postback_url
end
api_post(:job, data, binary_key)
end
# params
# emails, String | Array
# implementation for import_job
def process_import_job(list, emails, report_email = nil, postback_url = nil, options = {})
data = options
data['list'] = list
data['emails'] = Array(emails).join(',')
process_job(:import, data, report_email, postback_url)
end
# implementation for import job using file upload
def process_import_job_from_file(list, file_path, report_email = nil, postback_url = nil, options = {})
data = options
data['list'] = list
data['file'] = file_path
process_job(:import, data, report_email, postback_url, 'file')
end
# implementation for update job using file upload
def process_update_job_from_file(file_path, report_email = nil, postback_url = nil, options = {})
data = options
data['file'] = file_path
process_job(:update, data, report_email, postback_url, 'file')
end
# implementation for purchase import job using file upload
def process_purchase_import_job_from_file(file_path, report_email = nil, postback_url = nil, options = {})
data = options
data['file'] = file_path
process_job(:purchase_import, data, report_email, postback_url, 'file')
end
# implementation for snapshot job
def process_snapshot_job(query = {}, report_email = nil, postback_url = nil, options = {})
data = options
data['query'] = query
process_job(:snapshot, data, report_email, postback_url)
end
# implementation for export list job
def process_export_list_job(list, report_email = nil, postback_url = nil, options = {})
data = options
data['list'] = list
process_job(:export_list_data, data, report_email, postback_url)
end
# get status of a job
def get_job_status(job_id)
api_get(:job, {'job_id' => job_id})
end
# Get user by Sailthru ID
def get_user_by_sid(id, fields = {})
api_get(:user, {'id' => id, 'fields' => fields})
end
# Get user by specified key
def get_user_by_key(id, key, fields = {})
data = {
'id' => id,
'key' => key,
'fields' => fields
}
api_get(:user, data)
end
# Create new user, or update existing user
def save_user(id, options = {})
data = options
data['id'] = id
api_post(:user, data)
end
# params
# Get an existing trigger
def get_triggers
api_get(:trigger, {})
end
# params
# template, String
# trigger_id, String
# Get an existing trigger
def get_trigger_by_template(template, trigger_id = nil)
data = {}
data['template'] = template
if trigger_id != nil then data['trigger_id'] = trigger_id end
api_get(:trigger, data)
end
# params
# event, String
# Get an existing trigger
def get_trigger_by_event(event)
data = {}
data['event'] = event
api_get(:trigger, data)
end
# params
# template, String
# time, String
# time_unit, String
# event, String
# zephyr, String
# Create or update a trigger
def post_template_trigger(template, time, time_unit, event, zephyr)
data = {}
data['template'] = template
data['time'] = time
data['time_unit'] = time_unit
data['event'] = event
data['zephyr'] = zephyr
api_post(:trigger, data)
end
# params
# template, String
# time, String
# time_unit, String
# zephyr, String
# Create or update a trigger
def post_event_trigger(event, time, time_unit, zephyr)
data = {}
data['time'] = time
data['time_unit'] = time_unit
data['event'] = event
data['zephyr'] = zephyr
api_post(:trigger, data)
end
# params
# id, String
# event, String
# options, Hash (Can contain vars, Hash and/or key)
# Notify Sailthru of an Event
def post_event(id, event, options = {})
data = options
data['id'] = id
data['event'] = event
api_post(:event, data)
end
# Perform API GET request
def api_get(action, data)
api_request(action, data, 'GET')
end
# Perform API POST request
def api_post(action, data, binary_key = nil)
api_request(action, data, 'POST', binary_key)
end
#Perform API DELETE request
def api_delete(action, data)
api_request(action, data, 'DELETE')
end
# params
# endpoint, String a e.g. "user" or "send"
# method, String "GET" or "POST"
# returns
# Hash rate info
# Get rate info for a particular endpoint/method, as of the last time a request was sent to the given endpoint/method
# Includes the following keys:
# limit: the per-minute limit for the given endpoint/method
# remaining: the number of allotted requests remaining in the current minute for the given endpoint/method
# reset: unix timestamp of the top of the next minute, when the rate limit will reset
def get_last_rate_limit_info(endpoint, method)
rate_info_key = get_rate_limit_info_key(endpoint, method)
@last_rate_limit_info[rate_info_key]
end
protected
# params:
# action, String
# data, Hash
# request, String "GET" or "POST"
# returns:
# Hash
#
# Perform an API request, using the shared-secret auth hash.
#
def api_request(action, data, request_type, binary_key = nil)
if !binary_key.nil?
binary_key_data = data[binary_key]
data.delete(binary_key)
end
if data[:format].nil? || data[:format] == 'json'
data = prepare_json_payload(data)
else
data[:api_key] = @api_key
data[:format] ||= 'json'
data[:sig] = get_signature_hash(data, @secret)
end
if !binary_key.nil?
data[binary_key] = binary_key_data
end
_result = http_request(action, data, request_type, binary_key)
# NOTE: don't do the unserialize here
if data[:format] == 'json'
begin
unserialized = JSON.parse(_result)
return unserialized ? unserialized : _result
rescue JSON::JSONError => e
return {'error' => e}
end
end
_result
end
# set up our post request
def set_up_post_request(uri, data, headers, binary_key = nil)
if !binary_key.nil?
binary_data = data[binary_key]
if binary_data.is_a?(StringIO)
data[binary_key] = UploadIO.new(
binary_data, "text/plain", "local.path"
)
else
data[binary_key] = UploadIO.new(
File.open(binary_data), "text/plain"
)
end
req = Net::HTTP::Post::Multipart.new(uri.path, data)
else
req = Net::HTTP::Post.new(uri.path, headers)
req.set_form_data(data)
end
req
end
# params:
# uri, String
# data, Hash
# method, String "GET" or "POST"
# returns:
# String, body of response
def http_request(action, data, method = 'POST', binary_key = nil)
data = flatten_nested_hash(data, false)
uri = "#{@api_uri}/#{action}"
if method != 'POST'
uri += "?" + data.map{ |key, value| "#{CGI::escape(key.to_s)}=#{CGI::escape(value.to_s)}" }.join("&")
end
req = nil
headers = {"User-Agent" => "Sailthru API Ruby Client #{Sailthru::VERSION}"}
_uri = URI.parse(uri)
if method == 'POST'
req = set_up_post_request(
_uri, data, headers, binary_key
)
else
request_uri = "#{_uri.path}?#{_uri.query}"
if method == 'DELETE'
req = Net::HTTP::Delete.new(request_uri, headers)
else
req = Net::HTTP::Get.new(request_uri, headers)
end
end
begin
http = Net::HTTP::Proxy(@proxy_host, @proxy_port).new(_uri.host, _uri.port)
if _uri.scheme == 'https'
http.ssl_version = :TLSv1_2
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @verify_ssl != true # some openSSL client doesn't work without doing this
http.ssl_timeout = @opts[:http_ssl_timeout] || 5
end
http.open_timeout = @opts[:http_open_timeout] || 5
http.read_timeout = @opts[:http_read_timeout] || 10
http.close_on_empty_response = @opts[:http_close_on_empty_response] || true
response = http.start do
http.request(req)
end
rescue Timeout::Error, Errno::ETIMEDOUT => e
raise UnavailableError, "Timed out: #{_uri}"
rescue => e
raise ClientError, "Unable to open stream to #{_uri}: #{e.message}"
end
save_rate_limit_info(action, method, response)
response.body || raise(ClientError, "No response received from stream: #{_uri}")
end
def http_multipart_request(uri, data)
Net::HTTP::Post::Multipart.new url.path,
"file" => UploadIO.new(data['file'], "application/octet-stream")
end
def prepare_json_payload(data)
payload = {
:api_key => @api_key,
:format => 'json', #<3 XML
:json => data.to_json
}
payload[:sig] = get_signature_hash(payload, @secret)
payload
end
def save_rate_limit_info(action, method, response)
limit = response['x-rate-limit-limit'].to_i
remaining = response['x-rate-limit-remaining'].to_i
reset = response['x-rate-limit-reset'].to_i
if limit.nil? or remaining.nil? or reset.nil?
return
end
rate_info_key = get_rate_limit_info_key(action, method)
@last_rate_limit_info[rate_info_key] = {
limit: limit,
remaining: remaining,
reset: reset
}
end
def get_rate_limit_info_key(endpoint, method)
:"#{endpoint}_#{method.downcase}"
end
end
end