reidab-campaign_monitor 1.3.3

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.
@@ -0,0 +1,27 @@
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
+ elsif response["Code"].to_i == 100
11
+ raise InvalidAPIKey
12
+ else
13
+ # error!
14
+ raise ApiError, response["Code"] + ": " + response["Message"]
15
+ end
16
+ end
17
+
18
+ def timestamp_format
19
+ '%Y-%m-%d %H:%M:%S'
20
+ end
21
+
22
+ def formatted_timestamp(datetime, format=timestamp_format)
23
+ datetime.strftime(format)
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,217 @@
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 < Base
7
+ include CampaignMonitor::Helpers
8
+
9
+ id_field "ListID"
10
+ name_field "Title"
11
+
12
+ VALID_ATTRIBUTES=%w{ConfirmOptIn UnsubscribePage ConfirmationSuccessPage ListID Title}
13
+
14
+ # Example
15
+ # @list = new List(12345)
16
+ def initialize(attrs={})
17
+ super
18
+ @attributes=attrs
19
+ end
20
+
21
+ # Example
22
+ #
23
+ # @list = @client.new_list.defaults
24
+
25
+ def defaults
26
+ defaults={"ConfirmOptIn" => "false",
27
+ "UnsubscribePage" => "",
28
+ "ConfirmationSuccessPage" => ""}
29
+ @attributes=defaults.merge(@attributes)
30
+ self
31
+ end
32
+
33
+ # AR like
34
+ def save
35
+ id ? Update : Create
36
+ end
37
+
38
+ # Loads a list by it's ID
39
+ #
40
+ # @list = List.GetDetail(1234)
41
+ def self.GetDetail(id)
42
+ list=self.new("ListID" => id)
43
+ list.GetDetail(true)
44
+ list.result.code == 101 ? nil : list
45
+ end
46
+
47
+ # loads a list by it's ID
48
+ #
49
+ # @list = List.GetDetail(1234)
50
+ def self.[](k)
51
+ GetDetail(k)
52
+ end
53
+
54
+ def GetDetail(overwrite=false)
55
+ @result=Result.new(cm_client.List_GetDetail("ListID" => id))
56
+ @attributes=@result.raw.merge(@attributes)
57
+ @attributes.merge!(@result.raw) if overwrite
58
+ @result.success?
59
+ end
60
+
61
+ def Update
62
+ # if we're dealing with a half baked object that Client#lists has given
63
+ # us then we need to popular all the fields before we can attempt an update
64
+ unless @fully_baked
65
+ self.GetDetail
66
+ @fully_baked=true
67
+ end
68
+ @result=Result.new(cm_client.List_Update(@attributes))
69
+ @result.success?
70
+ end
71
+
72
+ def Delete
73
+ @result=Result.new(cm_client.List_Delete("ListID" => id))
74
+ @result.success?
75
+ end
76
+
77
+ def Create
78
+ @result=Result.new(cm_client.List_Create(@attributes))
79
+ self.id = @result.content if @result.success?
80
+ @result.success?
81
+ end
82
+
83
+ # Example
84
+ # @list = new List(12345)
85
+ # result = @list.add_subscriber("ralph.wiggum@simpsons.net")
86
+ #
87
+ # if result.succeeded?
88
+ # puts "Added Subscriber"
89
+ # end
90
+ def add_subscriber(email, name=nil, custom_fields=nil)
91
+ if custom_fields.nil?
92
+ Result.new(cm_client.Subscriber_Add("ListID" => self.id, "Email" => email, "Name" => name))
93
+ else
94
+ add_subscriber_with_custom_fields(email, name, custom_fields)
95
+ end
96
+ end
97
+
98
+ def add_and_resubscribe(email, name=nil, custom_fields=nil)
99
+ if custom_fields.nil?
100
+ Result.new(cm_client.Subscriber_AddAndResubscribe("ListID" => self.id, "Email" => email, "Name" => name))
101
+ else
102
+ add_and_resubscribe_with_custom_fields(email, name, custom_fields)
103
+ end
104
+ end
105
+
106
+ # Example
107
+ # @list = new List(12345)
108
+ # result = @list.remove_subscriber("ralph.wiggum@simpsons.net")
109
+ #
110
+ # if result.succeeded?
111
+ # puts "Deleted Subscriber"
112
+ # end
113
+ def remove_subscriber(email)
114
+ Result.new(cm_client.Subscriber_Unsubscribe("ListID" => self.id, "Email" => email))
115
+ end
116
+
117
+ # email The subscriber's email address.
118
+ # name The subscriber's name.
119
+ # custom_fields A hash of field name => value pairs.
120
+ def add_subscriber_with_custom_fields(email, name, custom_fields)
121
+ response = cm_client.using_soap do |driver|
122
+ driver.addSubscriberWithCustomFields \
123
+ :ApiKey => cm_client.api_key,
124
+ :ListID => self.id,
125
+ :Email => email,
126
+ :Name => name,
127
+ :CustomFields => { :SubscriberCustomField => custom_fields_array(custom_fields) }
128
+ end
129
+
130
+ response.subscriber_AddWithCustomFieldsResult
131
+ end
132
+
133
+ # email The subscriber's email address.
134
+ # name The subscriber's name.
135
+ # custom_fields A hash of field name => value pairs.
136
+ def add_and_resubscribe_with_custom_fields(email, name, custom_fields)
137
+ response = cm_client.using_soap do |driver|
138
+ driver.addAndResubscribeWithCustomFields \
139
+ :ApiKey => cm_client.api_key,
140
+ :ListID => self.id,
141
+ :Email => email,
142
+ :Name => name,
143
+ :CustomFields => { :SubscriberCustomField => custom_fields_array(custom_fields) }
144
+ end
145
+
146
+ response.subscriber_AddAndResubscribeWithCustomFieldsResult
147
+ end
148
+
149
+ # Example
150
+ # current_date = DateTime.new
151
+ # @list = new List(12345)
152
+ # @subscribers = @list.active_subscribers(current_date)
153
+ #
154
+ # for subscriber in @subscribers
155
+ # puts subscriber.email
156
+ # end
157
+ def active_subscribers(date)
158
+ response = cm_client.Subscribers_GetActive('ListID' => self.id, 'Date' => formatted_timestamp(date))
159
+ handle_response(response) do
160
+ response['Subscriber'].collect{|s| Subscriber.new(s['EmailAddress'], s['Name'], s['Date'])}
161
+ end
162
+ end
163
+
164
+ # Example
165
+ # current_date = DateTime.new
166
+ # @list = new List(12345)
167
+ # @subscribers = @list.unsubscribed(current_date)
168
+ #
169
+ # for subscriber in @subscribers
170
+ # puts subscriber.email
171
+ # end
172
+ def unsubscribed(date)
173
+ date = formatted_timestamp(date) unless date.is_a?(String)
174
+
175
+ response = cm_client.Subscribers_GetUnsubscribed('ListID' => self.id, 'Date' => date)
176
+
177
+ handle_response(response) do
178
+ response['Subscriber'].collect{|s| Subscriber.new(s['EmailAddress'], s['Name'], s['Date'])}
179
+ end
180
+ end
181
+
182
+ # Example
183
+ # current_date = DateTime.new
184
+ # @list = new List(12345)
185
+ # @subscribers = @list.bounced(current_date)
186
+ #
187
+ # for subscriber in @subscribers
188
+ # puts subscriber.email
189
+ # end
190
+ def bounced(date)
191
+ response = cm_client.Subscribers_GetBounced('ListID' => self.id, 'Date' => formatted_timestamp(date))
192
+
193
+ handle_response(response) do
194
+ response["Subscriber"].collect{|s| Subscriber.new(s["EmailAddress"], s["Name"], s["Date"])}
195
+ end
196
+ end
197
+
198
+
199
+ protected
200
+
201
+ # Converts hash of custom field name/values to array of hashes for the SOAP API.
202
+ def custom_fields_array(custom_fields)
203
+ arr = []
204
+ custom_fields.each do |key, value|
205
+ if value.is_a? Array
206
+ value.each do |array_value|
207
+ arr << { "Key" => key, "Value" => array_value }
208
+ end
209
+ else
210
+ arr << { "Key" => key, "Value" => value }
211
+ end
212
+ end
213
+ arr
214
+ end
215
+
216
+ end
217
+ end
@@ -0,0 +1,46 @@
1
+ class CampaignMonitor
2
+
3
+ # Encapsulates
4
+ class SubscriberBounce #:nodoc:
5
+ attr_reader :email_address, :bounce_type, :list_id
6
+
7
+ def initialize(email_address, list_id, bounce_type)
8
+ @email_address = email_address
9
+ @bounce_type = bounce_type
10
+ @list_id = list_id
11
+ end
12
+ end
13
+
14
+ # Encapsulates
15
+ class SubscriberOpen #:nodoc:
16
+ attr_reader :email_address, :list_id, :opens
17
+
18
+ def initialize(email_address, list_id, opens)
19
+ @email_address = email_address
20
+ @list_id = list_id
21
+ @opens = opens
22
+ end
23
+ end
24
+
25
+ # Encapsulates
26
+ class SubscriberClick #:nodoc:
27
+ attr_reader :email_address, :list_id, :clicked_links
28
+
29
+ def initialize(email_address, list_id, clicked_links)
30
+ @email_address = email_address
31
+ @list_id = list_id
32
+ @clicked_links = clicked_links
33
+ end
34
+ end
35
+
36
+ # Encapsulates
37
+ class SubscriberUnsubscribe #:nodoc:
38
+ attr_reader :email_address, :list_id
39
+
40
+ def initialize(email_address, list_id)
41
+ @email_address = email_address
42
+ @list_id = list_id
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,31 @@
1
+ class CampaignMonitor
2
+ # Encapsulates the response received from the CampaignMonitor webservice.
3
+ class Result
4
+ attr_reader :message, :code, :raw
5
+
6
+ def initialize(response)
7
+ @message = response["Message"]
8
+ @code = response["Code"].to_i
9
+ @raw = response
10
+ end
11
+
12
+ def success?
13
+ code == 0
14
+ end
15
+
16
+ def failed?
17
+ not success?
18
+ end
19
+
20
+ def content
21
+ # if we're a string (likely from SOAP)
22
+ return raw if raw.is_a?(String)
23
+ # if we're a hash
24
+ raw["__content__"]
25
+ end
26
+
27
+ alias :succeeded? :success?
28
+ alias :failure? :failed?
29
+
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ class CampaignMonitor
2
+ # Provides the ability to add/remove subscribers from a list
3
+ class Subscriber < Base
4
+ include CampaignMonitor::Helpers
5
+
6
+ attr_accessor :email_address, :name, :date_subscribed
7
+
8
+ def initialize(email_address, name=nil, date=nil)
9
+ @email_address = email_address
10
+ @name = name
11
+ @date_subscribed = date_subscribed
12
+ super
13
+ end
14
+
15
+ # Example
16
+ # @subscriber = Subscriber.new("ralph.wiggum@simpsons.net")
17
+ # @subscriber.add(12345)
18
+ def add(list_id)
19
+ Result.new(cm_client.Subscriber_Add("ListID" => list_id, "Email" => @email_address, "Name" => @name))
20
+ end
21
+
22
+ # Example
23
+ # @subscriber = Subscriber.new("ralph.wiggum@simpsons.net")
24
+ # @subscriber.add_and_resubscribe(12345)
25
+ def add_and_resubscribe(list_id)
26
+ Result.new(cm_client.Subscriber_AddAndResubscribe("ListID" => list_id, "Email" => @email_address, "Name" => @name))
27
+ end
28
+
29
+ # Example
30
+ # @subscriber = Subscriber.new("ralph.wiggum@simpsons.net")
31
+ # @subscriber.unsubscribe(12345)
32
+ def unsubscribe(list_id)
33
+ Result.new(cm_client.Subscriber_Unsubscribe("ListID" => list_id, "Email" => @email_address))
34
+ end
35
+
36
+ def is_subscribed?(list_id)
37
+ result = cm_client.Subscribers_GetIsSubscribed("ListID" => list_id, "Email" => @email_address)
38
+ return true if result['__content__'] == 'True'
39
+ return false if result['__content__'] == 'False'
40
+ raise "Invalid value for is_subscribed?: #{result}"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,35 @@
1
+ module ClassEnhancements
2
+
3
+ def inherited_property(accessor, default = nil)
4
+ instance_eval <<-RUBY, __FILE__, __LINE__ + 1
5
+ @#{accessor} = default
6
+
7
+ def set_#{accessor}(value)
8
+ @#{accessor} = value
9
+ end
10
+ alias #{accessor} set_#{accessor}
11
+
12
+ def get_#{accessor}
13
+ return @#{accessor} if instance_variable_defined?(:@#{accessor})
14
+ superclass.send(:get_#{accessor})
15
+ end
16
+ RUBY
17
+
18
+ # @path = default
19
+ #
20
+ # def set_path(value)
21
+ # @path = value
22
+ # end
23
+ # alias_method path, set_path
24
+
25
+ # def get_path
26
+ # return @path if instance_variable_defined?(:path)
27
+ # superclass.send(:path)
28
+ # end
29
+ end
30
+
31
+ end
32
+
33
+ class Class #:nodoc:
34
+ include ClassEnhancements
35
+ 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