active_esp 0.1.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
19
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in active_esp.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Brian Morton
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # ActiveESP
2
+
3
+ **NOTE**: ActiveESP is still a very early project with limited support. Be careful when using it as implementations are still changing rapidly. Feel free to contribute and help us with our first official release!
4
+
5
+ ActiveESP is an abstraction library for managing subscribers, campaigns, and other email marketing facilities. It aims to provide a consistent interface to interact with the numerous ESPs operating with different terminologies and strategies.
6
+
7
+ This framework provides some common classes for managing email marketing data structures as well as the adapters for interfacing with the providers' APIs.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'active_esp'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install active_esp
22
+
23
+ ## Basic usage
24
+
25
+ For basic usage, an instance of any of the supported providers may be instantiated and used independently by calling methods on the provider directly.
26
+
27
+ ```
28
+ subscriber = ActiveESP::Subscriber.new(
29
+ :email => 'brian@xq3.net',
30
+ :name => 'Brian Morton'
31
+ )
32
+
33
+ provider = ActiveESP::Providers::MailChimp.new(
34
+ :api_key => '12345678901234567890-us4'
35
+ )
36
+
37
+ list = ActiveESP::List.new(:id => '03b3b0f203')
38
+
39
+ provider.subscribe(subscriber, list) # => true
40
+ ```
41
+
42
+ ## Configuring and using a shared provider
43
+
44
+ For a better integration into an application, using a shared provider tries to hide as much of the provider's specific implementation details as it can. After the provider is configured, as many calls as possible are genericized to work across all implementations.
45
+
46
+ ```
47
+ ActiveESP.configure do |c|
48
+ c.provider :mail_chimp
49
+ c.credentials :api_key => '12345678901234567890-us4'
50
+ end
51
+
52
+ ActiveESP.provider # => #<ActiveESP::Providers::MailChimp:0x007f868b33fb30 @api_key="12345678901234567890-us4">
53
+ ```
54
+
55
+ ## Using convenience methods
56
+
57
+ Convenience methods are defined on the basic Subscriber and List classes for sending themselves in an API request to the shared provider. After configuring a provider, these methods can be fired on any subscriber or list object.
58
+
59
+ ```
60
+ ActiveESP.configure do |c|
61
+ c.provider :mail_chimp
62
+ c.credentials :api_key => '12345678901234567890-us4'
63
+ end
64
+
65
+ member = ActiveESP::Subscriber.new(
66
+ :email => 'brian@xq3.net',
67
+ :name => 'Brian Morton'
68
+ )
69
+
70
+ list = ActiveESP::List.new(:id => '123456')
71
+
72
+ member.subscribe!(list)
73
+ ```
74
+
75
+ ## Contributing
76
+
77
+ 1. Fork it
78
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
79
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
80
+ 4. Push to the branch (`git push origin my-new-feature`)
81
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new('spec')
6
+
7
+ # If you want to make this the default task
8
+ task :default => :spec
9
+
10
+ desc 'Execute a console with the environment preloaded'
11
+ task :console do
12
+ exec 'irb -rubygems -I lib -r active_esp.rb'
13
+ end
14
+
15
+ namespace :docs do
16
+ task :generate do
17
+ exec 'yard doc'
18
+ end
19
+
20
+ task :server do
21
+ exec 'yard server --reload --server thin'
22
+ end
23
+
24
+ task :stats do
25
+ exec 'yard stats'
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/active_esp/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Brian Morton"]
6
+ gem.email = ["bmorton@sdreader.com"]
7
+ gem.description = %q{Framework and tools for managing email service providers.}
8
+ gem.summary = %q{ActiveESP is an abstraction library for managing subscribers, campaigns, and other email marketing facilities. It provides a consistent interface to interact with the numerous ESPs.}
9
+ gem.homepage = "http://1703india.st/active_esp"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "active_esp"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = ActiveESP::VERSION
17
+
18
+ gem.add_runtime_dependency "json", "> 1.4.0"
19
+ gem.add_runtime_dependency "httparty", "~> 0.8.1"
20
+ gem.add_runtime_dependency "activesupport", ">= 3.0.0"
21
+ gem.add_runtime_dependency "shuber-interface", "~> 0.0.4"
22
+
23
+ gem.add_development_dependency "rspec", "~> 2.8.0"
24
+ gem.add_development_dependency "factory_girl", "~> 2.5.1"
25
+ gem.add_development_dependency "yard", "~> 0.7.5"
26
+ gem.add_development_dependency "thin", "~> 1.3.1"
27
+ gem.add_development_dependency "redcarpet", "~> 2.1.0"
28
+ end
@@ -0,0 +1,125 @@
1
+ module ActiveESP
2
+ module Configuration
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Instantiates a shared provider singleton for access through ActiveESP.provider
7
+ #
8
+ # To use the configuration method, call it with a block and specify a provider and
9
+ # any credentials that provider expects as a hash.
10
+ #
11
+ # == Example Usage
12
+ # ActiveESP.configure do |c|
13
+ # c.provider :mail_chimp
14
+ # c.credentials :api_key => '12345678901234567890-us4'
15
+ # end
16
+ #
17
+ # @param [Block] A block of method calls to set up a provider
18
+ # @return [Provider] Returns the shared provider that was instantiated due to
19
+ # the provided configuration
20
+ def configure(&block)
21
+ yield self
22
+ reset_provider
23
+ end
24
+
25
+ # Returns the shared provider and optionally sets up the provider with the class
26
+ # passed as its only argument.
27
+ #
28
+ # If a provider_class is provided, the method will set the class to use when
29
+ # instantiating the shared provider. Since provider and credentials could
30
+ # potentially be set in any order in the configuration block, we only want
31
+ # to try to instantiate a provider if the credentials have already been set.
32
+ # If the credentials have not been set, a new provider will not be returned
33
+ # until the method is called after credentials have been set.
34
+ #
35
+ # == Example Usage
36
+ # ActiveESP.provider :mail_chimp # => nil
37
+ #
38
+ # If the method is called with no arguments, it will return the shared provider,
39
+ # instantiating it if necessary. Again, this relies on the provider class and
40
+ # credentials being previously set.
41
+ #
42
+ # == Example Usage
43
+ # ActiveESP.provider # => #<ActiveESP::Providers::MailChimp:0x007f868b33fb30 @api_key="test">
44
+ #
45
+ # @param [Symbol] An optional symbol representing the provider class to use
46
+ # @return [Provider] The shared provider object or nil if it can't be instantiated
47
+ def provider(provider_class = nil)
48
+ # If the provider hasn't been instantiated yet and we're not using this method
49
+ # to set the provider's class, we need to reset the provider so that it can be
50
+ # returned properly.
51
+ reset_provider if !@provider && !provider_class
52
+ return @provider unless provider_class
53
+ self.provider_class = provider_class
54
+ end
55
+
56
+ # Sets the shared provider to any custom instantiated provider
57
+ #
58
+ # When necessary, the configuration method can be skipped and a provider can be
59
+ # directly assigned to be the shared provider.
60
+ #
61
+ # == Example Usage
62
+ # ActiveESP.provider = ActiveESP::Providers::MailChimp.new(
63
+ # :api_key => '12345678901234567890-us4'
64
+ # )
65
+ #
66
+ # @param [Provider] An instance of an object in the ActiveESP::Providers namespace
67
+ # @return [Provider] The passed provider object
68
+ def provider=(shared_provider)
69
+ @provider = shared_provider
70
+ end
71
+
72
+ # Assigns and/or returns the provider's credentials when configuring via the
73
+ # configuration method
74
+ #
75
+ # This is used to help the configuration method assign credentials conveniently.
76
+ # It works outside of the configuration block as well.
77
+ #
78
+ # == Example Usage
79
+ # ActiveESP.credentials :api_key => '12345678901234567890-us4'
80
+ #
81
+ # @param [Hash] provider_credentials An optional hash of values needed to
82
+ # instantiate a provider object
83
+ # @return [Hash] The credentials required to instantiate a provider object
84
+ def credentials(provider_credentials = nil)
85
+ @credentials = provider_credentials if provider_credentials
86
+ @credentials
87
+ end
88
+
89
+ # Assigns the provider's credentials
90
+ #
91
+ # == Example Usage
92
+ # ActiveESP.credentials = { :api_key => '12345678901234567890-us4' }
93
+ #
94
+ # @param [Hash] provider_credentials A hash of values needed to instantiate a
95
+ # provider object
96
+ # @return [Hash] The provided credentials
97
+ def credentials=(provider_credentials)
98
+ @credentials = provider_credentials
99
+ end
100
+
101
+ private
102
+
103
+ # Defines which provider class to use and resets the shared provider
104
+ #
105
+ # @return nil
106
+ def provider_class=(klass)
107
+ @provider_class = klass
108
+ @provider = nil
109
+ end
110
+
111
+ # Clears the shared provider, reinstantiates it, and returns it
112
+ #
113
+ # @return [Provider] shared provider instance
114
+ def reset_provider
115
+ begin
116
+ full_provider_class_name = "ActiveESP::Providers::#{@provider_class.to_s.classify}".constantize
117
+ rescue NameError
118
+ raise ActiveESP::ProviderNotSupportedException.new("#{@provider_class.to_s} is not a supported provider.")
119
+ end
120
+
121
+ @provider = full_provider_class_name.new(@credentials)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,3 @@
1
+ ActiveSupport::Inflector.inflections do |inflect|
2
+ inflect.acronym 'IContact'
3
+ end
@@ -0,0 +1,42 @@
1
+ module ActiveESP
2
+ class List
3
+ # Returns or sets the list's identifier
4
+ #
5
+ # @return [String]
6
+ attr_accessor :id
7
+
8
+ # Returns or sets the list's name
9
+ #
10
+ # @return [String]
11
+ attr_accessor :name
12
+
13
+ # Initialize object with an optional attributes hash
14
+ #
15
+ # @param [Hash] attributes An optional hash of attributes to assign to the new instance
16
+ def initialize(attributes = nil)
17
+ if attributes.is_a? Hash
18
+ attributes.each do |key, value|
19
+ self.send(key.to_s + "=", value)
20
+ end
21
+ end
22
+ end
23
+
24
+ # Accessing commonly used API calls
25
+
26
+ # Add the given subscriber to this list.
27
+ #
28
+ # @see ActiveESP::Providers::Interface#subscribe
29
+ def subscribe!(subscriber)
30
+ raise ActiveESP::ProviderNotConfiguredException unless ActiveESP.provider
31
+ ActiveESP.provider.subscribe(subscriber, self)
32
+ end
33
+
34
+ # Remove the given subscriber from this list.
35
+ #
36
+ # @see ActiveESP::Providers::Interface#unsubscribe
37
+ def unsubscribe!(subscriber)
38
+ raise ActiveESP::ProviderNotConfiguredException unless ActiveESP.provider
39
+ ActiveESP.provider.unsubscribe(subscriber, self)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,51 @@
1
+ module ActiveESP
2
+ # A +Provider+ in ActiveESP is an individual ESP class that implements the required
3
+ # methods to conform to a +Provider+ protocol.
4
+ #
5
+ # To make things work smoothly, the framework expects a +Provider+ to extend from
6
+ # +ActiveESP::Providers::Base+. This allows common functionality to be shared
7
+ # amongst the providers as well as exceptions to be raised when the provider
8
+ # doesn't implement a required method.
9
+ #
10
+ module Providers
11
+ class Base
12
+ implements ActiveESP::Providers::Interface
13
+ # Returns or sets the API key
14
+ #
15
+ # @return [String]
16
+ attr_accessor :api_key
17
+
18
+ # Sets the endpoint base URL without a trailing slash
19
+ #
20
+ # @return [String]
21
+ attr_writer :endpoint
22
+ cattr_accessor :endpoint
23
+
24
+ # Initialize object with an optional attributes hash
25
+ #
26
+ # @param [Hash] attributes An optional hash of attributes to assign to the new instance
27
+ def initialize(attributes = nil)
28
+ if attributes.is_a? Hash
29
+ attributes.each do |key, value|
30
+ self.send(key.to_s + "=", value)
31
+ end
32
+ end
33
+ end
34
+
35
+ def endpoint
36
+ if defined?(@endpoint)
37
+ @endpoint
38
+ elsif self.class.endpoint
39
+ self.class.endpoint
40
+ elsif superclass != Object && superclass.endpoint
41
+ superclass.endpoint
42
+ else
43
+ @endpoint ||= ''
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ class MethodNotImplementedException < Exception; end
50
+ end
51
+ end
@@ -0,0 +1,164 @@
1
+ module ActiveESP
2
+ module Providers
3
+ # This is the implementation for version 2.0 of iContact's API.
4
+ #
5
+ # == Example Usage
6
+ # ActiveESP.configure do |c|
7
+ # c.provider :icontact
8
+ # c.credentials :app_id => '1234567890',
9
+ # :username => 'testuser',
10
+ # :password => 'password',
11
+ # :api_version => '2.0'
12
+ # end
13
+ #
14
+ # list = ActiveESP::List.new(:id => 123)
15
+ # subscriber = ActiveESP::Subscriber.new(
16
+ # :email => 'user@example.com',
17
+ # :name => 'Brian Morton'
18
+ # )
19
+ #
20
+ # ActiveESP.provider.subscribe(subscriber, list)
21
+ #
22
+ class IContact < Base
23
+ include HTTParty
24
+ format :plain
25
+ self.endpoint = 'https://app.icontact.com/icp'
26
+
27
+ # Returns or sets the application ID provided by iContact
28
+ #
29
+ # @see https://app.icontact.com/icp/core/registerapp/
30
+ # @return [String]
31
+ attr_accessor :app_id
32
+
33
+ # Returns or sets the username of the account associated with
34
+ # the application ID.
35
+ #
36
+ # @see https://app.icontact.com/icp/core/registerapp/
37
+ # @see http://developer.icontact.com/documentation/authenticate-requests/
38
+ # @return [String]
39
+ attr_accessor :username
40
+
41
+ # Returns or sets the password of the username or application ID
42
+ # provided.
43
+ #
44
+ # @see https://app.icontact.com/icp/core/registerapp/
45
+ # @see http://developer.icontact.com/documentation/authenticate-requests/
46
+ # @return [String]
47
+ attr_accessor :password
48
+
49
+ # The version of the iContact API that is being accessed.
50
+ #
51
+ # Currently only version 2.0 is supported. This is for backporting
52
+ # functionality for 1.0 or supporting future versions of the API.
53
+ #
54
+ # @see http://developer.icontact.com/documentation/authenticate-requests/
55
+ # @return [String]
56
+ attr_accessor :api_version
57
+
58
+ # Interface implementation
59
+
60
+ # Create a contact and optionally subscribe them to a provided list.
61
+ #
62
+ # Note: On iContact, a user cannot be subscribed to a list that they have
63
+ # previously unsubscribed from.
64
+ #
65
+ # @see ActiveESP::Providers::Interface#subscribe
66
+ def subscribe(subscriber, list = nil)
67
+ response = call(:post, "/a/#{account_id}/c/#{client_folder_id}/contacts", [{:email => subscriber.email, :firstName => subscriber.first_name, :lastName => subscriber.last_name}])
68
+ subscriber.id = response['contacts'].first['contactId']
69
+ if list
70
+ list_response = call(:post, "/a/#{account_id}/c/#{client_folder_id}/subscriptions", [{:listId => list.id, :contactId => subscriber.id, :status => 'normal' }])
71
+ raise ActiveESP::Providers::CouldNotSubscribeToListException, list_response['warnings'] if list_response['warnings']
72
+ end
73
+ subscriber
74
+ end
75
+
76
+ # Unsubscribe a subscriber from the provided list.
77
+ #
78
+ # @see ActiveESP::Providers::Interface#unsubscribe
79
+ def unsubscribe(subscriber, list)
80
+ response = call(:post, "/a/#{account_id}/c/#{client_folder_id}/subscriptions/#{list.id}_#{subscriber.id}", :status => 'unsubscribed')
81
+
82
+ raise ActiveESP::Providers::CouldNotUnsubscribeFromListException, response['warnings'] if response['warnings']
83
+ raise ActiveESP::Providers::CouldNotUnsubscribeFromListException, response['errors'] if response['errors']
84
+ end
85
+
86
+ # Retrieve an array of lists
87
+ #
88
+ # @see ActiveESP::Providers::Interface#lists
89
+ def lists
90
+ call(:get, "/a/#{account_id}/c/#{client_folder_id}/lists")
91
+ end
92
+
93
+ # Getting iContact-specific account information
94
+
95
+ # Retrieves the iContact account information attached to the credentials
96
+ # provided.
97
+ #
98
+ # @see http://developer.icontact.com/documentation/request-your-accountid-and-clientfolderid/
99
+ # @return [Array] An array with hashes of the associated account information
100
+ def account
101
+ call(:get, '/a')
102
+ end
103
+
104
+ # Retrieves the iContact client folders for the account.
105
+ #
106
+ # @see http://developer.icontact.com/documentation/request-your-accountid-and-clientfolderid/
107
+ # @return [Array] An array with hashes of the client folders
108
+ def client_folders
109
+ call(:get, "/a/#{account_id}/c")
110
+ end
111
+
112
+ # Retrieving account information necessary for API calls
113
+
114
+ # Retrieves the account ID associated with the credentials in cases where it
115
+ # is not provided with the credentials.
116
+ #
117
+ # Note: You should *always* specify the account ID manually by setting
118
+ # account_id so that an additional API request doesn't need to be made
119
+ # to retrieve it.
120
+ #
121
+ # @see http://developer.icontact.com/documentation/request-your-accountid-and-clientfolderid/
122
+ # @return [Array] An array with hashes of the client folders
123
+ def account_id
124
+ @account_id ||= account['accounts'].first['accountId']
125
+ end
126
+ attr_writer :account_id
127
+
128
+ # Retrieves the client folder ID of the first folder returned.
129
+ #
130
+ # This ID is required to make API calls. It is recommended that it is
131
+ # set with the credentials, but if it is not, an attempt will be made
132
+ # to determine it via an API call.
133
+ #
134
+ # Note: You should *always* specify the folder ID manually by setting
135
+ # client_folder_id so that an additional API request doesn't need to be
136
+ # made to retrieve it.
137
+ #
138
+ # @see http://developer.icontact.com/documentation/request-your-accountid-and-clientfolderid/
139
+ # @return [Array] An array with hashes of the client folders
140
+ def client_folder_id
141
+ @client_folder_id ||= client_folders['clientfolders'].first['clientFolderId']
142
+ end
143
+ attr_writer :client_folder_id
144
+
145
+ private
146
+ def call(method, resource, params = {})
147
+ self.class.base_uri self.endpoint
148
+ body = params.to_json
149
+ response = self.class.send(method, resource, :headers => headers, :body => body)
150
+
151
+ JSON.parse(response.body)
152
+ end
153
+
154
+ def headers
155
+ { 'Accept' => 'application/json',
156
+ 'Content-Type' => 'application/json',
157
+ 'API-Version' => api_version,
158
+ 'API-AppId' => app_id,
159
+ 'API-Username' => username,
160
+ 'API-Password' => password }
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveESP
2
+ module Providers
3
+ module Interface
4
+ # Subscription methods
5
+
6
+ def subscribe(subscriber, list = nil); end
7
+ def unsubscribe(subscriber, list = nil); end
8
+ def subscribed?(subscriber, list = nil); end
9
+
10
+ # List methods
11
+
12
+ def lists; end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ module ActiveESP
2
+ module Providers
3
+ class MailChimp < Base
4
+ include HTTParty
5
+ format :plain
6
+
7
+ def endpoint
8
+ "https://#{dc_from_api_key}api.mailchimp.com/1.3"
9
+ end
10
+
11
+ def subscribe(subscriber, list)
12
+ call(:list_subscribe, { :id => list.id, :email_address => subscriber.email, :merge_vars => { :FNAME => subscriber.first_name, :LNAME => subscriber.last_name }})
13
+ end
14
+
15
+ def lists
16
+ call(:lists)
17
+ end
18
+
19
+ private
20
+ def call(method, params = {})
21
+ api_url = endpoint + '/?method=' + method.to_s.camelize(:lower)
22
+ params = api_params(params)
23
+ response = self.class.post(api_url, :body => CGI::escape(params.to_json), :timeout => 30)
24
+
25
+ # Some calls (e.g. listSubscribe) return json fragments
26
+ # (e.g. true) so wrap in an array prior to parsing
27
+ response = JSON.parse('['+response.body+']').first
28
+
29
+ if @throws_exceptions && response.is_a?(Hash) && response["error"]
30
+ raise "Error from MailChimp API: #{response["error"]} (code #{response["code"]})"
31
+ end
32
+
33
+ response
34
+ end
35
+
36
+ def api_params(additional_params = {})
37
+ { :apikey => api_key }.merge(additional_params)
38
+ end
39
+
40
+ def dc_from_api_key
41
+ (!@api_key.present? || @api_key !~ /-/) ? '' : "#{@api_key.split("-").last}."
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,18 @@
1
+ module ActiveESP
2
+ module Providers
3
+ # Thanks to ActiveMerchant for this autoloading code!
4
+ Dir[File.dirname(__FILE__) + '/providers/*.rb'].each do |f|
5
+ # Get camelized class name
6
+ filename = File.basename(f, '.rb')
7
+
8
+ # Camelize the string to get the class name
9
+ gateway_class = filename.camelize.to_sym
10
+
11
+ # Register for autoloading
12
+ autoload gateway_class, f
13
+ end
14
+
15
+ class CouldNotSubscribeToListException < Exception; end
16
+ class CouldNotUnsubscribeFromListException < Exception; end
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: binary
2
+
3
+ module ActiveESP #:nodoc:
4
+ # RFC822 Email Address Regex
5
+ #
6
+ # Originally written by Cal Henderson
7
+ # c.f. http://iamcal.com/publish/articles/php/parsing_email/
8
+ #
9
+ # Translated to Ruby by Tim Fletcher, with changes suggested by Dan Kubb.
10
+ #
11
+ # Licensed under a Creative Commons Attribution-ShareAlike 2.5 License
12
+ # http://creativecommons.org/licenses/by-sa/2.5/
13
+ #
14
+ # Source: http://tfletcher.com/lib/rfc822.rb
15
+ # RFC822: http://www.w3.org/Protocols/rfc822/#z8
16
+ module RFC822
17
+ EmailAddress = begin
18
+ qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
19
+ dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'
20
+ atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-' +
21
+ '\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'
22
+ quoted_pair = '\\x5c[\\x00-\\x7f]'
23
+ domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d"
24
+ quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22"
25
+ domain_ref = atom
26
+ sub_domain = "(?:#{domain_ref}|#{domain_literal})"
27
+ word = "(?:#{atom}|#{quoted_string})"
28
+ domain = "#{sub_domain}(?:\\x2e#{sub_domain})*"
29
+ local_part = "#{word}(?:\\x2e#{word})*"
30
+ addr_spec = "#{local_part}\\x40#{domain}"
31
+ pattern = /\A#{addr_spec}\z/
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,126 @@
1
+ module ActiveESP
2
+ # A +Subscriber+ object represents an email address stored within a service provider's
3
+ # system.
4
+ #
5
+ # At the very least, an email address is required to have a valid subscriber. If a name
6
+ # is also necessary, a +requires_name+ flag can be set on the class to indicate to the
7
+ # validator that it needs to check for a name as well.
8
+ #
9
+ # Email address are validated against the spec set forth in RFC822:
10
+ # http://www.w3.org/Protocols/rfc822/#z8
11
+ #
12
+ # == Example Usage
13
+ # subscriber = ActiveESP::Subscriber.new(
14
+ # :first_name => 'Billie Joe',
15
+ # :last_name => 'Armstong',
16
+ # :email => 'billie.joe@example.com'
17
+ # )
18
+ #
19
+ # subscriber.name # => 'Billie Joe Armstrong'
20
+ # subscriber.valid? # => true
21
+ #
22
+ class Subscriber
23
+ include RFC822
24
+
25
+ # Forces a first and last name to be present for the object to be valid.
26
+ # If true, the name is required. The default value is false.
27
+ #
28
+ # @return [Boolean]
29
+ cattr_accessor :requires_name
30
+ self.requires_name = false
31
+
32
+ # Returns or sets the subscriber's ID as determined by the provider
33
+ #
34
+ # @return [String]
35
+ attr_accessor :id
36
+
37
+ # Returns or sets the subscriber's email address
38
+ #
39
+ # @return [String]
40
+ attr_accessor :email
41
+
42
+ # Returns or sets the subscriber's first name
43
+ #
44
+ # @return [String]
45
+ attr_accessor :first_name
46
+
47
+ # Returns or sets the subscriber's last name
48
+ #
49
+ # @return [String]
50
+ attr_accessor :last_name
51
+
52
+ # Initialize object with an optional attributes hash
53
+ #
54
+ # @param [Hash] attributes An optional hash of attributes to assign to the new instance
55
+ def initialize(attributes = nil)
56
+ if attributes.is_a? Hash
57
+ attributes.each do |key, value|
58
+ self.send(key.to_s + "=", value)
59
+ end
60
+ end
61
+ end
62
+
63
+ # Returns the full name of the subscriber.
64
+ #
65
+ # @return [String] the full name of the subscriber
66
+ def name
67
+ [@first_name, @last_name].compact.join(' ')
68
+ end
69
+
70
+ # Assigns first and last names based on specifiying a full name string
71
+ #
72
+ # @param [String] full_name The subscriber's full name to be split programatically
73
+ def name=(full_name)
74
+ names = full_name.split
75
+ self.last_name = names.pop
76
+ self.first_name = names.join(" ")
77
+ end
78
+
79
+ # Returns whether either the +first_name+ or the +last_name+ attributes has been set.
80
+ def name?
81
+ first_name? || last_name?
82
+ end
83
+
84
+ # Returns whether the +first_name+ attribute has been set.
85
+ def first_name?
86
+ @first_name.present?
87
+ end
88
+
89
+ # Returns whether the +last_name+ attribute has been set.
90
+ def last_name?
91
+ @last_name.present?
92
+ end
93
+
94
+ def valid?
95
+ valid_email? && valid_name?
96
+ end
97
+
98
+ def valid_email?
99
+ @email =~ EmailAddress
100
+ end
101
+
102
+ def valid_name?
103
+ return false if self.class.requires_name && !name?
104
+ return true
105
+ end
106
+
107
+ # Accessing commonly used API calls
108
+
109
+ # Add the subscriber to the provider and optionally subscribe them to the
110
+ # given list.
111
+ #
112
+ # @see ActiveESP::Providers::Interface#subscribe
113
+ def subscribe!(list = nil)
114
+ raise ActiveESP::ProviderNotConfiguredException unless ActiveESP.provider
115
+ ActiveESP.provider.subscribe(self, list)
116
+ end
117
+
118
+ # Remove the subscriber from the given list.
119
+ #
120
+ # @see ActiveESP::Providers::Interface#unsubscribe
121
+ def unsubscribe!(list)
122
+ raise ActiveESP::ProviderNotConfiguredException unless ActiveESP.provider
123
+ ActiveESP.provider.unsubscribe(self, list)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveESP
2
+ VERSION = "0.1.0.alpha1"
3
+ end
data/lib/active_esp.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "interface"
2
+ require "httparty"
3
+ require "json"
4
+ require "cgi"
5
+
6
+ require "active_support/core_ext"
7
+ require "active_support/inflector"
8
+ require "active_esp/inflections"
9
+ require "active_esp/version"
10
+ require "active_esp/configuration"
11
+ require "active_esp/rfc822"
12
+ require "active_esp/subscriber"
13
+ require "active_esp/list"
14
+
15
+ # ActiveESP is an abstraction library for managing subscribers, campaigns, and other email
16
+ # marketing facilities. It aims to provide a consistent interface to interact with the
17
+ # numerous ESPs operating with different terminologies and strategies.
18
+ #
19
+ # This framework provides some common classes for managing email marketing data structures
20
+ # as well as the adapters for interfacing with the providers' APIs.
21
+ module ActiveESP
22
+ autoload :Providers, "active_esp/providers"
23
+
24
+ include Configuration
25
+
26
+ class ProviderNotSupportedException < Exception; end
27
+ class ProviderNotConfiguredException < Exception; end
28
+ end
data/spec/factories.rb ADDED
@@ -0,0 +1,7 @@
1
+ FactoryGirl.define do
2
+ factory :subscriber, :class => ActiveESP::Subscriber do
3
+ first_name "Billie Joe"
4
+ last_name "Armstrong"
5
+ sequence(:email) { |n| "kerplunk#{n}@example.com" }
6
+ end
7
+ end
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveESP::Subscriber do
4
+ before(:each) do
5
+ ActiveESP::Subscriber.requires_name = false
6
+ @attributes = Factory.attributes_for(:subscriber)
7
+ end
8
+
9
+ it "should properly assign attributes" do
10
+ subscriber = ActiveESP::Subscriber.new(@attributes)
11
+ subscriber.first_name.should eq(@attributes[:first_name])
12
+ subscriber.last_name.should eq(@attributes[:last_name])
13
+ subscriber.email.should eq(@attributes[:email])
14
+ end
15
+
16
+ it "should respond with the subscriber's full name" do
17
+ subscriber = ActiveESP::Subscriber.new(:first_name => 'Kris', :last_name => 'Roe')
18
+ subscriber.name.should eq('Kris Roe')
19
+ end
20
+
21
+ describe ".first_name?" do
22
+ it "should return true if a first name has been assigned" do
23
+ subscriber = ActiveESP::Subscriber.new(:first_name => 'Haushinka')
24
+ subscriber.first_name?.should be_true
25
+ end
26
+
27
+ it "should return false if a first name has not been assigned" do
28
+ subscriber = ActiveESP::Subscriber.new
29
+ subscriber.first_name?.should be_false
30
+ end
31
+
32
+ it "should return false if the first name is blank" do
33
+ subscriber = ActiveESP::Subscriber.new(:first_name => '')
34
+ subscriber.first_name?.should be_false
35
+ end
36
+ end
37
+
38
+ describe ".last_name?" do
39
+ it "should return true if a last name has been assigned" do
40
+ subscriber = ActiveESP::Subscriber.new(:last_name => 'Haushinka')
41
+ subscriber.last_name?.should be_true
42
+ end
43
+
44
+ it "should return false if a last name has not been assigned" do
45
+ subscriber = ActiveESP::Subscriber.new
46
+ subscriber.last_name?.should be_false
47
+ end
48
+
49
+ it "should return false if the last name is blank" do
50
+ subscriber = ActiveESP::Subscriber.new(:last_name => '')
51
+ subscriber.last_name?.should be_false
52
+ end
53
+ end
54
+
55
+ describe ".name?" do
56
+ it "should return true if a first name has been assigned" do
57
+ subscriber = ActiveESP::Subscriber.new(:first_name => 'Haushinka')
58
+ subscriber.name?.should be_true
59
+ end
60
+
61
+ it "should return true if a last name has been assigned" do
62
+ subscriber = ActiveESP::Subscriber.new(:last_name => 'Haushinka')
63
+ subscriber.name?.should be_true
64
+ end
65
+
66
+ it "should return false if neither a first name nor a last name has been assigned" do
67
+ subscriber = ActiveESP::Subscriber.new
68
+ subscriber.name?.should be_false
69
+ end
70
+ end
71
+
72
+
73
+ describe ".name=" do
74
+ it "should assign the last word of a given full name as the last name" do
75
+ subscriber = ActiveESP::Subscriber.new
76
+ subscriber.name = "Some Really Long Name Morton"
77
+ subscriber.last_name.should eq("Morton")
78
+ end
79
+
80
+ it "should assign the all words except the last of a given full name as the first name" do
81
+ subscriber = ActiveESP::Subscriber.new
82
+ subscriber.name = "Billie Joe Armstrong"
83
+ subscriber.first_name.should eq("Billie Joe")
84
+ end
85
+ end
86
+
87
+ describe ".valid_email?" do
88
+ it "should return true if the assigned email is valid" do
89
+ subscriber = ActiveESP::Subscriber.new(:email => 'user@example.com')
90
+ subscriber.should be_valid_email
91
+ end
92
+
93
+ it "should return false if the assigned email is not valid" do
94
+ subscriber = ActiveESP::Subscriber.new(:email => 'user')
95
+ subscriber.should_not be_valid_email
96
+ end
97
+ end
98
+
99
+ describe ".valid_name?" do
100
+ it "should return true if the name isn't required" do
101
+ ActiveESP::Subscriber.requires_name = false
102
+ subscriber = ActiveESP::Subscriber.new
103
+ subscriber.valid_name?.should be_true
104
+ end
105
+
106
+ it "should return true if the name is required and the name is present" do
107
+ ActiveESP::Subscriber.requires_name = true
108
+ subscriber = ActiveESP::Subscriber.new
109
+ subscriber.stub(:name?).and_return(true)
110
+ subscriber.valid_name?.should be_true
111
+ end
112
+
113
+ it "should return false if the name is required and the name isn't present" do
114
+ ActiveESP::Subscriber.requires_name = true
115
+ subscriber = ActiveESP::Subscriber.new
116
+ subscriber.stub(:name?).and_return(false)
117
+ subscriber.valid_name?.should_not be_true
118
+ end
119
+ end
120
+
121
+ describe ".valid?" do
122
+ it "should return true if the name and email address are valid" do
123
+ subscriber = ActiveESP::Subscriber.new
124
+ subscriber.stub(:valid_name?).and_return(true)
125
+ subscriber.stub(:valid_email?).and_return(true)
126
+ subscriber.valid?.should be_true
127
+ end
128
+
129
+ it "should return false if the name isn't valid" do
130
+ subscriber = ActiveESP::Subscriber.new
131
+ subscriber.stub(:valid_name?).and_return(false)
132
+ subscriber.valid?.should_not be_true
133
+ end
134
+
135
+ it "should return false if the email isn't valid" do
136
+ subscriber = ActiveESP::Subscriber.new
137
+ subscriber.stub(:valid_email?).and_return(false)
138
+ subscriber.valid?.should_not be_true
139
+ end
140
+ end
141
+
142
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveESP::Providers::IContact do
4
+ it { should implement_interface(ActiveESP::Providers::Interface) }
5
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveESP::Providers::MailChimp do
4
+ it { should implement_interface(ActiveESP::Providers::Interface) }
5
+ end
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'active_esp'
4
+ require 'factory_girl'
5
+ require './spec/factories'
6
+
7
+ Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
8
+
9
+ RSpec.configure do |config|
10
+ # some (optional) config here
11
+ end
@@ -0,0 +1,13 @@
1
+ RSpec::Matchers.define :implement_interface do |expected|
2
+ match do |object|
3
+ @unimplemented_methods = object.unimplemented_methods.reject do |interface, methods|
4
+ interface != expected
5
+ end
6
+
7
+ @unimplemented_methods.empty?
8
+ end
9
+
10
+ failure_message_for_should do |actual|
11
+ "did not implement #{@unimplemented_methods.inspect}"
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_esp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.alpha1
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Brian Morton
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: &70366026630040 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>'
20
+ - !ruby/object:Gem::Version
21
+ version: 1.4.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70366026630040
25
+ - !ruby/object:Gem::Dependency
26
+ name: httparty
27
+ requirement: &70366026629440 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.8.1
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70366026629440
36
+ - !ruby/object:Gem::Dependency
37
+ name: activesupport
38
+ requirement: &70366026628960 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 3.0.0
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70366026628960
47
+ - !ruby/object:Gem::Dependency
48
+ name: shuber-interface
49
+ requirement: &70366026628420 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.0.4
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70366026628420
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec
60
+ requirement: &70366026627840 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 2.8.0
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70366026627840
69
+ - !ruby/object:Gem::Dependency
70
+ name: factory_girl
71
+ requirement: &70366026627360 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: 2.5.1
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70366026627360
80
+ - !ruby/object:Gem::Dependency
81
+ name: yard
82
+ requirement: &70366026626880 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ version: 0.7.5
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *70366026626880
91
+ - !ruby/object:Gem::Dependency
92
+ name: thin
93
+ requirement: &70366026626380 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ~>
97
+ - !ruby/object:Gem::Version
98
+ version: 1.3.1
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *70366026626380
102
+ - !ruby/object:Gem::Dependency
103
+ name: redcarpet
104
+ requirement: &70366026625920 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 2.1.0
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: *70366026625920
113
+ description: Framework and tools for managing email service providers.
114
+ email:
115
+ - bmorton@sdreader.com
116
+ executables: []
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - .gitignore
121
+ - .rspec
122
+ - Gemfile
123
+ - LICENSE
124
+ - README.md
125
+ - Rakefile
126
+ - active_esp.gemspec
127
+ - lib/active_esp.rb
128
+ - lib/active_esp/configuration.rb
129
+ - lib/active_esp/inflections.rb
130
+ - lib/active_esp/list.rb
131
+ - lib/active_esp/providers.rb
132
+ - lib/active_esp/providers/base.rb
133
+ - lib/active_esp/providers/icontact.rb
134
+ - lib/active_esp/providers/interface.rb
135
+ - lib/active_esp/providers/mail_chimp.rb
136
+ - lib/active_esp/rfc822.rb
137
+ - lib/active_esp/subscriber.rb
138
+ - lib/active_esp/version.rb
139
+ - spec/factories.rb
140
+ - spec/models/subscriber_spec.rb
141
+ - spec/providers/icontact_spec.rb
142
+ - spec/providers/mail_chimp_spec.rb
143
+ - spec/spec_helper.rb
144
+ - spec/support/matchers/implement_interface.rb
145
+ homepage: http://1703india.st/active_esp
146
+ licenses: []
147
+ post_install_message:
148
+ rdoc_options: []
149
+ require_paths:
150
+ - lib
151
+ required_ruby_version: !ruby/object:Gem::Requirement
152
+ none: false
153
+ requirements:
154
+ - - ! '>='
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ none: false
159
+ requirements:
160
+ - - ! '>'
161
+ - !ruby/object:Gem::Version
162
+ version: 1.3.1
163
+ requirements: []
164
+ rubyforge_project:
165
+ rubygems_version: 1.8.16
166
+ signing_key:
167
+ specification_version: 3
168
+ summary: ActiveESP is an abstraction library for managing subscribers, campaigns,
169
+ and other email marketing facilities. It provides a consistent interface to interact
170
+ with the numerous ESPs.
171
+ test_files:
172
+ - spec/factories.rb
173
+ - spec/models/subscriber_spec.rb
174
+ - spec/providers/icontact_spec.rb
175
+ - spec/providers/mail_chimp_spec.rb
176
+ - spec/spec_helper.rb
177
+ - spec/support/matchers/implement_interface.rb
178
+ has_rdoc: