turingstudio-campaign_monitor 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2006 Jordan Brock
2
+ Copyright (c) 2009 The Turing Studio, Inc.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,52 @@
1
+ = campaign_monitor
2
+
3
+ This RubyGem provides access to the Campaign Monitor API (http://www.campaignmonitor.com/api).
4
+
5
+ Campaign Monitor recently made some changes to their API.
6
+
7
+ This fork makes the following changes:
8
+
9
+ * host changed from http://app.campaignmonitor.com to http://api.createsend.com
10
+ * ID values are no longer sent #to_i because they are hex strings
11
+ * added support for subscribers with custom fields using SOAP API
12
+ * refactored gemspec to build on github
13
+ * misc. cleanup and refactoring
14
+
15
+
16
+ == Pre-requisites
17
+
18
+ An account with Campaign Monitor and the API Key. Accounts are free and can be created at
19
+ http://www.campaignmonitor.com.
20
+
21
+ == Resources
22
+
23
+ === Install
24
+ gem install turingstudio-campaign_monitor
25
+
26
+ === Git Repository
27
+ http://github.com/turingstudio/campaign-monitor-ruby
28
+
29
+
30
+ == Usage
31
+
32
+ cm = CampaignMonitor.new # assumes you've set CAMPAIGN_MONITOR_API_KEY in your project
33
+
34
+ for client in cm.clients
35
+ for list in client.lists
36
+ client.name # => returns the name
37
+
38
+ # modify a subscriber list
39
+ list.add_subscriber(email, name, custom_fields_hash)
40
+ list.remove_subscriber(email)
41
+ list.add_and_resubscribe(email, name, custom_fields_hash)
42
+
43
+ # get subscriber list details
44
+ subscribers = list.active_subscribers(since_time)
45
+ unsubscribed = list.unsubscribed(since_time)
46
+ bounced = list.bounced(since_time)
47
+ end
48
+
49
+ for campaign in client.campaigns
50
+
51
+ end
52
+ end
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+
6
+ # read the contents of the gemspec, eval it, and assign it to 'spec'
7
+ # this lets us maintain all gemspec info in one place. Nice and DRY.
8
+ spec = eval(IO.read("campaign_monitor.gemspec"))
9
+
10
+ Rake::GemPackageTask.new(spec) do |pkg|
11
+ pkg.gem_spec = spec
12
+ end
13
+
14
+ task :install => [:package] do
15
+ sh %{sudo gem install pkg/#{spec.name}-#{spec.version}}
16
+ end
17
+
18
+ Rake::TestTask.new do |t|
19
+ t.libs << "test"
20
+ t.test_files = FileList['test/test*.rb']
21
+ t.verbose = true
22
+ end
23
+
24
+ Rake::RDocTask.new do |rd|
25
+ rd.main = "README.rdoc"
26
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
27
+ rd.rdoc_dir = 'doc'
28
+ rd.options = spec.rdoc_options
29
+ end
@@ -0,0 +1,45 @@
1
+ Gem::Specification.new do |s|
2
+ s.platform = Gem::Platform::RUBY
3
+ s.name = 'campaign_monitor'
4
+ s.version = "1.3.1"
5
+ s.summary = 'Provides access to the Campaign Monitor API.'
6
+ s.description = <<-EOF
7
+ A simple wrapper class that provides basic access to the Campaign Monitor API.
8
+ EOF
9
+
10
+ s.author = 'The Turing Studio, Inc.'
11
+ s.email = 'operations@turingstudio.com'
12
+ s.homepage = 'http://github.com/turingstudio/campaign-monitor-ruby/'
13
+ s.has_rdoc = true
14
+
15
+ s.requirements << 'none'
16
+ s.require_path = 'lib'
17
+
18
+ s.add_dependency 'xml-simple', ['>= 1.0.11']
19
+
20
+ s.files = [
21
+ 'campaign_monitor.gemspec',
22
+ 'init.rb',
23
+ 'install.rb',
24
+ 'MIT-LICENSE',
25
+ 'Rakefile',
26
+ 'README.rdoc',
27
+
28
+ 'lib/campaign_monitor.rb',
29
+ 'lib/campaign_monitor/campaign.rb',
30
+ 'lib/campaign_monitor/client.rb',
31
+ 'lib/campaign_monitor/helpers.rb',
32
+ 'lib/campaign_monitor/list.rb',
33
+ 'lib/campaign_monitor/result.rb',
34
+ 'lib/campaign_monitor/subscriber.rb',
35
+
36
+ 'support/faster-xml-simple/lib/faster_xml_simple.rb',
37
+ 'support/faster-xml-simple/test/regression_test.rb',
38
+ 'support/faster-xml-simple/test/test_helper.rb',
39
+ 'support/faster-xml-simple/test/xml_simple_comparison_test.rb',
40
+
41
+ 'test/test_campaign_monitor.rb',
42
+ ]
43
+
44
+ s.test_file = 'test/test_campaign_monitor.rb'
45
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'campaign_monitor'
data/install.rb ADDED
File without changes
@@ -0,0 +1,289 @@
1
+ # CampaignMonitor
2
+ # A wrapper class to access the Campaign Monitor API. Written using the wonderful
3
+ # Flickr interface by Scott Raymond as a guide on how to access remote web services
4
+ #
5
+ # For more information on the Campaign Monitor API, visit http://campaignmonitor.com/api
6
+ #
7
+ # Author:: Jordan Brock <jordan@spintech.com.au>, The Turing Studio, Inc. <operations@turingstudio.com>
8
+ # Copyright:: Copyright (c) 2006 Jordan Brock <jordan@spintech.com.au>, Copyright (c) 2009 The Turing Studio, Inc. <operations@turingstudio.com>
9
+ # License:: MIT <http://www.opensource.org/licenses/mit-license.php>
10
+ #
11
+ # USAGE:
12
+ # require 'campaign_monitor'
13
+ # cm = CampaignMonitor.new(API_KEY) # creates a CampaignMonitor object
14
+ # # Can set CAMPAIGN_MONITOR_API_KEY in environment.rb
15
+ # cm.clients # Returns an array of clients associated with
16
+ # # the user account
17
+ # cm.campaigns(client_id)
18
+ # cm.lists(client_id)
19
+ # cm.add_subscriber(list_id, email, name)
20
+ #
21
+ # CLIENT
22
+ # client = Client.new(client_id)
23
+ # client.lists
24
+ # client.campaigns
25
+ #
26
+ # LIST
27
+ # list = List.new(list_id)
28
+ # list.add_subscriber(email, name)
29
+ # list.remove_subscriber(email)
30
+ # list.active_subscribers(date)
31
+ # list.unsubscribed(date)
32
+ # list.bounced(date)
33
+ #
34
+ # CAMPAIGN
35
+ # campaign = Campaign.new(campaign_id)
36
+ # campaign.clicks
37
+ # campaign.opens
38
+ # campaign.bounces
39
+ # campaign.unsubscribes
40
+ # campaign.number_recipients
41
+ # campaign.number_clicks
42
+ # campaign.number_opens
43
+ # campaign.number_bounces
44
+ # campaign.number_unsubscribes
45
+ #
46
+ #
47
+ # SUBSCRIBER
48
+ # subscriber = Subscriber.new(email)
49
+ # subscriber.add(list_id)
50
+ # subscriber.unsubscribe(list_id)
51
+ #
52
+ # Data Types
53
+ # SubscriberBounce
54
+ # SubscriberClick
55
+ # SubscriberOpen
56
+ # SubscriberUnsubscribe
57
+ # Result
58
+ #
59
+
60
+ require 'rubygems'
61
+ require 'cgi'
62
+ require 'net/http'
63
+ require 'xmlsimple'
64
+ require 'date'
65
+
66
+ $:.unshift(File.dirname(__FILE__))
67
+ require 'campaign_monitor/helpers.rb'
68
+ require 'campaign_monitor/client.rb'
69
+ require 'campaign_monitor/list.rb'
70
+ require 'campaign_monitor/subscriber.rb'
71
+ require 'campaign_monitor/result.rb'
72
+ require 'campaign_monitor/campaign.rb'
73
+
74
+ class CampaignMonitor
75
+ include CampaignMonitor::Helpers
76
+
77
+ attr_reader :api_key, :api_url
78
+
79
+ # Replace this API key with your own (http://www.campaignmonitor.com/api/)
80
+ def initialize(api_key=CAMPAIGN_MONITOR_API_KEY)
81
+ @api_key = api_key
82
+ @api_url = 'http://api.createsend.com/api/api.asmx'
83
+ end
84
+
85
+
86
+ # Takes a CampaignMonitor API method name and set of parameters;
87
+ # returns an XmlSimple object with the response
88
+ def request(method, params)
89
+ response = PARSER.xml_in(http_get(request_url(method, params)), { 'keeproot' => false,
90
+ 'forcearray' => %w[List Campaign Subscriber Client SubscriberOpen SubscriberUnsubscribe SubscriberClick SubscriberBounce],
91
+ 'noattr' => true })
92
+ response.delete('d1p1:type')
93
+ response
94
+ end
95
+
96
+ # Takes a CampaignMonitor API method name and set of parameters; returns the correct URL for the REST API.
97
+ def request_url(method, params={})
98
+ params.merge!('ApiKey' => api_key)
99
+
100
+ query = params.collect do |key, value|
101
+ "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
102
+ end.sort * '&'
103
+
104
+ "#{api_url}/#{method}?#{query}"
105
+ end
106
+
107
+ # Does an HTTP GET on a given URL and returns the response body
108
+ def http_get(url)
109
+ Net::HTTP.get_response(URI.parse(url)).body.to_s
110
+ end
111
+
112
+ # By overriding the method_missing method, it is possible to easily support all of the methods
113
+ # available in the API
114
+ def method_missing(method_id, params = {})
115
+ request(method_id.id2name.gsub(/_/, '.'), params)
116
+ end
117
+
118
+ # Returns an array of Client objects associated with the API Key
119
+ #
120
+ # Example
121
+ # @cm = CampaignMonitor.new()
122
+ # @clients = @cm.clients
123
+ #
124
+ # for client in @clients
125
+ # puts client.name
126
+ # end
127
+ def clients
128
+ handle_response(User_GetClients()) do |response|
129
+ response["Client"].collect{|c| Client.new(c["ClientID"], c["Name"])}
130
+ end
131
+ end
132
+
133
+ def system_date
134
+ User_GetSystemDate()
135
+ end
136
+
137
+ def parsed_system_date
138
+ DateTime.strptime(system_date, timestamp_format)
139
+ end
140
+
141
+ # Returns an array of Campaign objects associated with the specified Client ID
142
+ #
143
+ # Example
144
+ # @cm = CampaignMonitor.new()
145
+ # @campaigns = @cm.campaigns(12345)
146
+ #
147
+ # for campaign in @campaigns
148
+ # puts campaign.subject
149
+ # end
150
+ def campaigns(client_id)
151
+ handle_response(Client_GetCampaigns("ClientID" => client_id)) do |response|
152
+ response["Campaign"].to_a.collect{|c| Campaign.new(c["CampaignID"], c["Subject"], c["SentDate"], c["TotalRecipients"].to_i)}
153
+ end
154
+ end
155
+
156
+ # Returns an array of Subscriber Lists for the specified Client ID
157
+ #
158
+ # Example
159
+ # @cm = CampaignMonitor.new()
160
+ # @lists = @cm.lists(12345)
161
+ #
162
+ # for list in @lists
163
+ # puts list.name
164
+ # end
165
+ def lists(client_id)
166
+ handle_response(Client_GetLists("ClientID" => client_id)) do |response|
167
+ response["List"].to_a.collect{|l| List.new(l["ListID"], l["Name"])}
168
+ end
169
+ end
170
+
171
+ # A quick method of adding a subscriber to a list. Returns a Result object
172
+ #
173
+ # Example
174
+ # @cm = CampaignMonitor.new()
175
+ # result = @cm.add_subscriber(12345, "ralph.wiggum@simpsons.net", "Ralph Wiggum")
176
+ #
177
+ # if result.succeeded?
178
+ # puts "Subscriber Added to List"
179
+ # end
180
+ # email The subscriber's email address.
181
+ # name The subscriber's name.
182
+ # custom_fields A hash of field name => value pairs.
183
+ def add_subscriber(list_id, email, name, force = false, custom_fields = {})
184
+ if custom_fields.empty?
185
+ method = force ? 'Subscriber_AddAndResubscribe' : 'Subscriber_Add'
186
+ response = send(method,
187
+ "ListID" => list_id,
188
+ "Email" => email,
189
+ "Name" => name
190
+ )
191
+ Result.new(response)
192
+ else
193
+ if force
194
+ request_method = 'AddAndResubscribeWithCustomFields'
195
+ result_method = 'Subscriber.AddAndResubscribeWithCustomFieldsResult'
196
+ else
197
+ request_method = 'AddSubscriberWithCustomFields'
198
+ result_method = 'Subscriber.AddWithCustomFieldsResult'
199
+ end
200
+
201
+ response = using_soap do |driver|
202
+ driver.send(request_method,
203
+ :ApiKey => api_key,
204
+ :ListID => list_id,
205
+ :Email => email,
206
+ :Name => name,
207
+ :CustomFields => { :SubscriberCustomField => custom_fields_array(custom_fields) }
208
+ )
209
+ end
210
+
211
+ Result.new(response[result_method])
212
+ end
213
+ end
214
+
215
+ def custom_fields_array(custom_fields)
216
+ arr = []
217
+ custom_fields.each do |key, value|
218
+ arr << { "Key" => key, "Value" => value }
219
+ end
220
+ arr
221
+ end
222
+
223
+ # Encapsulates
224
+ class SubscriberBounce
225
+ attr_reader :email_address, :bounce_type, :list_id
226
+
227
+ def initialize(email_address, list_id, bounce_type)
228
+ @email_address = email_address
229
+ @bounce_type = bounce_type
230
+ @list_id = list_id
231
+ end
232
+ end
233
+
234
+ # Encapsulates
235
+ class SubscriberOpen
236
+ attr_reader :email_address, :list_id, :opens
237
+
238
+ def initialize(email_address, list_id, opens)
239
+ @email_address = email_address
240
+ @list_id = list_id
241
+ @opens = opens
242
+ end
243
+ end
244
+
245
+ # Encapsulates
246
+ class SubscriberClick
247
+ attr_reader :email_address, :list_id, :clicked_links
248
+
249
+ def initialize(email_address, list_id, clicked_links)
250
+ @email_address = email_address
251
+ @list_id = list_id
252
+ @clicked_links = clicked_links
253
+ end
254
+ end
255
+
256
+ # Encapsulates
257
+ class SubscriberUnsubscribe
258
+ attr_reader :email_address, :list_id
259
+
260
+ def initialize(email_address, list_id)
261
+ @email_address = email_address
262
+ @list_id = list_id
263
+ end
264
+ end
265
+
266
+ end
267
+
268
+ # If libxml is installed, we use the FasterXmlSimple library, that provides most of the functionality of XmlSimple
269
+ # except it uses the xml/libxml library for xml parsing (rather than REXML).
270
+ # If libxml isn't installed, we just fall back on XmlSimple.
271
+
272
+ PARSER =
273
+ begin
274
+ require 'xml/libxml'
275
+ # Older version of libxml aren't stable (bus error when requesting attributes that don't exist) so we
276
+ # have to use a version greater than '0.3.8.2'.
277
+ raise LoadError unless XML::Parser::VERSION > '0.3.8.2'
278
+ $:.push(File.join(File.dirname(__FILE__), '..', 'support', 'faster-xml-simple', 'lib'))
279
+ require 'faster_xml_simple'
280
+ p 'Using libxml-ruby'
281
+ FasterXmlSimple
282
+ rescue LoadError
283
+ begin
284
+ require 'rexml-expansion-fix'
285
+ rescue LoadError => e
286
+ p 'Cannot load rexml security patch'
287
+ end
288
+ XmlSimple
289
+ end
@@ -0,0 +1,121 @@
1
+ class CampaignMonitor
2
+ # Provides access to the information about a campaign
3
+ class Campaign
4
+ include CampaignMonitor::Helpers
5
+
6
+ attr_reader :id, :subject, :sent_date, :total_recipients, :cm_client
7
+
8
+ def initialize(id=nil, subject=nil, sent_date=nil, total_recipients=nil)
9
+ @id = id
10
+ @subject = subject
11
+ @sent_date = sent_date
12
+ @total_recipients = total_recipients
13
+ @cm_client = CampaignMonitor.new
14
+ end
15
+
16
+ # Example
17
+ # @campaign = Campaign.new(12345)
18
+ # @subscriber_opens = @campaign.opens
19
+ #
20
+ # for subscriber in @subscriber_opens
21
+ # puts subscriber.email
22
+ # end
23
+ def opens
24
+ handle_response(cm_client.Campaign_GetOpens("CampaignID" => self.id)) do |response|
25
+ response["SubscriberOpen"].collect{|s| SubscriberOpen.new(s["EmailAddress"], s["ListID"], s["NumberOfOpens"])}
26
+ end
27
+ end
28
+
29
+ # Example
30
+ # @campaign = Campaign.new(12345)
31
+ # @subscriber_bounces = @campaign.bounces
32
+ #
33
+ # for subscriber in @subscriber_bounces
34
+ # puts subscriber.email
35
+ # end
36
+ def bounces
37
+ handle_response(cm_client.Campaign_GetBounces("CampaignID"=> self.id)) do |response|
38
+ response["SubscriberBounce"].collect{|s| SubscriberBounce.new(s["EmailAddress"], s["ListID"], s["BounceType"])}
39
+ end
40
+ end
41
+
42
+ # Example
43
+ # @campaign = Campaign.new(12345)
44
+ # @subscriber_clicks = @campaign.clicks
45
+ #
46
+ # for subscriber in @subscriber_clicks
47
+ # puts subscriber.email
48
+ # end
49
+ def clicks
50
+ handle_response(cm_client.Campaign_GetSubscriberClicks("CampaignID" => self.id)) do |response|
51
+ response["SubscriberClick"].collect{|s| SubscriberClick.new(s["EmailAddress"], s["ListID"], s["ClickedLinks"])}
52
+ end
53
+ end
54
+
55
+ # Example
56
+ # @campaign = Campaign.new(12345)
57
+ # @subscriber_unsubscribes = @campaign.unsubscribes
58
+ #
59
+ # for subscriber in @subscriber_unsubscribes
60
+ # puts subscriber.email
61
+ # end
62
+ def unsubscribes
63
+ handle_response(cm_client.Campaign_GetUnsubscribes("CampaignID" => self.id)) do |response|
64
+ response["SubscriberUnsubscribe"].collect{|s| SubscriberUnsubscribe.new(s["EmailAddress"], s["ListID"])}
65
+ end
66
+ end
67
+
68
+ # Example
69
+ # @campaign = Campaign.new(12345)
70
+ # puts @campaign.number_recipients
71
+ def number_recipients
72
+ @number_recipients ||= attributes[:number_recipients]
73
+ end
74
+
75
+ # Example
76
+ # @campaign = Campaign.new(12345)
77
+ # puts @campaign.number_opened
78
+ def number_opened
79
+ @number_opened ||= attributes[:number_opened]
80
+ end
81
+
82
+ # Example
83
+ # @campaign = Campaign.new(12345)
84
+ # puts @campaign.number_clicks
85
+ def number_clicks
86
+ @number_clicks ||= attributes[:number_clicks]
87
+ end
88
+
89
+ # Example
90
+ # @campaign = Campaign.new(12345)
91
+ # puts @campaign.number_unsubscribed
92
+ def number_unsubscribed
93
+ @number_unsubscribed ||= attributes[:number_unsubscribed]
94
+ end
95
+
96
+ # Example
97
+ # @campaign = Campaign.new(12345)
98
+ # puts @campaign.number_bounced
99
+ def number_bounced
100
+ @number_bounced ||= attributes[:number_bounced]
101
+ end
102
+
103
+ private
104
+
105
+ def attributes
106
+ @attributes ||= fetch_attributes
107
+ end
108
+
109
+ def fetch_attributes
110
+ summary = cm_client.Campaign_GetSummary('CampaignID' => self.id)
111
+
112
+ {
113
+ :number_recipients => summary['Recipients'].to_i,
114
+ :number_opened => summary['TotalOpened'].to_i,
115
+ :number_clicks => summary['Click'].to_i,
116
+ :number_unsubscribed => summary['Unsubscribed'].to_i,
117
+ :number_bounced => summary['Bounced'].to_i
118
+ }
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,38 @@
1
+ class CampaignMonitor
2
+ # Provides access to the lists and campaigns associated with a client
3
+ class Client
4
+ include CampaignMonitor::Helpers
5
+
6
+ attr_reader :id, :name, :cm_client
7
+
8
+ # Example
9
+ # @client = new Client(12345)
10
+ def initialize(id, name=nil)
11
+ @id = id
12
+ @name = name
13
+ @cm_client = CampaignMonitor.new
14
+ end
15
+
16
+ # Example
17
+ # @client = new Client(12345)
18
+ # @lists = @client.lists
19
+ #
20
+ # for list in @lists
21
+ # puts list.name
22
+ # end
23
+ def lists
24
+ cm_client.lists(self.id)
25
+ end
26
+
27
+ # Example
28
+ # @client = new Client(12345)
29
+ # @campaigns = @client.campaigns
30
+ #
31
+ # for campaign in @campaigns
32
+ # puts campaign.subject
33
+ # end
34
+ def campaigns
35
+ cm_client.campaigns(self.id)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ class CampaignMonitor
2
+ module Helpers
3
+
4
+ def handle_response(response)
5
+ return [] if response.empty?
6
+
7
+ if response["Code"].to_i == 0
8
+ # success!
9
+ yield(response)
10
+ else
11
+ # error!
12
+ raise response["Code"] + " - " + response["Message"]
13
+ end
14
+ end
15
+
16
+ def wsdl_driver_factory
17
+ SOAP::WSDLDriverFactory.new("#{api_url}?WSDL")
18
+ end
19
+
20
+ def using_soap
21
+ driver = wsdl_driver_factory.create_rpc_driver
22
+ response = yield(driver)
23
+ driver.reset_stream
24
+ response
25
+ end
26
+
27
+ def timestamp_format
28
+ '%Y-%m-%d %H:%M:%S'
29
+ end
30
+
31
+ def formatted_timestamp(datetime, format=timestamp_format)
32
+ datetime.strftime(format)
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,94 @@
1
+ require 'soap/wsdlDriver'
2
+
3
+ class CampaignMonitor
4
+ # Provides access to the subscribers and info about subscribers
5
+ # associated with a Mailing List
6
+ class List
7
+ include CampaignMonitor::Helpers
8
+
9
+ attr_reader :id, :name, :cm_client
10
+
11
+ # Example
12
+ # @list = new List(12345)
13
+ def initialize(id=nil, name=nil)
14
+ @id = id
15
+ @name = name
16
+ @cm_client = CampaignMonitor.new
17
+ end
18
+
19
+ # Example
20
+ # @list = new List(12345)
21
+ # result = @list.add_subscriber("ralph.wiggum@simpsons.net")
22
+ #
23
+ # if result.succeeded?
24
+ # puts "Added Subscriber"
25
+ # end
26
+ def add_subscriber(email, name = nil, custom_fields = {})
27
+ cm_client.add_subscriber(self.id, email, name, false, custom_fields)
28
+ end
29
+
30
+ def add_and_resubscribe(email, name = nil, custom_fields = {})
31
+ cm_client.add_subscriber(self.id, email, name, true, custom_fields)
32
+ end
33
+
34
+ # Example
35
+ # @list = new List(12345)
36
+ # result = @list.remove_subscriber("ralph.wiggum@simpsons.net")
37
+ #
38
+ # if result.succeeded?
39
+ # puts "Deleted Subscriber"
40
+ # end
41
+ def remove_subscriber(email)
42
+ Result.new(cm_client.Subscriber_Unsubscribe("ListID" => self.id, "Email" => email))
43
+ end
44
+
45
+ # Example
46
+ # current_date = DateTime.new
47
+ # @list = new List(12345)
48
+ # @subscribers = @list.active_subscribers(current_date)
49
+ #
50
+ # for subscriber in @subscribers
51
+ # puts subscriber.email
52
+ # end
53
+ def active_subscribers(date)
54
+ response = cm_client.Subscribers_GetActive('ListID' => self.id, 'Date' => formatted_timestamp(date))
55
+ handle_response(response) do
56
+ response['Subscriber'].collect{|s| Subscriber.new(s['EmailAddress'], s['Name'], s['Date'])}
57
+ end
58
+ end
59
+
60
+ # Example
61
+ # current_date = DateTime.new
62
+ # @list = new List(12345)
63
+ # @subscribers = @list.unsubscribed(current_date)
64
+ #
65
+ # for subscriber in @subscribers
66
+ # puts subscriber.email
67
+ # end
68
+ def unsubscribed(date)
69
+ date = formatted_timestamp(date) unless date.is_a?(String)
70
+
71
+ response = cm_client.Subscribers_GetUnsubscribed('ListID' => self.id, 'Date' => date)
72
+
73
+ handle_response(response) do
74
+ response['Subscriber'].collect{|s| Subscriber.new(s['EmailAddress'], s['Name'], s['Date'])}
75
+ end
76
+ end
77
+
78
+ # Example
79
+ # current_date = DateTime.new
80
+ # @list = new List(12345)
81
+ # @subscribers = @list.bounced(current_date)
82
+ #
83
+ # for subscriber in @subscribers
84
+ # puts subscriber.email
85
+ # end
86
+ def bounced(date)
87
+ response = cm_client.Subscribers_GetBounced('ListID' => self.id, 'Date' => formatted_timestamp(date))
88
+
89
+ handle_response(response) do
90
+ response["Subscriber"].collect{|s| Subscriber.new(s["EmailAddress"], s["Name"], s["Date"])}
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,19 @@
1
+ class CampaignMonitor
2
+ # Encapsulates the response received from the CampaignMonitor webservice.
3
+ class Result
4
+ attr_reader :message, :code
5
+
6
+ def initialize(response)
7
+ @message = response["Message"]
8
+ @code = response["Code"].to_i
9
+ end
10
+
11
+ def succeeded?
12
+ code == 0
13
+ end
14
+
15
+ def failed?
16
+ !succeeded?
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ class CampaignMonitor
2
+ # Provides the ability to add/remove subscribers from a list
3
+ class Subscriber
4
+ include CampaignMonitor::Helpers
5
+
6
+ attr_accessor :email_address, :name, :date_subscribed
7
+ attr_reader :cm_client
8
+
9
+ def initialize(email_address, name=nil, date=nil)
10
+ @email_address = email_address
11
+ @name = name
12
+ @date_subscribed = date_subscribed
13
+ @cm_client = CampaignMonitor.new
14
+ end
15
+
16
+ # Example
17
+ # @subscriber = Subscriber.new("ralph.wiggum@simpsons.net")
18
+ # @subscriber.add(12345)
19
+ def add(list_id, custom_fields = {})
20
+ cm_client.add_subscriber(list_id, @email_address, @name, false, custom_fields)
21
+ end
22
+
23
+ # Example
24
+ # @subscriber = Subscriber.new("ralph.wiggum@simpsons.net")
25
+ # @subscriber.add_and_resubscribe(12345)
26
+ def add_and_resubscribe(list_id, custom_fields = {})
27
+ cm_client.add_subscriber(list_id, @email_address, @name, true, custom_fields)
28
+ end
29
+
30
+ # Example
31
+ # @subscriber = Subscriber.new("ralph.wiggum@simpsons.net")
32
+ # @subscriber.unsubscribe(12345)
33
+ def unsubscribe(list_id)
34
+ Result.new(cm_client.Subscriber_Unsubscribe("ListID" => list_id, "Email" => @email_address))
35
+ end
36
+
37
+ def is_subscribed?(list_id)
38
+ result = cm_client.Subscribers_GetIsSubscribed("ListID" => list_id, "Email" => @email_address)
39
+ return true if result == 'True'
40
+ return false if result == 'False'
41
+ raise "Invalid value for is_subscribed?: #{result}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,187 @@
1
+ #
2
+ # Copyright (c) 2006 Michael Koziarski
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ # this software and associated documentation files (the "Software"), to deal in the
6
+ # Software without restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8
+ # Software, and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
18
+ # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
19
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+
21
+ require 'rubygems'
22
+ require 'xml/libxml'
23
+
24
+ class FasterXmlSimple
25
+ Version = '0.5.0'
26
+ class << self
27
+ # Take an string containing XML, and returns a hash representing that
28
+ # XML document. For example:
29
+ #
30
+ # FasterXmlSimple.xml_in("<root><something>1</something></root>")
31
+ # {"root"=>{"something"=>{"__content__"=>"1"}}}
32
+ #
33
+ # Faster XML Simple is designed to be a drop in replacement for the xml_in
34
+ # functionality of http://xml-simple.rubyforge.org
35
+ #
36
+ # The following options are supported:
37
+ #
38
+ # * <tt>contentkey</tt>: The key to use for the content of text elements,
39
+ # defaults to '\_\_content__'
40
+ # * <tt>forcearray</tt>: The list of elements which should always be returned
41
+ # as arrays. Under normal circumstances single element arrays are inlined.
42
+ # * <tt>suppressempty</tt>: The value to return for empty elements, pass +true+
43
+ # to remove empty elements entirely.
44
+ # * <tt>keeproot</tt>: By default the hash returned has a single key with the
45
+ # name of the root element. If the name of the root element isn't
46
+ # interesting to you, pass +false+.
47
+ # * <tt>forcecontent</tt>: By default a text element with no attributes, will
48
+ # be collapsed to just a string instead of a hash with a single key.
49
+ # Pass +true+ to prevent this.
50
+ #
51
+ #
52
+ def xml_in(string, options={})
53
+ new(string, options).out
54
+ end
55
+ end
56
+
57
+ def initialize(string, options) #:nodoc:
58
+ @doc = parse(string)
59
+ @options = default_options.merge options
60
+ end
61
+
62
+ def out #:nodoc:
63
+ if @options['keeproot']
64
+ {@doc.root.name => collapse(@doc.root)}
65
+ else
66
+ collapse(@doc.root)
67
+ end
68
+ end
69
+
70
+ private
71
+ def default_options
72
+ {'contentkey' => '__content__', 'forcearray' => [], 'keeproot'=>true}
73
+ end
74
+
75
+ def collapse(element)
76
+ result = hash_of_attributes(element)
77
+ if text_node? element
78
+ text = collapse_text(element)
79
+ result[content_key] = text if text =~ /\S/
80
+ elsif element.children?
81
+ element.inject(result) do |hash, child|
82
+ unless child.text?
83
+ child_result = collapse(child)
84
+ (hash[child.name] ||= []) << child_result
85
+ end
86
+ hash
87
+ end
88
+ end
89
+ if result.empty?
90
+ return empty_element
91
+ end
92
+ # Compact them to ensure it complies with the user's requests
93
+ inline_single_element_arrays(result)
94
+ remove_empty_elements(result) if suppress_empty?
95
+ if content_only?(result) && !force_content?
96
+ result[content_key]
97
+ else
98
+ result
99
+ end
100
+ end
101
+
102
+ def content_only?(result)
103
+ result.keys == [content_key]
104
+ end
105
+
106
+ def content_key
107
+ @options['contentkey']
108
+ end
109
+
110
+ def force_array?(key_name)
111
+ Array(@options['forcearray']).include?(key_name)
112
+ end
113
+
114
+ def inline_single_element_arrays(result)
115
+ result.each do |key, value|
116
+ if value.size == 1 && value.is_a?(Array) && !force_array?(key)
117
+ result[key] = value.first
118
+ end
119
+ end
120
+ end
121
+
122
+ def remove_empty_elements(result)
123
+ result.each do |key, value|
124
+ if value == empty_element
125
+ result.delete key
126
+ end
127
+ end
128
+ end
129
+
130
+ def suppress_empty?
131
+ @options['suppressempty'] == true
132
+ end
133
+
134
+ def empty_element
135
+ if !@options.has_key? 'suppressempty'
136
+ {}
137
+ else
138
+ @options['suppressempty']
139
+ end
140
+ end
141
+
142
+ # removes the content if it's nothing but blanks, prevents
143
+ # the hash being polluted with lots of content like "\n\t\t\t"
144
+ def suppress_empty_content(result)
145
+ result.delete content_key if result[content_key] !~ /\S/
146
+ end
147
+
148
+ def force_content?
149
+ @options['forcecontent']
150
+ end
151
+
152
+ # a text node is one with 1 or more child nodes which are
153
+ # text nodes, and no non-text children, there's no sensible
154
+ # way to support nodes which are text and markup like:
155
+ # <p>Something <b>Bold</b> </p>
156
+ def text_node?(element)
157
+ !element.text? && element.all? {|c| c.text?}
158
+ end
159
+
160
+ # takes a text node, and collapses it into a string
161
+ def collapse_text(element)
162
+ element.map {|c| c.content } * ''
163
+ end
164
+
165
+ def hash_of_attributes(element)
166
+ result = {}
167
+ element.each_attr do |attribute|
168
+ name = attribute.name
169
+ name = [attribute.ns, attribute.name].join(':') if attribute.ns?
170
+ result[name] = attribute.value
171
+ end
172
+ result
173
+ end
174
+
175
+ def parse(string)
176
+ if string == ''
177
+ string = ' '
178
+ end
179
+ XML::Parser.string(string).parse
180
+ end
181
+ end
182
+
183
+ class XmlSimple # :nodoc:
184
+ def self.xml_in(*args)
185
+ FasterXmlSimple.xml_in *args
186
+ end
187
+ end
@@ -0,0 +1,47 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class RegressionTest < FasterXSTest
4
+ def test_content_nil_regressions
5
+ expected = {"asdf"=>{"jklsemicolon"=>{}}}
6
+ assert_equal expected, FasterXmlSimple.xml_in("<asdf><jklsemicolon /></asdf>")
7
+ assert_equal expected, FasterXmlSimple.xml_in("<asdf><jklsemicolon /></asdf>", 'forcearray'=>['asdf'])
8
+ end
9
+
10
+ def test_s3_regression
11
+ str = File.read("test/fixtures/test-7.xml")
12
+ assert_nil FasterXmlSimple.xml_in(str)["AccessControlPolicy"]["AccessControlList"]["__content__"]
13
+ end
14
+
15
+ def test_xml_simple_transparency
16
+ assert_equal XmlSimple.xml_in("<asdf />"), FasterXmlSimple.xml_in("<asdf />")
17
+ end
18
+
19
+ def test_suppress_empty_variations
20
+ str = "<asdf><fdsa /></asdf>"
21
+
22
+ assert_equal Hash.new, FasterXmlSimple.xml_in(str)["asdf"]["fdsa"]
23
+ assert_nil FasterXmlSimple.xml_in(str, 'suppressempty'=>nil)["asdf"]["fdsa"]
24
+ assert_equal '', FasterXmlSimple.xml_in(str, 'suppressempty'=>'')["asdf"]["fdsa"]
25
+ assert !FasterXmlSimple.xml_in(str, 'suppressempty'=>true)["asdf"].has_key?("fdsa")
26
+ end
27
+
28
+ def test_empty_string_doesnt_crash
29
+ assert_raise(XML::Parser::ParseError) do
30
+ silence_stderr do
31
+ FasterXmlSimple.xml_in('')
32
+ end
33
+ end
34
+ end
35
+
36
+ def test_keeproot_false
37
+ str = "<asdf><fdsa>1</fdsa></asdf>"
38
+ expected = {"fdsa"=>"1"}
39
+ assert_equal expected, FasterXmlSimple.xml_in(str, 'keeproot'=>false)
40
+ end
41
+
42
+ def test_keeproot_false_with_force_content
43
+ str = "<asdf><fdsa>1</fdsa></asdf>"
44
+ expected = {"fdsa"=>{"__content__"=>"1"}}
45
+ assert_equal expected, FasterXmlSimple.xml_in(str, 'keeproot'=>false, 'forcecontent'=>true)
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+
2
+ require 'test/unit'
3
+ require 'faster_xml_simple'
4
+
5
+ class FasterXSTest < Test::Unit::TestCase
6
+ def default_test
7
+ end
8
+
9
+ def silence_stderr
10
+ str = STDERR.dup
11
+ STDERR.reopen("/dev/null")
12
+ STDERR.sync=true
13
+ yield
14
+ ensure
15
+ STDERR.reopen(str)
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+ require 'yaml'
3
+
4
+ class XmlSimpleComparisonTest < FasterXSTest
5
+
6
+ # Define test methods
7
+
8
+ Dir["test/fixtures/test-*.xml"].each do |file_name|
9
+ xml_file_name = file_name
10
+ method_name = File.basename(file_name, ".xml").gsub('-', '_')
11
+ yml_file_name = file_name.gsub('xml', 'yml')
12
+ rails_yml_file_name = file_name.gsub('xml', 'rails.yml')
13
+ class_eval <<-EOV, __FILE__, __LINE__
14
+ def #{method_name}
15
+ assert_equal YAML.load(File.read('#{yml_file_name}')),
16
+ FasterXmlSimple.xml_in(File.read('#{xml_file_name}'), default_options )
17
+ end
18
+
19
+ def #{method_name}_rails
20
+ assert_equal YAML.load(File.read('#{rails_yml_file_name}')),
21
+ FasterXmlSimple.xml_in(File.read('#{xml_file_name}'), rails_options)
22
+ end
23
+ EOV
24
+ end
25
+
26
+ def default_options
27
+ {
28
+ 'keeproot' => true,
29
+ 'contentkey' => '__content__',
30
+ 'forcecontent' => true,
31
+ 'suppressempty' => nil,
32
+ 'forcearray' => ['something-else']
33
+ }
34
+ end
35
+
36
+ def rails_options
37
+ {
38
+ 'forcearray' => false,
39
+ 'forcecontent' => true,
40
+ 'keeproot' => true,
41
+ 'contentkey' => '__content__'
42
+ }
43
+ end
44
+
45
+
46
+ end
@@ -0,0 +1,93 @@
1
+ require 'rubygems'
2
+ require 'campaign_monitor'
3
+ require 'test/unit'
4
+
5
+ CAMPAIGN_MONITOR_API_KEY = 'Your API key'
6
+ CLIENT_NAME = 'TestClient'
7
+ LIST_NAME = 'TestList'
8
+ CAMPAIGN_NAME = 'TestCampaign'
9
+
10
+ class CampaignMonitorTest < Test::Unit::TestCase
11
+ def setup
12
+ @cm = CampaignMonitor.new
13
+ end
14
+
15
+ def test_has_clients
16
+ clients = @cm.clients
17
+ assert clients.size > 0
18
+ end
19
+
20
+ def test_default_client
21
+ client = default_client
22
+ assert_not_nil client
23
+ assert_equal CLIENT_NAME, client.name
24
+ end
25
+
26
+ def test_has_lists
27
+ lists = default_client.lists
28
+ assert lists.size > 0
29
+ end
30
+
31
+ def test_default_list
32
+ list = default_list
33
+ assert_not_nil list
34
+ assert_equal LIST_NAME, list.name
35
+ end
36
+
37
+ def test_add_subscriber_fail
38
+ result = @cm.add_subscriber(1, 2, 3)
39
+ assert_equal result.code, 101
40
+ assert_not_nil result.message
41
+ end
42
+
43
+ def test_list_add_subscriber
44
+ list = default_list
45
+ assert_success list.add_and_resubscribe('first@example.com', 'Test Subscriber')
46
+ assert_success list.remove_subscriber('first@example.com')
47
+ end
48
+
49
+ def test_list_add_subscriber_fail
50
+ list = default_list
51
+ list.remove_subscriber('first@example.com')
52
+ assert_failure list.add_subscriber('first@example.com', 'Test Subscriber')
53
+ end
54
+
55
+ def test_add_and_resubscribe_with_custom_fields
56
+ list = default_list
57
+ assert_success list.add_and_resubscribe('third@example.com', 'Test Subscriber', 'TestKey' => 'TestValue')
58
+ assert_success list.remove_subscriber('third@example.com')
59
+ end
60
+
61
+ def test_add_with_custom_fields
62
+ list = default_list
63
+ assert_success list.add_subscriber('fourth@example.com', 'Test Subscriber', 'TestKey' => 'TestValue')
64
+ list.remove_subscriber('fourth@example.com')
65
+ end
66
+
67
+ def test_campaigns
68
+ client = default_client
69
+ assert_equal client.campaigns, []
70
+ end
71
+
72
+ protected
73
+
74
+ def assert_success(result)
75
+ assert result.succeeded?, result.message
76
+ end
77
+
78
+ def assert_failure(result)
79
+ assert result.failed?
80
+ end
81
+
82
+ def default_client(clients = @cm.clients)
83
+ clients.detect { |c| c.name == CLIENT_NAME }
84
+ end
85
+
86
+ def default_list(lists = default_client.lists)
87
+ lists.detect { |l| l.name == LIST_NAME }
88
+ end
89
+
90
+ def default_campaign(campaigns = default_client.campaigns)
91
+ campaigns.detect { |c| c.name == CAMPAIGN_NAME }
92
+ end
93
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: turingstudio-campaign_monitor
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.3.1
5
+ platform: ruby
6
+ authors:
7
+ - The Turing Studio, Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-11 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: xml-simple
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.11
23
+ version:
24
+ description: A simple wrapper class that provides basic access to the Campaign Monitor API.
25
+ email: operations@turingstudio.com
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files: []
31
+
32
+ files:
33
+ - campaign_monitor.gemspec
34
+ - init.rb
35
+ - install.rb
36
+ - MIT-LICENSE
37
+ - Rakefile
38
+ - README.rdoc
39
+ - lib/campaign_monitor.rb
40
+ - lib/campaign_monitor/campaign.rb
41
+ - lib/campaign_monitor/client.rb
42
+ - lib/campaign_monitor/helpers.rb
43
+ - lib/campaign_monitor/list.rb
44
+ - lib/campaign_monitor/result.rb
45
+ - lib/campaign_monitor/subscriber.rb
46
+ - support/faster-xml-simple/lib/faster_xml_simple.rb
47
+ - support/faster-xml-simple/test/regression_test.rb
48
+ - support/faster-xml-simple/test/test_helper.rb
49
+ - support/faster-xml-simple/test/xml_simple_comparison_test.rb
50
+ - test/test_campaign_monitor.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/turingstudio/campaign-monitor-ruby/
53
+ post_install_message:
54
+ rdoc_options: []
55
+
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ requirements:
71
+ - none
72
+ rubyforge_project:
73
+ rubygems_version: 1.2.0
74
+ signing_key:
75
+ specification_version: 2
76
+ summary: Provides access to the Campaign Monitor API.
77
+ test_files:
78
+ - test/test_campaign_monitor.rb