lib/sailthru.rb in sailthru-client-1.01 vs lib/sailthru.rb in sailthru-client-1.02
- old
+ new
@@ -1,374 +1,429 @@
-################################################################################
-#
-# A simple client library to remotely access the Sailthru REST API.
-#
-################################################################################
-#
-# Copyright (c) 2007 Sailthru, Inc.
-# All rights reserved.
-#
-# Special thanks to the iminlikewithyou.com team for the development
-# of this library.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
-# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
-# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
-# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
-# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
-# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-################################################################################
-
require 'net/http'
require 'uri'
require 'cgi'
require 'rubygems'
require 'json'
require 'md5'
-class SailthruClientException < Exception
-end
+module Sailthru
-class SailthruClient
- attr_accessor :api_uri, :api_key, :secret, :version, :last_request
-
- VERSION = '1.01'
+ Version = VERSION = '1.02'
- # params:
- # api_key, String
- # secret, String
- # api_uri, String
- #
- # Instantiate a new client; constructor optionally takes overrides for key/secret/uri.
- def initialize(api_key, secret, api_uri)
- @api_key = api_key
- @secret = secret
- @api_uri = api_uri
+ class SailthruClientException < Exception
end
-
- # params:
- # template_name, String
- # email, String
- # replacements, 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(template_name, email, vars, options = {}, schedule_time = nil)
- post = {}
- post[:template] = template_name
- post[:email] = email
- post[:vars] = vars
- post[:options] = options
- if schedule_time != nil
- post[:schedule_time] = schedule_time
+
+ module Helpers
+ # params:
+ # params, Hash
+ # returns:
+ # Array, values of each item in the Hash (and nested hashes)
+ #
+ # Extracts the values of a set of parameters, recursing into nested assoc arrays.
+ def extract_param_values(params)
+ values = []
+ params.each do |k, v|
+ if v.class == Hash
+ values.concat extract_param_values(v)
+ elsif v.class == Array
+ temp_hash = Hash.new()
+ v.each_with_index do |v_,i_|
+ temp_hash[i_.to_s] = v_
+ end
+ values.concat extract_param_values(temp_hash)
+ else
+ values.push v.to_s
+ end
+ end
+ return values
end
- return self.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)
- self.api_get(: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
- self.api_post(:blast, post)
- end
-
-
- # params:
- # blast_id, Fixnum
- # returns:
- # Hash, response data from server
- #
- # Get information on a previously scheduled email blast
- def get_blast(blast_id)
- self.api_get(:blast, {:blast_id => blast_id.to_s})
- 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)
- self.api_get(:email, {:email => email})
- end
-
- # params:
- # email, String
- # vars, Hash
- # lists, Hash mapping list name => 1 for subscribed, 0 for unsubscribed
- # returns:
- # Hash, response data from server
- #
- # Set replacement vars and/or list subscriptions for an email address.
- def set_email(email, vars = {}, lists = {}, templates = {})
- data = {:email => email}
- data[:vars] = vars unless vars.empty?
- data[:lists] = lists unless lists.empty?
- data[:templates] = templates unless templates.empty?
- self.api_post(:email, data)
- end
-
- # params:
- # email, String
- # password, String
- # with_names, Boolean
- # returns:
- # Hash, response data from server
- #
- # Fetch email contacts from an address book at one of the major email providers (aol/gmail/hotmail/yahoo)
- # Use the with_names parameter if you want to fetch the contact names as well as emails
- def import_contacts(email, password, with_names = false)
- data = { :email => email, :password => password }
- data[:names] = 1 if with_names
- self.api_post(:contacts, data)
- end
-
-
- # params:
- # template_name, String
- # returns:
- # Hash of response data.
- #
- # Get a template.
- def get_template(template_name)
- self.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
- self.api_post(:template, data)
- end
-
-
- # params:
- # params, Hash
- # request, String
- # returns:
- # TrueClass or FalseClass, 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[:sig]
- params.delete(:sig)
- return false unless sig == SailthruClient.get_signature_hash(params, @secret)
-
- _send = self.get_send(params[:send_id])
- return false unless _send.has_key?(:email)
-
- return false unless _send[:email] == params[:email]
-
- return true
- else
+ # params:
+ # params, Hash
+ # secret, String
+ # returns:
+ # String
+ #
+ # Returns the unhashed signature string (secret + sorted list of param values) for an API call.
+ def get_signature_string(params, secret)
+ return secret + extract_param_values(params).sort.join("")
+ end
+
+
+ # params:
+ # params, Hash
+ # secret, String
+ # returns:
+ # String
+ #
+ # Returns an MD5 hash of the signature string for an API call.
+ def get_signature_hash(params, secret)
+ MD5.md5(get_signature_string(params, secret)).to_s # debuggin
+ end
+
+
+ # Flatten nested hash for GET / POST request.
+ def flatten_nested_hash(hash, brackets = true)
+ f = {}
+ hash.each do |key, value|
+ _key = brackets ? "[#{key}]" : key.to_s
+ if value.class == Hash
+ flatten_nested_hash(value).each do |k, v|
+ f["#{_key}#{k}"] = v
+ end
+ elsif value.class == Array
+ temp_hash = Hash.new()
+ value.each_with_index do |v, i|
+ temp_hash[i.to_s] = v
+ end
+ flatten_nested_hash(temp_hash).each do |k, v|
+ f["#{_key}#{k}"] = v
+ end
+
+ else
+ f[_key] = value
+ end
+ end
+ return f
+ end
+
+ def verify_purchase_items (items)
+ if items.class == Array and !items.empty?
+ required_item_fields = ['qty', 'title', 'price', 'id', 'url'].sort
+ items.each do |v|
+ keys = v.keys.sort
+ return false if keys != required_item_fields
+ end
+ return true
+ end
return false
end
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)
- api_request(action, data, 'POST')
- end
-
- # 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)
- data[:api_key] = @api_key
- data[:format] ||= 'json'
- data[:sig] = SailthruClient.get_signature_hash(data, @secret)
- _result = self.http_request("#{@api_uri}/#{action}", data, request_type)
-
- # NOTE: don't do the unserialize here
- unserialized = JSON.parse(_result)
- return unserialized ? unserialized : _result
- end
-
-
- # params:
- # uri, String
- # data, Hash
- # method, String "GET" or "POST"
- # returns:
- # String, body of response
- def http_request(uri, data, method = 'POST')
- data = flatten_nested_hash(data, false)
- # puts data.inspect
- if method == 'POST'
- post_data = data
- else
- uri += "?" + data.map{ |key, value| "#{CGI::escape(key)}=#{CGI::escape(value)}" }.join("&")
+ class SailthruClient
+
+ include Helpers
+
+ # params:
+ # api_key, String
+ # secret, String
+ # api_uri, String
+ #
+ # Instantiate a new client; constructor optionally takes overrides for key/secret/uri.
+ def initialize(api_key, secret, api_uri)
+ @api_key = api_key
+ @secret = secret
+ @api_uri = api_uri
end
- req = nil
- headers = {"User-Agent" => "Sailthru API Ruby Client #{VERSION}"}
-
- _uri = URI.parse(uri)
- if method == 'POST'
- req = Net::HTTP::Post.new(_uri.path, headers)
- req.set_form_data(data)
- else
- req = Net::HTTP::Get.new("#{_uri.path}?#{_uri.query}", headers)
+
+ # params:
+ # template_name, String
+ # email, String
+ # replacements, 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(template_name, email, vars={}, options = {}, schedule_time = nil)
+ post = {}
+ post[:template] = template_name
+ post[:email] = email
+ post[:options] = options
+
+ if vars.length > 0
+ post[:vars] = vars
+ end
+
+ if schedule_time != nil
+ post[:schedule_time] = schedule_time
+ end
+ return self.api_post(:send, post)
end
-
- @last_request = req
- begin
- response = Net::HTTP.start(_uri.host, _uri.port) {|http|
- http.request(req)
- }
- rescue Exception => e
- raise SailthruClientException.new("Unable to open stream: #{_uri.to_s}");
+
+
+ def multi_send(template_name, emails, vars={}, options = {}, schedule_time = nil)
+ post = {}
+ post[:template] = template_name
+ post[:email] = emails
+ post[:options] = options
+
+ if schedule_time != nil
+ post[:schedule_time] = schedule_time
+ end
+
+ if vars.length > 0
+ post[:vars] = vars
+ end
+
+ return self.api_post(:send, post)
end
-
- if response.body
- return response.body
- else
- raise SailthruClientException.new("No response received from stream: #{_uri.to_s}")
+
+
+ # params:
+ # send_id, Fixnum
+ # returns:
+ # Hash, response data from server
+ #
+ # Get the status of a send.
+ def get_send(send_id)
+ self.api_get(:send, {:send_id => send_id.to_s})
end
+
- end
-
- # Flatten nested hash for GET / POST request.
- def flatten_nested_hash(hash, brackets = true)
- f = {}
- hash.each do |key, value|
- _key = brackets ? "[#{key}]" : key.to_s
- if value.class == Hash
- flatten_nested_hash(value).each do |k, v|
- f["#{_key}#{k}"] = v
- end
- elsif value.class == Array
- temp_hash = Hash.new()
- value.each_with_index do |v, i|
- temp_hash[i.to_s] = v
- end
- flatten_nested_hash(temp_hash).each do |k, v|
- f["#{_key}#{k}"] = v
- end
+ def cancel_send(send_id)
+ self.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
+ self.api_post(:blast, post)
+ end
+
+
+ # params:
+ # blast_id, Fixnum
+ # returns:
+ # Hash, response data from server
+ #
+ # Get information on a previously scheduled email blast
+ def get_blast(blast_id)
+ self.api_get(:blast, {:blast_id => blast_id.to_s})
+ 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)
+ self.api_get(:email, {:email => email})
+ end
+
+ # params:
+ # email, String
+ # vars, Hash
+ # lists, Hash mapping list name => 1 for subscribed, 0 for unsubscribed
+ # returns:
+ # Hash, response data from server
+ #
+ # Set replacement vars and/or list subscriptions for an email address.
+ def set_email(email, vars = {}, lists = {}, templates = {})
+ data = {:email => email}
+ data[:vars] = vars unless vars.empty?
+ data[:lists] = lists unless lists.empty?
+ data[:templates] = templates unless templates.empty?
+ self.api_post(:email, data)
+ end
+
+ # params:
+ # email, String
+ # password, String
+ # with_names, Boolean
+ # returns:
+ # Hash, response data from server
+ #
+ # Fetch email contacts from an address book at one of the major email providers (aol/gmail/hotmail/yahoo)
+ # Use the with_names parameter if you want to fetch the contact names as well as emails
+ def import_contacts(email, password, with_names = false)
+ data = { :email => email, :password => password }
+ data[:names] = 1 if with_names
+ self.api_post(:contacts, data)
+ end
+
+
+ # params:
+ # template_name, String
+ # returns:
+ # Hash of response data.
+ #
+ # Get a template.
+ def get_template(template_name)
+ self.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
+ self.api_post(:template, data)
+ end
+
+
+ # params:
+ # params, Hash
+ # request, String
+ # returns:
+ # TrueClass or FalseClass, 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[:sig]
+ params.delete(:sig)
+ return false unless sig == get_signature_hash(params, @secret)
+
+ _send = self.get_send(params[:send_id])
+ return false unless _send.has_key?(:email)
+
+ return false unless _send[:email] == params[:email]
+
+ return true
else
- f[_key] = value
+ return false
end
end
- return f
- end
- # params:
- # params, Hash
- # returns:
- # Array, values of each item in the Hash (and nested hashes)
- #
- # Extracts the values of a set of parameters, recursing into nested assoc arrays.
- def self.extract_param_values(params)
- values = []
- params.each do |k, v|
- # puts "k,v: #{k}, #{v}"
- if v.class == Hash
- values.concat SailthruClient.extract_param_values(v)
- elsif v.class == Array
- temp_hash = Hash.new()
- v.each_with_index do |v_,i_|
- temp_hash[i_.to_s] = v_
+ # params:
+ # email, String
+ # items, String
+ # incomplete, Integer
+ # message_id, String
+ # 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)
+ data = {}
+ data[:email] = email
+
+ if verify_purchase_items(items)
+ data[:items] = items
+ end
+
+ if incomplete != nil
+ data[:incomplete] = incomplete.to_i
+ end
+
+ if message_id != nil
+ data[:message_id] = message_id
+ end
+ api_post(:purchase, data)
+ end
+
+ # params:
+ # stat, String
+ #
+ # returns:
+ # hash, response from server
+ # Request various stats from Sailthru.
+ def get_stats(stat)
+ api_get(:stats, {:stat => stat})
+ end
+
+
+ protected
+
+ # Perform API GET request
+ def api_get(action, data)
+ api_request(action, data, 'GET')
+ end
+
+ # Perform API POST request
+ def api_post(action, data)
+ api_request(action, data, 'POST')
+ end
+
+ #Perform API DELETE request
+ def api_delete(action, data)
+ api_request(action, data, 'DELETE')
+ end
+
+ # 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)
+ data[:api_key] = @api_key
+ data[:format] ||= 'json'
+ data[:sig] = get_signature_hash(data, @secret)
+ _result = self.http_request("#{@api_uri}/#{action}", data, request_type)
+
+
+ # NOTE: don't do the unserialize here
+ unserialized = JSON.parse(_result)
+ return unserialized ? unserialized : _result
+ end
+
+
+ # params:
+ # uri, String
+ # data, Hash
+ # method, String "GET" or "POST"
+ # returns:
+ # String, body of response
+ def http_request(uri, data, method = 'POST')
+ data = flatten_nested_hash(data, false)
+ if method == 'POST'
+ post_data = data
+ else
+ 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 #{VERSION}"}
+
+ _uri = URI.parse(uri)
+ if method == 'POST'
+ req = Net::HTTP::Post.new(_uri.path, headers)
+ req.set_form_data(data)
+ 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
- values.concat self.extract_param_values(temp_hash)
+ end
+
+ begin
+ response = Net::HTTP.start(_uri.host, _uri.port) {|http|
+ http.request(req)
+ }
+ rescue Exception => e
+ raise SailthruClientException.new("Unable to open stream: #{_uri.to_s}");
+ end
+
+ if response.body
+ return response.body
else
- values.push v.to_s()
+ raise SailthruClientException.new("No response received from stream: #{_uri.to_s}")
end
- end
- return values
+ end
end
-
- # params:
- # params, Hash
- # secret, String
- # returns:
- # String
- #
- # Returns the unhashed signature string (secret + sorted list of param values) for an API call.
- def self.get_signature_string(params, secret)
- return secret + self.extract_param_values(params).sort.join("")
- end
-
-
- # params:
- # params, Hash
- # secret, String
- # returns:
- # String
- #
- # Returns an MD5 hash of the signature string for an API call.
- def self.get_signature_hash(params, secret)
- return MD5.md5(self.get_signature_string(params, secret)).to_s # debuggin
- end
-
-end
+end
\ No newline at end of file