turingstudio-campaign_monitor 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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